Red Hat Certified Engineer (RHCE) #7: Jinja2 テンプレート — フィルター、制御フロー、lookup
#6 変数と fact で、変数と fact を使ってホストごとに異なる値を扱う方法を身につけました。それらの値を実際の設定ファイルに刻み込むには テンプレート が必要です。Ansible は Jinja2 テンプレートエンジンを内蔵しているので、変数が埋め込まれた .j2 ファイルをホストごとにレンダリングして /etc/hosts やサーバー設定ファイルを動的に生成します。今回は template モジュールと Jinja2 構文、フィルター、lookup を試験に出る形で整理します。
テンプレートが必要な理由 #
同じ設定ファイルでも、ホストごとに値が異なる必要がある場合が多いです。ウェブサーバーの ServerName はホスト名に従う必要があり、/etc/hosts はインベントリに含まれるすべてのホストの IP と名前を収める必要があります。こうしたファイルをホストごとに手で作ると、冪等性も崩れ、時間もかかります。
解決策は 変数が埋め込まれたテンプレートを 1 枚作り、ホストごとの値でレンダリングする ことです。copy モジュールはファイルをそのままコピーしますが、template モジュールはコピーする前に Jinja2 エンジンで変数を置換し、制御フローを実行します。これが copy と template の決定的な違いです。
template モジュール #
template モジュールは control node の .j2 ファイルをレンダリングして managed node の目的地に書き込みます。慣例として、テンプレートファイルは role やプレイブックディレクトリの templates/ に置き、拡張子は .j2 を付けます。
- name: 호스트별 설정 파일 생성
ansible.builtin.template:
src: motd.j2
dest: /etc/motd
owner: root
group: root
mode: '0644'src は templates/ を基準とした相対パスで書けば Ansible が自動的に見つけます。dest は managed node の絶対パスです。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 には 3 つの区切り記号があります。この 3 つを見分けられれば、テンプレートのほぼすべてが読めます。
| 区切り記号 | 用途 |
|---|---|
{{ ... }} | 変数や式を出力 |
{% ... %} | 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 個に固定する、といったホストごとの分岐を 1 つのテンプレートの中で処理します。
for 反復 #
{% for %} でリストや辞書を巡回します。反復で設定行を複数生成するパターンは試験の定番です。
{% for user in allowed_users %}
AllowUsers {{ user }}
{% endfor %}allowed_users がリストなら、要素ごとに AllowUsers 行が 1 つずつできます。反復の中では 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 を引っ張ってきて 1 つのファイルに集められます。
このパターンが動作するには、巡回対象のホストたちの fact が先に収集されている必要があります。一部のホストにだけプレイを回しながら全ホストの fact が必要な場合、先行するプレイですべてのホストを一度通すか、gather_facts をオンにしておきます。
Jinja2 フィルター #
フィルターは | の後ろに付けて値を変換します。RHCE で覚えておくに値するフィルターを整理します。
default: 変数がないときの代替値 #
Listen {{ http_port | default(80) }}http_port が定義されていなければ 80 を使います。定義されていても空文字列まで代替するには、default(80, true) のように第 2 引数を渡します。
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 を使わずに 1 行でリストを展開するときに便利です。
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 は control node でファイルや環境変数といった外部ソースを読み込みます。プレイブックの中でも、テンプレートの中でも使います。
- 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', パス) は control node のファイルの内容をそのまま読み、lookup('env', 名前) は control node の環境変数を読みます。テンプレートの中でも {{ lookup('file', '/etc/banner.txt') }} のように同じ方法で外部の内容を差し込みます。
空白制御 #
Jinja2 の {% %} ブロックは行を占めるので、for・if を使うと結果に空行が残りやすいです。区切り記号の内側に - を付けると、その方向の空白と改行を除去します。
{% for port in open_ports -%}
-A INPUT -p tcp --dport {{ port }} -j ACCEPT
{% endfor -%}{%- は前方の空白を、-%} は後方の空白を消します。設定ファイルに不要な空行が入ってはいけない場合に、きれいに整理できます。ただし使いすぎると可読性が落ちるので、結果ファイルの形式が重要なときだけ使います。
全体例: ホストごとのウェブサーバー設定 #
ここまで扱った要素を 1 つの流れにまとめます。まずテンプレートです。
{# 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 には権限と検証を一緒に指定する習慣を付けます。
- 3 つの区切り記号。
{{ }}出力、{% %}制御、{# #}コメントを見分けます。 - groups と hostvars の組み合わせ。インベントリ全体で
/etc/hostsを作る型は、fact 収集の有無まで確認します。 - よく使うフィルター。
default・upper・lower・join・length・to_nice_yaml・mandatoryを手に馴染ませます。 - lookup。
lookup('file', パス)とlookup('env', 名前)は control node 基準で読みます。 - 空白制御。
-で 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 で control node のファイルと環境変数を読み、空白制御 で結果の形式を整える
次へ: Error handling #
テンプレートで設定を動的に生成する方法まで押さえました。ところがプレイブックは、1 つの task でも失敗するとそのホストの残りの task を止めます。検証コマンドが失敗したり、特定の条件でのみエラーを無視しなければならない状況も多いです。
#8 Error handling: block/rescue/always、failed_when、ignore_errors では、block/rescue/always で例外を構造的に処理する方法、failed_when と changed_when で成功・変更の判定を自分で定義する方法、そして ignore_errors で特定の失敗を見送る方法を、試験に出る形で整理します。