Ansible Playbook
변수, 조건문, 반복문, Handler, Jinja2 Template, 에러 처리, Tags
📚 시리즈 네비게이션
| 이전 | 현재 | 다음 |
|---|---|---|
| Ansible Overview | Playbook | Role |
🎯 Playbook 구조
Playbook은 하나 이상의 Play로 구성되고, 각 Play는 Tasks 목록을 포함함.
---
# 하나의 Playbook 파일에 여러 Play 가능
- name: Play 1 - Web Server 구성
hosts: web
become: yes
tasks:
- name: Task 1
apt:
name: nginx
state: present
- name: Play 2 - DB Server 구성
hosts: db
become: yes
tasks:
- name: Task 1
apt:
name: mariadb-server
state: present
계층 구조
Play 키워드
| 키워드 | 설명 | 예시 |
|---|---|---|
name | Play 이름 (로그에 표시) | Web Server Setup |
hosts | 대상 호스트/그룹 | web, all, web:was |
become | sudo 권한 사용 | yes |
become_user | sudo 대상 사용자 | root |
gather_facts | 서버 정보 수집 여부 | yes (기본), no |
vars | Play 레벨 변수 | 아래 변수 섹션 참고 |
vars_files | 외부 변수 파일 로드 | - vars/main.yml |
tasks | 작업 목록 | 순서대로 실행 |
handlers | 이벤트 트리거 작업 | notify로 호출 |
roles | Role 포함 | - nginx |
tags | 태그 (선택 실행용) | - install |
📝 Task 작성
기본 형식
tasks:
# 모듈 인자를 key: value로
- name: Nginx 설치
apt:
name: nginx
state: present
update_cache: yes
# 한 줄 형식 (간단한 경우)
- name: 파일 삭제
file:
path: /tmp/old-file
state: absent
Task 실행 순서
Tasks는 위에서 아래로 순서대로 실행됨. 하나가 실패하면 이후 Task는 실행되지 않음 (해당 호스트에서).
tasks:
- name: 1. 패키지 업데이트 # 먼저
apt:
update_cache: yes
- name: 2. Nginx 설치 # 다음
apt:
name: nginx
state: present
- name: 3. 서비스 시작 # 마지막
systemd:
name: nginx
state: started
Task 결과 상태
| 상태 | 의미 | 후속 동작 |
|---|---|---|
| ok | 이미 원하는 상태 | handler 트리거 안 됨 |
| changed | 변경 수행됨 | handler 트리거 |
| failed | 실패 | 해당 호스트 중단 |
| skipped | 조건 불일치 | 건너뜀 |
📦 변수 (Variables)
변수 정의 방법
1. Play 내 직접 정의:
- name: Web 구성
hosts: web
vars:
http_port: 80
app_name: myapp
packages:
- nginx
- curl
- vim
tasks:
- name: "{{ app_name }} 패키지 설치"
apt:
name: "{{ packages }}"
state: present
2. vars_files로 분리:
# playbook.yml
- name: Web 구성
hosts: web
vars_files:
- vars/common.yml
- vars/web.yml
# vars/web.yml
http_port: 80
doc_root: /var/www/html
max_clients: 256
3. group_vars / host_vars:
project/
├── inventory.yml
├── group_vars/
│ ├── all.yml # 전체 공통
│ ├── web.yml # web 그룹
│ └── db.yml # db 그룹
└── host_vars/
└── web-01.yml # web-01 호스트 전용
# group_vars/web.yml
http_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
4. 커맨드라인 전달 (최우선):
ansible-playbook site.yml -e "http_port=8080 env=staging"
변수 우선순위 (낮은 순 → 높은 순)
role defaults → inventory vars → group_vars/all
→ group_vars/<group> → host_vars/<host>
→ play vars → play vars_files → task vars
→ extra vars (-e)
💡 extra vars (-e) 가 항상 최우선. 디버깅이나 임시 오버라이드에 유용.
변수 사용 (Jinja2)
tasks:
- name: 설정 파일 배포
template:
src: nginx.conf.j2
dest: "/etc/nginx/nginx.conf"
- name: 포트 확인
debug:
msg: "HTTP 포트: {{ http_port }}"
# 변수가 문장 시작이면 반드시 따옴표
- name: 서비스 시작
systemd:
name: "{{ service_name }}" # O: 따옴표 필수
state: started
⚠️
{{ }}가 YAML 값의 시작에 오면 반드시 따옴표로 감싸야 함. YAML이 딕셔너리로 해석하는 것을 방지함.
Fact 변수
gather_facts: yes (기본) 시 대상 서버의 시스템 정보를 자동 수집.
tasks:
- name: OS 정보 출력
debug:
msg: >
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP: {{ ansible_default_ipv4.address }}
CPU: {{ ansible_processor_vcpus }}코어
RAM: {{ ansible_memtotal_mb }}MB
주요 Fact:
| Fact | 설명 | 예시 |
|---|---|---|
ansible_distribution | OS 배포판 | Ubuntu, CentOS |
ansible_distribution_version | OS 버전 | 22.04 |
ansible_os_family | OS 계열 | Debian, RedHat |
ansible_default_ipv4.address | 기본 IP | 192.168.1.10 |
ansible_hostname | 호스트명 | web-01 |
ansible_processor_vcpus | CPU 코어 수 | 4 |
ansible_memtotal_mb | 전체 메모리 (MB) | 8192 |
ansible_devices | 디스크 정보 | sda, sdb |
# 특정 호스트의 모든 Fact 확인
ansible web-01 -m setup
# 필터링
ansible web-01 -m setup -a "filter=ansible_distribution*"
Register (실행 결과 저장)
Task 실행 결과를 변수에 저장.
tasks:
- name: 디스크 사용량 확인
command: df -h /
register: disk_result
- name: 결과 출력
debug:
msg: "{{ disk_result.stdout_lines }}"
- name: 실패 시 처리
debug:
msg: "명령 실패: {{ disk_result.stderr }}"
when: disk_result.rc != 0
register 변수 구조:
| 속성 | 설명 |
|---|---|
.stdout | 표준 출력 (문자열) |
.stdout_lines | 표준 출력 (라인별 리스트) |
.stderr | 표준 에러 |
.rc | 리턴 코드 (0 = 성공) |
.changed | 변경 여부 (bool) |
.failed | 실패 여부 (bool) |
🔀 조건문 (when)
기본 사용
tasks:
# OS별 패키지 매니저 분기
- name: Nginx 설치 (Debian/Ubuntu)
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Nginx 설치 (RHEL/CentOS)
dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
조건 연산자
tasks:
# 비교
- name: 메모리 부족 경고
debug:
msg: "메모리 {{ ansible_memtotal_mb }}MB - 부족!"
when: ansible_memtotal_mb < 2048
# 논리 연산 (AND)
- name: Ubuntu 22.04만
debug:
msg: "Ubuntu 22.04 확인"
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version == "22.04"
# 리스트는 자동으로 AND
# OR
- name: Debian 계열
debug:
msg: "Debian or Ubuntu"
when: ansible_distribution == "Debian" or ansible_distribution == "Ubuntu"
# NOT
- name: CentOS가 아닌 경우
debug:
msg: "CentOS 아님"
when: ansible_distribution != "CentOS"
# in (포함 여부)
- name: 지원 OS 확인
debug:
msg: "지원 OS"
when: ansible_distribution in ["Ubuntu", "CentOS", "Rocky"]
register + when 조합
tasks:
- name: 설정 파일 존재 확인
stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: 백업 (파일 있을 때만)
copy:
src: /etc/nginx/nginx.conf
dest: /etc/nginx/nginx.conf.bak
remote_src: yes
when: nginx_conf.stat.exists
- name: 서비스 상태 확인
command: systemctl is-active nginx
register: nginx_status
ignore_errors: yes
- name: Nginx 시작 (중지 상태일 때만)
systemd:
name: nginx
state: started
when: nginx_status.rc != 0
🔁 반복문 (Loop)
기본 loop
tasks:
# 여러 패키지 설치
- name: 패키지 설치
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
- vim
- htop
# 위와 동일 (리스트 직접 전달이 더 효율적)
- name: 패키지 설치 (권장)
apt:
name:
- nginx
- curl
- vim
- htop
state: present
💡
apt,yum모듈은 리스트를 직접 받을 수 있어 loop보다 리스트 전달이 더 빠름 (1번의 트랜잭션).
딕셔너리 loop
tasks:
- name: 사용자 생성
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: "{{ item.shell | default('/bin/bash') }}"
state: present
loop:
- { name: deploy, groups: sudo, shell: /bin/bash }
- { name: monitor, groups: monitor }
- { name: backup, groups: backup, shell: /bin/sh }
- name: 가상 호스트 설정
template:
src: vhost.conf.j2
dest: "/etc/nginx/conf.d/{{ item.domain }}.conf"
loop:
- { domain: app1.example.com, port: 8080 }
- { domain: app2.example.com, port: 8081 }
notify: Reload Nginx
loop + when 조합
tasks:
- name: 특정 서비스만 시작
systemd:
name: "{{ item.name }}"
state: started
enabled: yes
loop:
- { name: nginx, enabled: true }
- { name: php-fpm, enabled: true }
- { name: memcached, enabled: false }
when: item.enabled
loop + register
tasks:
- name: 각 서비스 상태 확인
command: "systemctl is-active {{ item }}"
loop:
- nginx
- tomcat
- mariadb
register: service_status
ignore_errors: yes
- name: 결과 출력
debug:
msg: "{{ item.item }}: {{ item.stdout }}"
loop: "{{ service_status.results }}"
loop_control
tasks:
- name: 파일 배포
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: nginx.conf, dest: /etc/nginx/nginx.conf }
- { src: app.conf, dest: /etc/nginx/conf.d/app.conf }
loop_control:
label: "{{ item.dest }}" # 로그에 표시할 내용 (기본: 전체 item)
pause: 1 # 각 반복 사이 대기 (초)
🔔 Handler
변경(changed)이 발생했을 때만 실행되는 특수 Task. 주로 서비스 재시작에 사용.
기본 사용
tasks:
- name: Nginx 설정 배포
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx # changed일 때 handler 호출
- name: 사이트 설정 배포
template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
notify:
- Validate Nginx # 여러 handler 호출 가능
- Reload Nginx
handlers:
- name: Validate Nginx
command: nginx -t
- name: Restart Nginx
systemd:
name: nginx
state: restarted
- name: Reload Nginx
systemd:
name: nginx
state: reloaded
Handler 동작 규칙
| 규칙 | 설명 |
|---|---|
| 실행 시점 | 모든 Task 완료 후 (Play 끝에서) |
| 중복 방지 | 여러 Task가 같은 handler를 notify해도 1번만 실행 |
| 순서 | handler 정의 순서대로 실행 (notify 순서 아님) |
| 조건 | changed 상태일 때만 트리거 |
즉시 실행 (flush_handlers)
기본적으로 handler는 Play 끝에서 실행되지만, 중간에 실행이 필요할 때:
tasks:
- name: 설정 파일 배포
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
# 여기서 handler 즉시 실행
- meta: flush_handlers
- name: Health Check (Nginx 재시작 후 확인)
uri:
url: "http://localhost/health"
status_code: 200
📄 Template (Jinja2)
기본 사용
Jinja2 템플릿으로 동적 설정 파일을 생성.
# playbook.yml
- name: Web 구성
hosts: web
vars:
http_port: 80
server_name: example.com
worker_processes: auto
worker_connections: 1024
tasks:
- name: Nginx 설정 배포
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart Nginx
{# templates/nginx.conf.j2 #}
# Managed by Ansible - Do not edit manually
# Generated: {{ ansible_date_time.iso8601 }}
user nginx;
worker_processes {{ worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ worker_connections }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen {{ http_port }};
server_name {{ server_name }};
location / {
proxy_pass http://{{ was_host | default('127.0.0.1') }}:{{ was_port | default(8080) }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
Jinja2 문법
변수 출력:
{{ variable }}
{{ ansible_hostname }}
{{ http_port }}
주석:
{# 이 줄은 출력되지 않음 #}
조건문:
{% if ansible_os_family == "Debian" %}
# Debian/Ubuntu 설정
include /etc/nginx/modules-enabled/*.conf;
{% elif ansible_os_family == "RedHat" %}
# RHEL/CentOS 설정
include /usr/share/nginx/modules/*.conf;
{% endif %}
반복문:
{% for server in backend_servers %}
server {{ server.host }}:{{ server.port }}{% if server.backup | default(false) %} backup{% endif %};
{% endfor %}
# 변수
backend_servers:
- { host: 192.168.1.20, port: 8080 }
- { host: 192.168.1.21, port: 8080 }
- { host: 192.168.1.22, port: 8080, backup: true }
# 렌더링 결과
server 192.168.1.20:8080;
server 192.168.1.21:8080;
server 192.168.1.22:8080 backup;
주요 필터
| 필터 | 설명 | 예시 |
|---|---|---|
default(val) | 기본값 | {{ var | default('none') }} |
lower / upper | 대소문자 | {{ name | lower }} |
int / float | 타입 변환 | {{ port | int }} |
join(sep) | 리스트 결합 | {{ list | join(', ') }} |
length | 길이 | {{ list | length }} |
regex_replace | 정규식 치환 | {{ str | regex_replace('old', 'new') }} |
to_yaml / to_json | 형식 변환 | {{ dict | to_yaml }} |
ipaddr | IP 주소 처리 | {{ ip | ipaddr('network') }} |
basename | 파일명 추출 | {{ path | basename }} |
dirname | 디렉토리 추출 | {{ path | dirname }} |
password_hash | 패스워드 해시 | {{ pw | password_hash('sha512') }} |
{# 필터 활용 예시 #}
# 기본값
listen {{ http_port | default(80) }};
# 리스트 → 문자열
allow {{ allowed_ips | join('; ') }};
# 패스워드 해시 (사용자 생성 시)
password: {{ user_password | password_hash('sha512') }}
⚠️ 에러 처리
ignore_errors
실패해도 다음 Task 계속 진행:
tasks:
- name: 오래된 서비스 중지 (없어도 OK)
systemd:
name: old-service
state: stopped
ignore_errors: yes
failed_when / changed_when
실패/변경 조건을 커스터마이징:
tasks:
- name: 사용자 확인
command: id deploy
register: user_check
failed_when: false # 절대 실패하지 않음
changed_when: false # 절대 changed 아님
- name: 스크립트 실행
shell: /opt/scripts/deploy.sh
register: deploy_result
failed_when: "'ERROR' in deploy_result.stderr"
changed_when: "'UPDATED' in deploy_result.stdout"
block / rescue / always
try-catch-finally와 유사한 에러 처리:
tasks:
- name: 배포 프로세스
block:
- name: 새 버전 배포
copy:
src: app-v2.war
dest: /opt/tomcat/webapps/app.war
notify: Restart Tomcat
- name: Health Check
uri:
url: "http://localhost:8080/health"
status_code: 200
retries: 5
delay: 10
rescue:
- name: 롤백 (실패 시)
copy:
src: app-v1.war
dest: /opt/tomcat/webapps/app.war
notify: Restart Tomcat
- name: 알림
debug:
msg: "배포 실패 - v1으로 롤백 완료"
always:
- name: 로그 기록 (항상 실행)
shell: echo "Deploy attempt at $(date)" >> /var/log/deploy.log
🏷️ Tags
특정 Task만 선택적으로 실행:
tasks:
- name: 패키지 설치
apt:
name: nginx
state: present
tags:
- install
- nginx
- name: 설정 배포
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- config
- nginx
- name: 서비스 시작
systemd:
name: nginx
state: started
tags:
- service
# install 태그만 실행
ansible-playbook site.yml --tags "install"
# config, service 태그만
ansible-playbook site.yml --tags "config,service"
# install 태그 제외
ansible-playbook site.yml --skip-tags "install"
# 태그 목록 확인
ansible-playbook site.yml --list-tags
📁 파일 & 디렉토리 작업
자주 쓰는 패턴
tasks:
# 디렉토리 생성
- name: 앱 디렉토리 생성
file:
path: /opt/myapp
state: directory
owner: deploy
group: deploy
mode: '0755'
# 파일 복사 (Control → Managed)
- name: 스크립트 배포
copy:
src: files/deploy.sh
dest: /opt/scripts/deploy.sh
owner: root
mode: '0755'
# 원격 파일 복사
- name: 설정 백업
copy:
src: /etc/nginx/nginx.conf
dest: /etc/nginx/nginx.conf.bak
remote_src: yes
# 파일 내 문자열 치환
- name: 포트 변경
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port'
line: 'Port 2222'
notify: Restart SSHD
# 블록 삽입/교체
- name: iptables 규칙 추가
blockinfile:
path: /etc/sysctl.conf
block: |
net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 1
marker: "# {mark} ANSIBLE MANAGED - network"
# 심볼릭 링크
- name: 링크 생성
file:
src: /opt/myapp/current
dest: /var/www/html
state: link
🔄 실행 제어
순서 제어 (pre_tasks / post_tasks)
- name: Web 배포
hosts: web
become: yes
pre_tasks:
- name: 로드밸런서에서 제거
debug:
msg: "LB에서 {{ inventory_hostname }} 제거"
tasks:
- name: 앱 배포
copy:
src: app.war
dest: /opt/tomcat/webapps/
post_tasks:
- name: 로드밸런서에 등록
debug:
msg: "LB에 {{ inventory_hostname }} 등록"
롤링 업데이트 (serial)
한 번에 모든 서버가 아닌 N대씩 순차 배포:
- name: 롤링 배포
hosts: web
become: yes
serial: 1 # 1대씩 (또는 "30%", 2 등)
max_fail_percentage: 0 # 1대라도 실패하면 중단
tasks:
- name: 배포
copy:
src: app.war
dest: /opt/tomcat/webapps/
notify: Restart Tomcat
위임 (delegate_to)
특정 Task를 다른 호스트에서 실행:
tasks:
- name: 로드밸런서에서 제거
command: /usr/local/bin/lb-remove {{ inventory_hostname }}
delegate_to: lb-server
- name: 앱 배포
copy:
src: app.war
dest: /opt/tomcat/webapps/
- name: 로드밸런서에 등록
command: /usr/local/bin/lb-add {{ inventory_hostname }}
delegate_to: lb-server
✅ Playbook 작성 권장 사항
명명 규칙
| 항목 | 권장 | 비권장 |
|---|---|---|
| Task name | Install Nginx package | nginx |
| 변수 | http_port | httpPort, HTTP_PORT |
| Role | nginx, mariadb | Nginx-Role |
| 파일 | site.yml, web.yml | SITE.YML |
멱등성 유지
# 나쁜 예: 멱등성 없음
- name: 사용자 추가
shell: useradd deploy
# 좋은 예: 멱등성 보장
- name: 사용자 추가
user:
name: deploy
state: present
command/shell 최소화
# 나쁜 예
- name: 패키지 설치
shell: apt-get install -y nginx
# 좋은 예
- name: 패키지 설치
apt:
name: nginx
state: present
전용 모듈이 있으면 항상 모듈을 사용.
command/shell은 대응 모듈이 없을 때만.
민감 정보 관리
# Ansible Vault로 암호화
ansible-vault create vars/secrets.yml
ansible-vault edit vars/secrets.yml
# 실행 시 복호화
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# vars/secrets.yml (암호화됨)
db_password: "s3cr3t_p@ssw0rd"
api_key: "abc123def456"
📋 Playbook 예시: 서버 초기 구성
# common.yml - 모든 서버 공통 초기 구성
---
- name: 서버 공통 초기 구성
hosts: all
become: yes
vars:
common_packages:
- curl
- wget
- vim
- htop
- net-tools
- unzip
ntp_server: time.google.com
deploy_user: deploy
tasks:
# 1. 패키지 업데이트 및 설치
- name: 패키지 캐시 업데이트
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: 공통 패키지 설치
apt:
name: "{{ common_packages }}"
state: present
when: ansible_os_family == "Debian"
# 2. 사용자 설정
- name: 배포 사용자 생성
user:
name: "{{ deploy_user }}"
groups: sudo
shell: /bin/bash
create_home: yes
- name: SSH 키 배포
authorized_key:
user: "{{ deploy_user }}"
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
# 3. 시간 동기화
- name: chrony 설치
apt:
name: chrony
state: present
- name: NTP 서버 설정
lineinfile:
path: /etc/chrony/chrony.conf
regexp: '^server'
line: "server {{ ntp_server }} iburst"
notify: Restart chrony
# 4. 보안 설정
- name: SSH root 로그인 비활성화
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
notify: Restart SSHD
- name: SSH 패스워드 인증 비활성화
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: Restart SSHD
# 5. 커널 파라미터
- name: sysctl 튜닝
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
sysctl_set: yes
reload: yes
loop:
- { name: net.core.somaxconn, value: '65535' }
- { name: net.ipv4.tcp_max_syn_backlog, value: '65535' }
- { name: vm.swappiness, value: '10' }
handlers:
- name: Restart chrony
systemd:
name: chrony
state: restarted
- name: Restart SSHD
systemd:
name: sshd
state: restarted
🔗 시리즈 네비게이션
| 이전 | 다음 |
|---|---|
| Ansible Overview | Ansible Role |