🔧

Ansible - 템플릿과 Role

최민석·2026-03-11

템플릿과 Role

🔧 Ansible 실습 시리즈

이전 포스팅에서 copy 모듈로 고정된 설정 파일을 배포하고, handler로 변경 시에만 reload하는 패턴을 실습했습니다. 이번에는 Jinja2 템플릿으로 노드별 맞춤 설정을 생성하고, Role로 playbook을 구조화하는 방법을 다룹니다.


템플릿 (Jinja2 + template 모듈)

Chap 3에서 copy 모듈로 nginx 설정을 배포했습니다. 그런데 server_name을 각 노드의 호스트명으로 다르게 설정하고 싶다면? 같은 파일을 3개 만들 수는 없습니다.

copy vs template

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 작성

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에서 copytemplate으로 바꾼 것이 핵심입니다.

에러: nginx reload 실패

최초 작성한 템플릿에서 작은따옴표 안에 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

하나의 템플릿으로 노드별 맞춤 설정을 배포할 수 있습니다. 이것이 copytemplate의 차이입니다.


Role — playbook 구조화

지금까지 만든 playbook들은 하나의 파일에 tasks, handlers, vars, templates가 모두 들어있습니다. 규모가 커지면 관리가 어려워집니다.

Role은 이것들을 정해진 디렉토리 구조로 분리하는 방식입니다. Helm Chart가 templates/, values.yaml, Chart.yaml 등 정해진 구조를 갖는 것과 같습니다.

Role 디렉토리 구조

roles/
  nginx/
    tasks/main.yml        # tasks
    handlers/main.yml     # handlers
    templates/            # 템플릿 파일들
    files/                # 정적 파일들
    vars/main.yml         # 변수 (우선순위 높음)
    defaults/main.yml     # 기본값 (우선순위 가장 낮음)

각 디렉토리의 main.yml자동으로 로드됩니다. playbook에서 일일이 경로를 지정할 필요가 없습니다.

💡 defaults vs vars

defaults/main.yml vars/main.yml
우선순위 가장 낮음 높음
용도 사용자가 오버라이드할 수 있는 기본값 Role 내부에서 고정으로 쓸 값

Helm의 values.yaml(사용자가 덮어쓸 수 있는 기본값)은 defaults에 해당합니다.

nginx_template.yml을 Role로 변환

기존 구조:

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를 설치하는 것과 같습니다.

에러: Role을 찾을 수 없음

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.cfgroles_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까지 단계적으로 구조화해왔습니다.