이전 포스팅에서 변수와 조건문으로 playbook을 유연하게 만들었습니다. 이번에는 **반복문(loop)**으로 여러 패키지를 한 번에 처리하고, **핸들러(handler)**로 설정 변경 시에만 서비스를 reload하는 패턴을 실습합니다.
Chap 2에서 작성한 install_and_execute.yml은 패키지를 하나만 설치합니다. 여러 패키지를 설치하려면 어떻게 해야 할까요?
tasks:
- name: install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: install curl
ansible.builtin.apt:
name: curl
state: present
- name: install vim
ansible.builtin.apt:
name: vim
state: present
패키지마다 task를 복사해야 합니다. 쿠버네티스에서 동일 구조의 Pod을 여러 개 만들기 위해 Deployment의 replicas를 쓰듯, Ansible에서는 loop으로 해결합니다.
tasks:
- name: install packages
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
- vim
loop: — 리스트를 순회{{ item }} — 현재 반복 요소를 참조하는 예약 변수💡 예약 변수
item
item은 사용자가 정의하는 변수가 아니라, Ansible이loop실행 시 자동으로 생성하는 예약 변수입니다. loop이 리스트를 순회할 때마다 현재 요소가item에 할당됩니다.쿠버네티스의 Helm 템플릿에서
{{ range }}안의.(현재 요소)와 같은 역할입니다.Ansible에는 이 외에도 여러 예약 변수가 있습니다:
예약 변수 설명 itemloop에서 현재 반복 요소 inventory_hostname현재 task를 실행 중인 호스트 이름 ansible_factsgather_facts로 수집된 시스템 정보 hostvars다른 호스트의 변수에 접근 groups인벤토리의 모든 그룹과 호스트 목록 이런 예약 변수는
vars:로 재정의하면 안 됩니다. 예약 변수와 같은 이름으로 변수를 만들면 의도치 않은 동작이 발생할 수 있습니다.
리스트를 vars로 분리하면 더 깔끔합니다:
vars:
packages:
- nginx
- curl
- vim
tasks:
- name: "install {{ item }}"
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
Chap 2에서 배운 조건문과 함께 쓰면, 패키지별로 서비스 시작 여부를 분기할 수 있습니다:
---
- name: "Ansible loop install & execute"
hosts: managed
gather_facts: false
vars:
service_packages:
- name: nginx
service: true
- name: curl
service: false
- name: vim
service: false
tasks:
- name: "install package"
become: true
ansible.builtin.apt:
name: "{{ item.name }}"
state: present
loop: "{{ service_packages }}"
- name: "service start"
become: true
ansible.builtin.service:
name: "{{ item.name }}"
state: started
when: item.service == true
loop: "{{ service_packages }}"
리스트 요소를 딕셔너리로 만들면 item.name, item.service처럼 속성에 접근할 수 있습니다.
⚠️
name:필드에서{{ item }}을 쓸 수 없다task의
name:은 loop 실행 이전에 평가됩니다. 이 시점에item은 아직 정의되지 않았기 때문에 에러가 발생합니다.# 에러 발생 - name: "install {{ item.name }}" # OK — 고정 문자열 사용 - name: "install package"대신 실행 로그에
(item=...)으로 현재 항목이 표시되므로, 어떤 항목을 처리 중인지 구분할 수 있습니다.
ansible-playbook playbook/install_and_execute_loop.yml
TASK [install package] ************************************************
ok: [node3] => (item={'name': 'nginx', 'service': True})
ok: [node1] => (item={'name': 'nginx', 'service': True})
ok: [node2] => (item={'name': 'nginx', 'service': True})
ok: [node3] => (item={'name': 'curl', 'service': False})
ok: [node1] => (item={'name': 'curl', 'service': False})
ok: [node2] => (item={'name': 'curl', 'service': False})
ok: [node3] => (item={'name': 'vim', 'service': False})
ok: [node1] => (item={'name': 'vim', 'service': False})
ok: [node2] => (item={'name': 'vim', 'service': False})
TASK [service start] **************************************************
ok: [node3] => (item={'name': 'nginx', 'service': True})
skipping: [node3] => (item={'name': 'curl', 'service': False})
skipping: [node3] => (item={'name': 'vim', 'service': False})
ok: [node1] => (item={'name': 'nginx', 'service': True})
skipping: [node1] => (item={'name': 'curl', 'service': False})
skipping: [node1] => (item={'name': 'vim', 'service': False})
ok: [node2] => (item={'name': 'nginx', 'service': True})
skipping: [node2] => (item={'name': 'curl', 'service': False})
skipping: [node2] => (item={'name': 'vim', 'service': False})
service: true인 nginx만 서비스가 시작되고, curl과 vim은 skipping으로 건너뛰었습니다.
지금까지의 패턴은 "설치 → 서비스 시작"이었습니다. 실무에서는 여기에 설정 파일 배포가 추가됩니다:
패키지 설치 → 설정 파일 배포 → 서비스 시작/재시작
문제는, 설정 파일이 변경되지 않았는데도 매번 서비스를 재시작하면 불필요한 다운타임이 발생한다는 점입니다.
쿠버네티스에서 ConfigMap이 변경되었을 때만 Pod이 롤링 업데이트되는 것처럼, Ansible에서는 handler가 이 역할을 합니다.
tasks:
- name: nginx 설정 복사
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
notify: reload nginx # 변경이 있을 때만(changed) 핸들러 호출
handlers:
- name: reload nginx # notify의 이름과 일치해야 함
ansible.builtin.service:
name: nginx
state: reloaded
notify: — 해당 task가 changed일 때만 핸들러를 호출handlers: — tasks:와 같은 레벨에 정의. 모든 task 실행이 끝난 뒤 마지막에 실행됨ok(변경 없음) → handler 실행 안 됨changed(변경 있음) → handler 실행됨💡
reloadedvsrestarted
reloaded— 프로세스를 재시작하지 않고 설정만 다시 읽음. 다운타임 없음restarted— 프로세스를 종료 후 재시작. 바이너리 업그레이드 등 재시작이 필요한 경우에 사용설정 파일 변경만이라면
reloaded가 적절합니다.
설정 파일 (playbook/files/nginx.conf):
server {
listen 80;
server_name localhost;
location / {
return 200 'Hello from Ansible!';
add_header Content-Type text/plain;
}
}
playbook (playbook/nginx_with_handler.yml):
---
- name: nginx install and configure
hosts: managed
become: true
gather_facts: false
tasks:
- name: install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: copy nginx config
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/sites-available/default
notify: reload nginx
- name: ensure nginx running
ansible.builtin.service:
name: nginx
state: started
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
💡
ensure nginx running과 handler는 역할이 다르다
역할 실행 조건 ensure nginx running(task)nginx가 실행 중인 상태를 보장 매번 (이미 실행 중이면 ok)reload nginx(handler)설정 변경을 반영 copy가changed일 때만
start는 "실행 중 상태 보장",reload는 "설정 변경 반영"으로 목적이 다릅니다.
1회차 — 설정 파일 최초 배포:
TASK [copy nginx config] **********************************************
changed: [node1]
changed: [node2]
changed: [node3]
RUNNING HANDLER [reload nginx] ****************************************
changed: [node1]
changed: [node2]
changed: [node3]
PLAY RECAP ************************************************************
node1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0
새 설정 파일이 복사되어 changed → handler 실행됨.
2회차 — 변경 없이 재실행:
TASK [copy nginx config] **********************************************
ok: [node1]
ok: [node2]
ok: [node3]
PLAY RECAP ************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0
설정 파일이 동일하여 ok → handler 실행 안 됨. 불필요한 reload이 발생하지 않습니다.
3회차 — 설정 파일 수정 후 재실행:
nginx.conf의 응답 메시지를 'Hello from Ansible! 2nd version!'으로 수정한 뒤:
TASK [copy nginx config] **********************************************
changed: [node1]
changed: [node2]
changed: [node3]
RUNNING HANDLER [reload nginx] ****************************************
changed: [node1]
changed: [node2]
changed: [node3]
PLAY RECAP ************************************************************
node1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0
변경이 감지되어 다시 handler가 실행되었습니다.
| 실행 | copy 결과 | handler 실행 | 이유 |
|---|---|---|---|
| 1회차 | changed | 실행됨 | 새 설정 파일 배포 |
| 2회차 | ok | 실행 안 됨 | 파일 동일 |
| 3회차 | changed | 실행됨 | 파일 수정 |
이것이 handler의 핵심입니다 — 변경이 있을 때만 서비스를 reload하여 불필요한 다운타임을 방지합니다.