Red Hat Certified Engineer (RHCE) #7 Jinja2 템플릿: 필터, 제어 흐름, lookup
#6 변수와 fact에서 변수와 fact로 호스트마다 다른 값을 다루는 법을 익혔습니다. 그 값들을 실제 설정 파일로 찍어 내려면 템플릿이 필요합니다. Ansible은 Jinja2 템플릿 엔진을 내장하고 있어, 변수가 박힌 .j2 파일을 호스트별로 렌더링해 /etc/hosts나 서버 설정 파일을 동적으로 생성합니다. 이번 글에서는 template 모듈과 Jinja2 문법, 필터, lookup을 시험에 나오는 형태로 정리하겠습니다.
템플릿이 필요한 이유 #
같은 설정 파일이라도 호스트마다 값이 달라야 하는 경우가 많습니다. 웹 서버의 ServerName은 호스트 이름을 따라가야 하고, /etc/hosts는 인벤토리에 든 모든 호스트의 IP와 이름을 담아야 합니다. 이런 파일을 호스트마다 손으로 만들면 멱등성도 깨지고 시간도 많이 듭니다.
해법은 변수가 박힌 템플릿 한 장을 만들고, 호스트별 값으로 렌더링하는 것입니다. copy 모듈은 파일을 그대로 복사하지만, template 모듈은 복사 전에 Jinja2 엔진으로 변수를 치환하고 제어 흐름을 실행합니다. 이것이 copy와 template의 결정적 차이입니다.
template 모듈 #
template 모듈은 제어 노드의 .j2 파일을 렌더링해 관리 노드의 목적지에 씁니다. 관례상 템플릿 파일은 role이나 플레이북 디렉터리의 templates/에 두고, 확장자는 .j2를 붙입니다.
- name: 호스트별 설정 파일 생성
ansible.builtin.template:
src: motd.j2
dest: /etc/motd
owner: root
group: root
mode: '0644'src는 templates/ 기준 상대 경로로 적으면 Ansible이 자동으로 찾습니다. dest는 관리 노드의 절대 경로입니다. owner,group,mode로 소유권과 권한을 함께 지정하며, copy 모듈과 옵션이 거의 같습니다.
validate로 잘못된 설정 차단 #
서버 설정 파일은 문법이 틀리면 서비스가 시작되지 못합니다. template 모듈의 validate는 파일을 목적지에 옮기기 전에 검증 명령을 돌려, 통과한 경우에만 적용합니다. %s 자리에 임시 파일 경로가 들어갑니다.
- name: sshd 설정 배포(검증 후 적용)
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
validate: /usr/sbin/sshd -t -f %s
notify: restart sshdvisudo -cf %s, nginx -t -c %s, httpd -t처럼 서비스마다 검증 명령이 다릅니다. 잘못된 설정으로 서비스가 죽는 사고를 막아 주므로, 설정 배포 task에는 가능하면 validate를 붙이겠습니다.
Jinja2 기본 문법 #
Jinja2에는 세 가지 구분 기호가 있습니다. 이 세 가지만 구분하면 템플릿의 거의 전부를 읽을 수 있습니다.
| 구분 기호 | 용도 |
|---|---|
{{ ... }} | 변수나 식을 출력 |
{% ... %} | for,if 같은 제어문 |
{# ... #} | 주석(출력되지 않음) |
변수 출력 #
{{ }} 안에 변수 이름을 적으면 그 값이 렌더링 결과에 들어갑니다. 점 표기로 딕셔너리나 fact의 하위 값에도 접근합니다.
{# templates/motd.j2 #}
Welcome to {{ inventory_hostname }}.
OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
Managed by Ansible. Do not edit by hand.inventory_hostname은 인벤토리에 적힌 호스트 이름이고, ansible_facts는 #6에서 다룬 자동 수집 fact입니다. 이 템플릿을 template 모듈로 배포하면 각 호스트의 이름과 OS 정보가 박힌 /etc/motd가 생깁니다.
if 제어 흐름 #
{% if %}로 조건에 따라 출력을 바꿉니다. 끝에는 반드시 {% endif %}가 필요합니다.
{% if ansible_facts['memtotal_mb'] > 4096 %}
worker_processes auto;
{% else %}
worker_processes 2;
{% endif %}메모리가 4GB를 넘으면 worker를 자동으로 잡고, 그렇지 않으면 2개로 고정하는 식의 호스트별 분기를 한 템플릿 안에서 처리합니다.
for 반복 #
{% for %}로 리스트나 딕셔너리를 순회합니다. 반복으로 설정 줄을 여러 개 생성하는 패턴이 시험의 단골입니다.
{% for user in allowed_users %}
AllowUsers {{ user }}
{% endfor %}allowed_users가 리스트라면 원소마다 AllowUsers 줄이 하나씩 생깁니다. 반복 안에서는 loop.index(1부터),loop.first,loop.last 같은 특수 변수도 쓸 수 있습니다.
호스트 정보로 /etc/hosts 생성 #
시험에서 가장 자주 나오는 형태가 인벤토리의 모든 호스트로 /etc/hosts를 만드는 작업입니다. magic variable인 groups와 hostvars를 조합합니다.
{# templates/hosts.j2 #}
127.0.0.1 localhost localhost.localdomain
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[host]['inventory_hostname'] }}
{% endfor %}groups['all']은 인벤토리의 모든 호스트 이름 리스트입니다. groups['web']처럼 특정 그룹만 순회할 수도 있습니다. hostvars[host]는 그 호스트의 모든 변수와 fact에 접근하는 통로이므로, 다른 호스트의 IP를 끌어와 한 파일에 모을 수 있습니다.
이 패턴이 동작하려면 순회 대상 호스트들의 fact가 먼저 수집되어 있어야 합니다. 일부 호스트에만 플레이를 돌리면서 전체 호스트의 fact가 필요한 경우, 앞선 플레이에서 모든 호스트를 한 번 거치게 하거나 gather_facts를 켜 두겠습니다.
Jinja2 필터 #
필터는 | 뒤에 붙여 값을 변환합니다. RHCE에서 외워 둘 만한 필터를 정리합니다.
default: 변수가 없을 때 대체값 #
Listen {{ http_port | default(80) }}http_port가 정의되지 않았으면 80을 씁니다. 정의되었더라도 빈 문자열까지 대체하려면 default(80, true)처럼 두 번째 인자를 줍니다.
upper,lower,join: 문자열 가공 #
ENV={{ env_name | upper }}
region={{ aws_region | lower }}
SUPPORTED_DB={{ db_list | join(', ') }}upper,lower는 대소문자를 바꾸고, join은 리스트를 구분자로 이어 붙입니다. db_list가 ['mysql', 'postgres']라면 mysql, postgres로 합쳐지므로, for를 쓰지 않고 한 줄로 리스트를 펼칠 때 편합니다.
length: 개수 세기 #
# total managed hosts: {{ groups['all'] | length }}리스트나 딕셔너리, 문자열의 원소 개수를 반환합니다. when 조건에서 groups['web'] | length > 0처럼 비어 있는지 확인하는 데도 씁니다.
to_nice_yaml: 자료 구조를 YAML로 #
딕셔너리나 리스트를 YAML 형식 문자열로 예쁘게 출력합니다. 설정을 통째로 덤프할 때 유용합니다.
{{ app_config | to_nice_yaml }}비슷하게 to_nice_json은 JSON으로, to_yaml,to_json은 줄바꿈 없이 출력합니다.
mandatory: 변수 누락을 강제 실패 #
db_password={{ vault_db_password | mandatory }}변수가 정의되지 않았으면 default처럼 넘어가지 않고 즉시 오류로 실패시킵니다. 반드시 있어야 하는 값에 붙여, 빈 설정이 조용히 배포되는 사고를 막습니다.
lookup으로 외부 값 읽기 #
lookup은 제어 노드에서 파일이나 환경 변수 같은 외부 소스를 읽어 옵니다. 플레이북 안에서도, 템플릿 안에서도 씁니다.
- name: 로컬 공개키를 변수로 읽기
ansible.builtin.set_fact:
my_pubkey: "{{ lookup('file', '/home/curtis/.ssh/id_rsa.pub') }}"
- name: 환경 변수 읽기
ansible.builtin.debug:
msg: "{{ lookup('env', 'HOME') }}"lookup('file', 경로)는 제어 노드의 파일 내용을 그대로 읽고, lookup('env', 이름)은 제어 노드의 환경 변수를 읽습니다. 템플릿 안에서도 {{ lookup('file', '/etc/banner.txt') }}처럼 같은 방식으로 외부 내용을 끼워 넣습니다.
공백 제어 #
Jinja2의 {% %} 블록은 줄을 차지하므로, for,if를 쓰면 결과에 빈 줄이 남기 쉽습니다. 구분 기호 안쪽에 -를 붙이면 그쪽 방향의 공백과 줄바꿈을 제거합니다.
{% for port in open_ports -%}
-A INPUT -p tcp --dport {{ port }} -j ACCEPT
{% endfor -%}{%-는 앞쪽 공백을, -%}는 뒤쪽 공백을 지웁니다. 설정 파일에 불필요한 빈 줄이 들어가면 안 되는 경우에 깔끔하게 정리할 수 있습니다. 다만 과하게 쓰면 가독성이 떨어지므로, 결과 파일의 형식이 중요할 때만 쓰겠습니다.
전체 예제: 호스트별 웹 서버 설정 #
지금까지 다룬 요소를 한 흐름으로 묶어 보겠습니다. 먼저 템플릿입니다.
{# templates/vhost.conf.j2 #}
{# 호스트별 가상 호스트 설정 #}
<VirtualHost *:{{ http_port | default(80) }}>
ServerName {{ inventory_hostname }}
ServerAdmin {{ admin_email | default('root@localhost') }}
DocumentRoot /var/www/{{ inventory_hostname }}
{% for alias in server_aliases | default([]) %}
ServerAlias {{ alias }}
{% endfor %}
</VirtualHost>다음은 이 템플릿을 배포하는 플레이북입니다.
- name: 웹 서버 가상 호스트 구성
hosts: web
become: true
vars:
http_port: 8080
admin_email: webmaster@example.com
server_aliases:
- www
- app
tasks:
- name: vhost 설정 배포
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/httpd/conf.d/vhost.conf
owner: root
group: root
mode: '0644'
validate: /usr/sbin/httpd -t
notify: restart httpd
handlers:
- name: restart httpd
ansible.builtin.service:
name: httpd
state: restartedweb 그룹의 호스트마다 ServerName과 DocumentRoot가 자신의 호스트 이름으로 채워지고, server_aliases 리스트가 ServerAlias 줄로 펼쳐집니다. validate로 httpd -t 검증을 통과한 경우에만 적용되며, 설정이 바뀐 경우에만 handler가 httpd를 재시작합니다.
시험 포인트 #
- copy와 template의 차이. 변수 치환과 제어 흐름이 필요하면 template, 정적 파일은 copy를 씁니다.
- template의 src는
templates/기준. 절대 경로를 적지 않아도 Ansible이 찾습니다. 확장자는 관례상.j2입니다. - owner,group,mode,validate. 설정 배포 task에는 권한과 검증을 함께 지정하는 습관을 들이겠습니다.
- 세 가지 구분 기호.
{{ }}출력,{% %}제어,{# #}주석을 구분합니다. - groups와 hostvars 조합. 인벤토리 전체로
/etc/hosts를 만드는 유형은 fact 수집 여부까지 확인합니다. - 자주 쓰는 필터.
default,upper,lower,join,length,to_nice_yaml,mandatory를 손에 익힙니다. - lookup.
lookup('file', 경로)와lookup('env', 이름)은 제어 노드 기준으로 읽습니다. - 공백 제어.
-로 for,if가 남기는 빈 줄을 제거합니다.
정리 #
이번 글에서 잡은 것:
- template 모듈로
.j2파일을 호스트별 설정으로 렌더링하고, owner,group,mode,validate를 함께 지정한다 - Jinja2 문법.
{{ }}로 변수 출력,{% if %},{% for %}로 제어 흐름,{# #}로 주석 - 필터. default,upper,lower,join,length,to_nice_yaml,mandatory로 값을 변환하고 누락을 막는다
- groups,hostvars 반복으로 인벤토리 전체를 담는
/etc/hosts나 서버 설정을 생성한다 - lookup으로 제어 노드의 파일과 환경 변수를 읽고, 공백 제어로 결과 형식을 다듬는다
다음: Error handling #
템플릿으로 설정을 동적으로 생성하는 법까지 잡았습니다. 그런데 플레이북은 한 task라도 실패하면 그 호스트의 나머지 task를 멈춥니다. 검증 명령이 실패하거나 특정 조건에서만 오류를 무시해야 하는 상황도 많습니다.
#8 Error handling: block/rescue/always, failed_when, ignore_errors에서는 block/rescue/always로 예외를 구조적으로 처리하는 법, failed_when과 changed_when으로 성공,변경 판정을 직접 정의하는 법, 그리고 ignore_errors로 특정 실패를 넘기는 법을 시험에 나오는 형태로 정리하겠습니다.