이전 포스팅에서
copy모듈로 고정된 설정 파일을 배포하고, handler로 변경 시에만 reload하는 패턴을 실습했습니다. 이번에는 Jinja2 템플릿으로 노드별 맞춤 설정을 생성하고, Role로 playbook을 구조화하는 방법을 다룹니다.
Chap 3에서 copy 모듈로 nginx 설정을 배포했습니다. 그런데 server_name을 각 노드의 호스트명으로 다르게 설정하고 싶다면? 같은 파일을 3개 만들 수는 없습니다.
| copy | template | |
|---|---|---|
| 파일 | 그대로 복사 | 변수를 치환하여 생성 |
| 확장자 | .conf 등 |
.conf.j2 (Jinja2 템플릿) |
| 용도 | 고정된 파일 | 노드별/환경별로 다른 설정 |
Helm의 templates/ 디렉토리에서 {{ .Values.name }}으로 값을 주입하는 것과 동일한 개념입니다. 다만 Helm은 Go template이고, Ansible은 Jinja2(Python)라는 차이가 있습니다. {{ }} 문법이 비슷해서 혼동할 수 있지만 별개의 템플릿 엔진입니다.
💡
.j2확장자는 필수인가?아닙니다.
template모듈은 확장자와 관계없이 파일 내용을 Jinja2로 처리합니다..j2를 붙이는 이유는 일반 파일과 템플릿 파일을 한눈에 구분하기 위한 관례이며, IDE에서 Jinja2 문법 하이라이팅을 자동 적용해주는 이점도 있습니다.
playbook/templates/nginx.conf.j2:
server {
listen {{ nginx_port }};
server_name {{ inventory_hostname }};
location / {
return 200 "Hello from Ansible! I'm {{ inventory_hostname }}";
add_header Content-Type text/plain;
}
}
{{ nginx_port }} — vars에서 정의할 변수{{ inventory_hostname }} — Ansible 예약 변수 (현재 호스트명: node1, node2, node3)playbook/nginx_template.yml:
---
- name: nginx with template
hosts: managed
become: true
gather_facts: false
vars:
nginx_port: 80
tasks:
- name: install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
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
Chap 3의 nginx_with_handler.yml에서 copy → template으로 바꾼 것이 핵심입니다.
최초 작성한 템플릿에서 작은따옴표 안에 I'm을 넣었더니 nginx reload가 실패했습니다:
return 200 'Hello from Ansible! I'm {{ inventory_hostname }}';
RUNNING HANDLER [reload nginx] ****************************************
fatal: [node1]: FAILED! => {"changed": false, "msg": " * Reloading nginx configuration nginx\n ...fail!\n"}
fatal: [node2]: FAILED! => {"changed": false, "msg": " * Reloading nginx configuration nginx\n ...fail!\n"}
원인: I'm의 작은따옴표(')가 nginx 문자열의 닫는 따옴표로 인식되어 nginx 설정 문법 오류가 발생한 것입니다. Ansible 자체의 문제가 아니라 생성된 설정 파일의 문법 오류입니다.
해결: 큰따옴표로 감싸면 해결됩니다:
return 200 "Hello from Ansible! I'm {{ inventory_hostname }}";
💡 템플릿 디버깅 팁
이런 에러가 발생하면 배포된 설정 파일을 직접 확인하거나, 대상 서비스의 설정 검증 명령을 실행하면 원인을 빠르게 찾을 수 있습니다:
# 배포된 설정 파일 확인 ansible managed -m shell -a "cat /etc/nginx/sites-available/default" --become # nginx 설정 문법 검증 ansible managed -m shell -a "nginx -t" --become
TASK [deploy nginx config] ********************************************
changed: [node1]
changed: [node2]
changed: [node3]
RUNNING HANDLER [reload nginx] ****************************************
changed: [node1]
changed: [node2]
changed: [node3]
ansible managed -m shell -a "cat /etc/nginx/sites-available/default"
node1 | CHANGED | rc=0 >>
server {
listen 80;
server_name node1;
location / {
return 200 "Hello from Ansible! I'm node1";
add_header Content-Type text/plain;
}
}
node2 | CHANGED | rc=0 >>
server {
...
server_name node2;
...
return 200 "Hello from Ansible! I'm node2";
...
}
같은 템플릿에서 {{ inventory_hostname }}이 각 노드에서 node1, node2, node3으로 치환되었습니다.
root@control:/workspace/ansible# curl node1
Hello from Ansible! I'm node1
root@control:/workspace/ansible# curl node2
Hello from Ansible! I'm node2
root@control:/workspace/ansible# curl node3
Hello from Ansible! I'm node3
하나의 템플릿으로 노드별 맞춤 설정을 배포할 수 있습니다. 이것이 copy와 template의 차이입니다.
지금까지 만든 playbook들은 하나의 파일에 tasks, handlers, vars, templates가 모두 들어있습니다. 규모가 커지면 관리가 어려워집니다.
Role은 이것들을 정해진 디렉토리 구조로 분리하는 방식입니다. Helm Chart가 templates/, values.yaml, Chart.yaml 등 정해진 구조를 갖는 것과 같습니다.
roles/
nginx/
tasks/main.yml # tasks
handlers/main.yml # handlers
templates/ # 템플릿 파일들
files/ # 정적 파일들
vars/main.yml # 변수 (우선순위 높음)
defaults/main.yml # 기본값 (우선순위 가장 낮음)
각 디렉토리의 main.yml이 자동으로 로드됩니다. playbook에서 일일이 경로를 지정할 필요가 없습니다.
💡
defaultsvsvars
defaults/main.yml vars/main.yml 우선순위 가장 낮음 높음 용도 사용자가 오버라이드할 수 있는 기본값 Role 내부에서 고정으로 쓸 값 Helm의
values.yaml(사용자가 덮어쓸 수 있는 기본값)은defaults에 해당합니다.
기존 구조:
playbook/
nginx_template.yml # tasks + handlers + vars 전부 포함
templates/nginx.conf.j2
Role 구조:
roles/
nginx/
tasks/main.yml
handlers/main.yml
templates/nginx.conf.j2
defaults/main.yml
각 파일 내용:
roles/nginx/defaults/main.yml:
nginx_port: 80
roles/nginx/tasks/main.yml — play 레벨 키워드(hosts, become 등) 없이 task만 작성합니다:
---
- name: install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: deploy nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/default
notify: reload nginx
- name: ensure nginx running
ansible.builtin.service:
name: nginx
state: started
src: nginx.conf.j2만 쓰면 Role의 templates/ 디렉토리에서 자동으로 찾습니다.
roles/nginx/handlers/main.yml:
---
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
playbook (playbook/nginx_role.yml):
---
- name: nginx with role
hosts: managed
become: true
gather_facts: false
roles:
- nginx
playbook이 매우 간결해졌습니다. roles:에 이름만 쓰면 roles/nginx/ 아래의 파일들이 자동으로 로드됩니다.
여러 Role을 사용할 수도 있습니다:
roles:
- role: nginx
vars:
nginx_port: 8080
- role: mysql
쿠버네티스에서 하나의 클러스터에 여러 Helm Chart를 설치하는 것과 같습니다.
ansible-playbook playbook/nginx_role.yml
[ERROR]: the role 'nginx' was not found in
/workspace/ansible/playbook/roles:/root/.ansible/roles:
/usr/share/ansible/roles:/etc/ansible/roles:/workspace/ansible/playbook
원인: Ansible은 기본적으로 playbook 파일 기준으로 roles/ 디렉토리를 탐색합니다. playbook이 /workspace/ansible/playbook/nginx_role.yml에 있으므로 /workspace/ansible/playbook/roles/에서 찾지만, 실제 Role은 /workspace/ansible/roles/에 있습니다.
해결: ansible.cfg에 roles_path를 추가합니다:
[defaults]
inventory = ./inventory.ini
roles_path = ./roles
TASK [nginx : install nginx] *****************************************
ok: [node1]
ok: [node2]
ok: [node3]
TASK [nginx : deploy nginx config] ***********************************
changed: [node1]
changed: [node2]
changed: [node3]
RUNNING HANDLER [nginx : reload nginx] *******************************
changed: [node1]
changed: [node2]
changed: [node3]
PLAY RECAP ***********************************************************
node1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0
node2 : ok=4 changed=2 unreachable=0 failed=0 skipped=0
node3 : ok=4 changed=2 unreachable=0 failed=0 skipped=0
task 이름 앞에 nginx : 가 자동으로 붙습니다 — Role 이름이 prefix로 표시되어 어떤 Role의 task인지 구분할 수 있습니다.
2회차 실행:
PLAY RECAP ***********************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0
node2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0
node3 : ok=3 changed=0 unreachable=0 failed=0 skipped=0
변경 없이 ok=3, changed=0. handler도 실행되지 않았습니다. 멱등성이 Role에서도 동일하게 동작합니다.
ansible/
├── ansible.cfg
├── inventory.ini
├── playbook/
│ ├── files/
│ │ └── nginx.conf
│ ├── templates/
│ │ └── nginx.conf.j2
│ ├── ping.yml
│ ├── install_and_execute.yml
│ ├── install_and_execute_loop.yml
│ ├── nginx_with_handler.yml
│ ├── nginx_template.yml
│ └── nginx_role.yml
└── roles/
└── nginx/
├── defaults/main.yml
├── handlers/main.yml
├── tasks/main.yml
└── templates/nginx.conf.j2
Chap 1~4를 거치면서 하나의 playbook에서 시작해, 변수 → 조건문 → 반복문 → 핸들러 → 템플릿 → Role까지 단계적으로 구조화해왔습니다.