Red Hat Certified Engineer (RHCE) #7: Jinja2 テンプレート — フィルター、制御フロー、lookup

読了 9分

#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 を付けます。

template モジュールの基本
- name: 호스트별 설정 파일 생성
  ansible.builtin.template:
    src: motd.j2
    dest: /etc/motd
    owner: root
    group: root
    mode: '0644'

srctemplates/ を基準とした相対パスで書けば Ansible が自動的に見つけます。dest は managed node の絶対パスです。ownergroupmode で所有権と権限を一緒に指定し、copy モジュールとオプションがほぼ同じです。

validate で誤った設定を防ぐ #

サーバー設定ファイルは構文が間違っているとサービスが起動できません。template モジュールの validate は、ファイルを目的地に移す 前に 検証コマンドを走らせ、通過した場合にのみ適用します。%s の位置に一時ファイルのパスが入ります。

validate で検証して配布
- 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 sshd

visudo -cf %snginx -t -c %shttpd -t のように、サービスごとに検証コマンドが異なります。誤った設定でサービスが落ちる事故を防いでくれるので、設定デプロイの task にはできるだけ validate を付けます。

Jinja2 の基本構文 #

Jinja2 には 3 つの区切り記号があります。この 3 つを見分けられれば、テンプレートのほぼすべてが読めます。

区切り記号用途
{{ ... }}変数や式を出力
{% ... %}for・if のような制御文
{# ... #}コメント (出力されない)

変数出力 #

{{ }} の中に変数名を書くと、その値がレンダリング結果に入ります。ドット表記で辞書や fact の下位の値にもアクセスできます。

templates/motd.j2
{# 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 分岐
{% if ansible_facts['memtotal_mb'] > 4096 %}
worker_processes auto;
{% else %}
worker_processes 2;
{% endif %}

メモリが 4GB を超えれば worker を自動で取り、そうでなければ 2 個に固定する、といったホストごとの分岐を 1 つのテンプレートの中で処理します。

for 反復 #

{% for %} でリストや辞書を巡回します。反復で設定行を複数生成するパターンは試験の定番です。

for 反復
{% for user in allowed_users %}
AllowUsers {{ user }}
{% endfor %}

allowed_users がリストなら、要素ごとに AllowUsers 行が 1 つずつできます。反復の中では loop.index (1 から)・loop.firstloop.last といった特殊変数も使えます。

ホスト情報で /etc/hosts を生成 #

試験で最もよく出る形が インベントリのすべてのホストで /etc/hosts を作る 作業です。magic variable の groupshostvars を組み合わせます。

templates/hosts.j2
{# 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: 変数がないときの代替値 #

default フィルター
Listen {{ http_port | default(80) }}

http_port が定義されていなければ 80 を使います。定義されていても空文字列まで代替するには、default(80, true) のように第 2 引数を渡します。

upper・lower・join: 文字列の加工 #

upper・lower・join フィルター
ENV={{ env_name | upper }}
region={{ aws_region | lower }}
SUPPORTED_DB={{ db_list | join(', ') }}

upperlower は大文字小文字を変え、join はリストを区切り文字でつなぎ合わせます。db_list['mysql', 'postgres'] なら mysql, postgres に結合されるので、for を使わずに 1 行でリストを展開するときに便利です。

length: 個数を数える #

length フィルター
# total managed hosts: {{ groups['all'] | length }}

リストや辞書、文字列の要素数を返します。when 条件で groups['web'] | length > 0 のように、空かどうかを確認するのにも使います。

to_nice_yaml: データ構造を YAML に #

辞書やリストを YAML 形式の文字列としてきれいに出力します。設定をまるごとダンプするときに役立ちます。

to_nice_yaml フィルター
{{ app_config | to_nice_yaml }}

同様に to_nice_json は JSON に、to_yamlto_json は改行なしで出力します。

mandatory: 変数の欠落を強制失敗 #

mandatory フィルター
db_password={{ vault_db_password | mandatory }}

変数が定義されていなければ default のように素通りせず、即座にエラーで失敗 させます。必ず存在しなければならない値に付けて、空の設定が静かにデプロイされる事故を防ぎます。

lookup で外部の値を読む #

lookup は control node でファイルや環境変数といった外部ソースを読み込みます。プレイブックの中でも、テンプレートの中でも使います。

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', パス) は 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
{# 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>

次は、このテンプレートをデプロイするプレイブックです。

vhost 配布プレイブック
- 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: restarted

web グループのホストごとに ServerNameDocumentRoot が自分のホスト名で埋まり、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 収集の有無まで確認します。
  • よく使うフィルターdefaultupperlowerjoinlengthto_nice_yamlmandatory を手に馴染ませます。
  • lookuplookup('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_whenchanged_when で成功・変更の判定を自分で定義する方法、そして ignore_errors で特定の失敗を見送る方法を、試験に出る形で整理します。

X