Skip to main content

Ansible Playbook

변수, 조건문, 반복문, Handler, Jinja2 Template, 에러 처리, Tags


📚 시리즈 네비게이션

이전현재다음
Ansible OverviewPlaybookRole

🎯 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 키워드

키워드설명예시
namePlay 이름 (로그에 표시)Web Server Setup
hosts대상 호스트/그룹web, all, web:was
becomesudo 권한 사용yes
become_usersudo 대상 사용자root
gather_facts서버 정보 수집 여부yes (기본), no
varsPlay 레벨 변수아래 변수 섹션 참고
vars_files외부 변수 파일 로드- vars/main.yml
tasks작업 목록순서대로 실행
handlers이벤트 트리거 작업notify로 호출
rolesRole 포함- 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_distributionOS 배포판Ubuntu, CentOS
ansible_distribution_versionOS 버전22.04
ansible_os_familyOS 계열Debian, RedHat
ansible_default_ipv4.address기본 IP192.168.1.10
ansible_hostname호스트명web-01
ansible_processor_vcpusCPU 코어 수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 }}
ipaddrIP 주소 처리{{ 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 nameInstall Nginx packagenginx
변수http_porthttpPort, HTTP_PORT
Rolenginx, mariadbNginx-Role
파일site.yml, web.ymlSITE.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 OverviewAnsible Role

🔗 참고 자료