Red Hat Certified Engineer (RHCE) #5 Playbook 기초: task, handler, 멱등성
#4 ad-hoc 명령에서 모듈 하나를 즉석에서 실행해 호스트 상태를 바꿔 보았다면, 이번에는 그 작업들을 파일로 묶어 선언적으로 구성하는 playbook으로 넘어갑니다. RHCE 실기에서 채점되는 산출물은 결국 playbook입니다. 한 번 작성해 두면 같은 결과를 몇 번이고 재현할 수 있어야 하고, 그 재현성을 보장하는 성질이 바로 멱등성입니다. 이번 글에서는 playbook의 구조와 task 작성, handler와 notify, 그리고 멱등성 검증까지 실기 관점으로 정리하겠습니다.
playbook이란 #
playbook은 여러 task를 순서대로 정의한 YAML 파일입니다. ad-hoc 명령이 한 모듈을 한 번 실행하는 일회성 도구라면, playbook은 같은 작업을 파일로 보존해 반복 실행하고 버전 관리할 수 있는 형태입니다. 채점관은 시험장에서 여러분이 작성한 playbook을 실행해 결과를 확인하므로, RHCE 실기의 거의 모든 답안은 playbook으로 제출합니다.
playbook은 하나 이상의 play로 구성되고, 각 play는 어떤 호스트에 어떤 task를 적용할지를 정의합니다. play 안에는 적용 대상(hosts), 권한 상승(become), 그리고 실제로 수행할 작업 목록(tasks)이 들어갑니다.
playbook의 YAML 구조 #
가장 단순한 playbook 한 편을 보겠습니다.
---
- name: 웹 서버 기본 구성
hosts: webservers
become: true
tasks:
- name: httpd 패키지 설치
ansible.builtin.dnf:
name: httpd
state: present
- name: httpd 서비스 시작 및 부팅 시 활성화
ansible.builtin.service:
name: httpd
state: started
enabled: true이 파일의 구조를 분해하면 다음과 같습니다.
- 파일 맨 위의
---는 YAML 문서의 시작을 표시합니다. - 최상위는 play의 목록입니다. 위 예시에는 play가 하나뿐이라
-로 시작하는 항목이 하나입니다. - 각 play는 적용 대상
hosts, 권한 상승 여부become, 작업 목록tasks를 가집니다. tasks아래의 각 항목이 하나의 task이며, task마다 모듈 하나를 호출합니다.
play와 task의 관계 #
play는 호스트 집합에 적용되는 작업 묶음입니다. 하나의 playbook 안에 play를 여러 개 둘 수도 있고, 각 play가 서로 다른 호스트 그룹을 대상으로 할 수도 있습니다.
---
- name: 웹 서버 구성
hosts: webservers
become: true
tasks:
- name: httpd 설치
ansible.builtin.dnf:
name: httpd
state: present
- name: 데이터베이스 서버 구성
hosts: dbservers
become: true
tasks:
- name: mariadb 설치
ansible.builtin.dnf:
name: mariadb-server
state: present위 playbook은 play 두 개를 담고 있습니다. 첫 play는 webservers 그룹에, 둘째 play는 dbservers 그룹에 적용됩니다. play는 정의된 순서대로 위에서 아래로 실행됩니다.
YAML 들여쓰기 주의 #
YAML은 들여쓰기로 구조를 표현하므로, 공백 개수가 어긋나면 파일 전체가 깨집니다. 다음 규칙을 지키면 시험장에서 사소한 문법 오류로 시간을 잃지 않습니다.
- 들여쓰기는 스페이스만 사용하고 탭은 쓰지 않습니다.
- 같은 수준의 항목은 들여쓰기를 정확히 맞춥니다.
- 콜론 뒤에는 공백을 한 칸 둡니다(
name: httpd). - 목록 항목은
-뒤에 공백을 한 칸 둡니다.
task 작성 #
task는 모듈 하나의 호출입니다. 각 task는 어떤 모듈을, 어떤 인자로 실행할지를 정의합니다. ad-hoc 명령에서 쓰던 모듈과 옵션이 그대로 task로 옮겨 온다고 보면 됩니다.
tasks:
- name: chrony 패키지 설치
ansible.builtin.dnf:
name: chrony
state: present위 task는 ansible.builtin.dnf 모듈을 호출해 chrony 패키지를 present 상태로 만듭니다. name은 task의 설명이고, 그 아래가 모듈 이름과 인자입니다.
name을 붙이는 습관 #
task에 name은 선택이지만, 실기에서는 항상 붙이는 편이 유리합니다. 실행 출력에 name이 그대로 표시되므로, 어떤 task에서 무엇이 바뀌었는지 한눈에 파악할 수 있습니다. 디버깅 시간과 채점 전 자기 점검 시간을 모두 줄여 줍니다.
TASK [chrony 패키지 설치] ********************************************
ok: [server1]name을 생략하면 출력에 모듈 이름만 나와 task를 식별하기 어렵습니다. 모든 task에 의미 있는 name을 붙이는 습관을 들이겠습니다.
FQCN 권장 #
ansible.builtin.dnf처럼 네임스페이스.컬렉션.모듈 형태로 모듈을 적는 표기를 FQCN(Fully Qualified Collection Name)이라고 합니다. 짧게 dnf라고만 적어도 동작하지만, 어떤 컬렉션의 모듈인지 명확히 드러나고 이름 충돌을 피할 수 있어 FQCN을 권장합니다. 시험장에서도 FQCN으로 쓰면 안전합니다.
handler와 notify #
서비스 설정 파일을 바꾼 뒤에는 서비스를 다시 시작해야 변경이 반영됩니다. 그런데 설정이 바뀌지 않았다면 굳이 재시작할 필요가 없습니다. 이 “변경이 일어났을 때만 무언가를 실행"하는 패턴을 위해 Ansible은 handler를 제공합니다.
handler는 일반 task와 같은 모양이지만, play의 handlers 절에 따로 정의하고 notify로 호출될 때만 실행됩니다.
---
- name: 웹 서버 구성
hosts: webservers
become: true
tasks:
- name: httpd 설치
ansible.builtin.dnf:
name: httpd
state: present
- name: httpd 설정 배포
ansible.builtin.copy:
src: files/httpd.conf
dest: /etc/httpd/conf/httpd.conf
owner: root
group: root
mode: '0644'
notify: restart httpd
handlers:
- name: restart httpd
ansible.builtin.service:
name: httpd
state: restarted위 playbook의 동작을 짚어 보겠습니다.
httpd 설정 배포task가 실제로 파일을 바꾸면(changed)notify로restart httpdhandler를 호출합니다.- 파일이 이미 같은 내용이라 바뀌지 않으면(ok) handler는 호출되지 않습니다.
- 호출된 handler는 곧바로 실행되지 않고, play의 모든 task가 끝난 뒤 한 번만 실행됩니다.
handler의 두 가지 핵심 성질 #
handler를 정확히 이해하려면 두 가지를 기억하면 됩니다.
- 변경이 있을 때만 실행됩니다. notify를 거는 task가 changed가 되어야 handler가 깨어납니다. 같은 설정을 그대로 두는 두 번째 실행에서는 task가 ok가 되므로 handler도 돌지 않습니다.
- 끝에 한 번만 실행됩니다. 여러 task가 같은 handler를 notify해도, 그 handler는 play 끝에 단 한 번 실행됩니다. 설정 파일을 세 군데 바꿔도 서비스 재시작은 한 번이면 충분하다는 실제 운영 감각과 맞아떨어집니다.
이 패턴 덕분에 “설정이 바뀐 경우에만 서비스를 재시작"하는 동작을 멱등성을 깨지 않고 표현할 수 있습니다.
멱등성 #
멱등성(idempotency)은 같은 playbook을 몇 번 실행해도 결과가 같음을 보장하는 성질입니다. 첫 실행에서 시스템을 원하는 상태로 맞추고, 두 번째 실행부터는 이미 그 상태이므로 아무것도 바꾸지 않습니다. RHCE 채점의 핵심 기준이 바로 이 멱등성입니다.
changed와 ok #
playbook 실행 출력에는 각 task가 무엇을 했는지가 색과 단어로 표시됩니다.
- ok. 이미 원하는 상태라서 바꿀 것이 없었음을 뜻합니다.
- changed. task가 실제로 시스템 상태를 변경했음을 뜻합니다.
- failed. task가 실패했음을 뜻합니다.
잘 작성된 playbook은 첫 실행에서 changed가 나오고, 두 번째 실행에서는 changed가 0이 되고 모두 ok가 됩니다. 두 번째 실행에서도 changed가 계속 나온다면 그 task는 멱등성이 깨져 있다는 신호입니다.
실행이 끝나면 마지막에 PLAY RECAP이 요약을 보여 줍니다.
PLAY RECAP **********************************************************
server1 : ok=3 changed=0 unreachable=0 failed=0두 번째 실행에서 changed=0이 나오면 멱등성이 보장되었다는 뜻입니다.
command와 shell은 멱등성이 없다 #
대부분의 모듈은 “원하는 상태"를 선언하므로 자동으로 멱등합니다. dnf는 이미 설치된 패키지를 다시 설치하지 않고, service는 이미 실행 중인 서비스를 다시 시작하지 않습니다.
반면 command와 shell 모듈은 현재 상태를 따지지 않고 매번 명령을 실행합니다. 그래서 멱등성이 없으며, 두 번째 실행에서도 항상 changed가 됩니다.
- name: 잘못된 예시 - 매번 실행됨
ansible.builtin.command: useradd appuser위 task는 두 번째 실행에서 사용자가 이미 있어 오류가 나거나, 매번 changed로 표시됩니다. 가능하면 command,shell 대신 전용 모듈(user, dnf, service 등)을 쓰는 것이 정답입니다.
creates와 removes로 보완 #
전용 모듈이 없어 부득이 command나 shell을 써야 한다면, creates와 removes 옵션으로 멱등성을 흉내 낼 수 있습니다.
- creates. 지정한 파일이 이미 있으면 명령을 실행하지 않습니다.
- removes. 지정한 파일이 없으면 명령을 실행하지 않습니다.
- name: 초기화 스크립트 실행 (한 번만)
ansible.builtin.command: /usr/local/bin/init-db.sh
args:
creates: /var/lib/myapp/initialized위 task는 /var/lib/myapp/initialized 파일이 없을 때만 스크립트를 실행하고, 파일이 생긴 뒤로는 건너뜁니다. 스크립트가 그 파일을 만들도록 작성해 두면, 두 번째 실행부터는 ok로 표시되어 멱등성이 유지됩니다.
playbook 실행 #
작성한 playbook은 두 가지 방식으로 실행할 수 있습니다.
# 전통적인 실행기
ansible-playbook site.yml
# execution environment 기반 실행기 (출력을 기존과 유사하게)
ansible-navigator run site.yml -m stdout#1에서 보았듯 시험 환경에 따라 둘 중 어느 쪽이든 쓸 수 있으니, 익숙한 쪽을 정해 두되 ansible-navigator run -m stdout 옵션도 익혀 두겠습니다.
–syntax-check로 문법 먼저 #
실행 전에 YAML 문법과 playbook 구조를 빠르게 검사하면 사소한 오류를 미리 잡을 수 있습니다.
ansible-playbook --syntax-check site.yml오류가 없으면 playbook 이름만 출력됩니다. 실기에서 답안을 제출하기 전 가벼운 확인 절차로 권장합니다.
–check로 dry run #
--check는 실제로 시스템을 바꾸지 않고 무엇이 바뀔지를 미리 보여 주는 dry run 모드입니다. 위험한 변경을 적용 전에 점검하거나, 두 번째 실행에서 changed가 나올 task를 미리 찾는 데 유용합니다.
ansible-playbook --check site.yml–diff로 변경 내용 확인 #
--diff는 파일이 어떻게 바뀌는지 줄 단위 차이를 보여 줍니다. --check와 함께 쓰면, 적용하지 않은 채 어떤 줄이 바뀔지까지 확인할 수 있습니다.
ansible-playbook --check --diff site.yml설정 파일을 다루는 task가 의도대로 동작하는지 확인할 때 특히 유용합니다.
종합 예제 #
지금까지 다룬 요소를 모은 playbook 한 편을 보겠습니다. 패키지를 설치하고, 설정 파일을 배포하고, 설정이 바뀐 경우에만 handler로 서비스를 재시작합니다.
---
- name: chrony 시간 동기화 구성
hosts: all
become: true
tasks:
- name: chrony 패키지 설치
ansible.builtin.dnf:
name: chrony
state: present
- name: chrony 설정 배포
ansible.builtin.copy:
src: files/chrony.conf
dest: /etc/chrony.conf
owner: root
group: root
mode: '0644'
notify: restart chronyd
- name: chronyd 서비스 시작 및 활성화
ansible.builtin.service:
name: chronyd
state: started
enabled: true
handlers:
- name: restart chronyd
ansible.builtin.service:
name: chronyd
state: restarted이 playbook을 두 번 실행하며 멱등성을 검증하는 흐름은 다음과 같습니다.
# 1) 문법 확인
ansible-playbook --syntax-check site.yml
# 2) 첫 실행 - changed가 나옴
ansible-playbook site.yml
# 3) 두 번째 실행 - changed=0이어야 정상
ansible-playbook site.yml첫 실행에서는 패키지 설치,설정 배포,서비스 시작이 changed로 표시되고, 설정이 바뀌었으므로 restart chronyd handler가 play 끝에 한 번 실행됩니다. 두 번째 실행에서는 모든 task가 ok가 되고 handler는 호출되지 않으며, PLAY RECAP에 changed=0이 나옵니다. 이 두 번 실행 검증을 모든 답안에 적용하는 습관이 RHCE 합격의 토대입니다.
시험 포인트 #
- handler는 notify로만 깨어나고, 변경이 있을 때만, play 끝에 한 번 실행됩니다. 설정 배포 task에
notify를 걸고 handler에서 서비스를 재시작하는 패턴을 손에 익혀 두겠습니다. - 멱등성은 두 번 실행으로 검증합니다. 두 번째 실행에서
changed=0이 나오는지 PLAY RECAP으로 확인하는 절차를 답안마다 거치겠습니다. - command,shell은 멱등성이 없습니다. 전용 모듈로 바꾸는 것이 우선이고, 불가피하면
creates,removes로 보완하겠습니다. - 모든 task에 name을 붙이고 FQCN을 사용해 출력 가독성과 정확성을 확보하겠습니다.
- 제출 전
--syntax-check로 문법을 확인하고, 필요하면--check,--diff로 변경 내용을 미리 점검하겠습니다.
정리 #
이번 글에서 잡은 것:
- playbook은 play의 목록이고, play는
hosts,become,tasks로 호스트 집합에 작업을 적용합니다. - task는 모듈 하나의 호출이며, 가독성과 채점 점검을 위해 항상
name을 붙이고 FQCN으로 작성합니다. - handler와 notify로 변경이 있을 때만, play 끝에 한 번 서비스를 재시작하는 패턴을 구성합니다.
- 멱등성은 RHCE의 핵심 기준이며, 두 번 실행해
changed=0을 확인합니다. command,shell은 멱등성이 없으므로 전용 모듈이나creates,removes로 보완합니다. - 실행은
ansible-playbook또는ansible-navigator run -m stdout으로 하고,--syntax-check,--check,--diff로 미리 점검합니다.
다음: 변수와 fact #
playbook의 뼈대는 잡았습니다. 이제 같은 playbook을 호스트마다 다르게 동작시키는 변수와, 시스템에서 자동으로 수집되는 fact로 넘어갑니다.
#6 변수와 fact: 우선순위, magic vars, custom facts에서는 변수를 정의하는 여러 위치와 그 우선순위, ansible_facts로 수집되는 시스템 정보, hostvars,inventory_hostname 같은 magic variable, 그리고 custom fact를 직접 추가하는 방법까지 정리하겠습니다.