YAMLのAnchorとAliasをAnsibleで使う

レッドハットでコンサルタントをしている id:nanodayo です。 赤帽エンジニア Advent Calendar 2019の8日目のエントリーです。

ご存知、Ansible のplaybookはYAML形式で記述しますが、YAMLのAnchor と Aliasも適用できるのでご紹介します。 なお、多用すると複雑なplaybookになり保守しにくくなるので強くはおすすめはしません。

YAMLのAnchorとAlias

同じような記述をたくさん書くときに便利な記法です。 以下のような仮想マシンの変数定義(vars)があり、殆ど同じパラメータを設定するので、共通設定として一箇所で書きたいとします。 (説明用に作った物なのでデータ構造は適当です。)

  vars:
     vms:
       - hostname: "test0.example.com"
         image: "cirros"
         flavor: "m1.tiny"
         az: "az1"
         nics:
           - name: "net0"
           - name: "net1"

       - hostname: "test1.example.com"
         image: "cirros"
         flavor: "m1.tiny"
         az: "az1"
         nics:
           - name: "net0"
           - name: "net1"

       - hostname: "test2.example.com"
         image: "cirros"
         flavor: "m1.tiny"
         az: "az1"
         nics:
           - name: "net3"

このような定義の場合、例えばimage の定義を全台違うものにするには3箇所に変更が必要なってしまうので 雛形を決めて、1箇所だけで管理できるようにしたいケースでAnchorとAliasを使います。

---
 - name: yaml anchor and alias test
   hosts: all
   gather_facts: no
   vars:
     vms:
       # Anchorに登録
       - &test
         hostname: "test0.example.com"
         image: "cirros"
         flavor: "m1.tiny"
         az: "az1"
         nics:
           - name: "net0"
           - name: "net1"

       # 全く同じ内容を参照
       - *test

       # 参照先で部分的に上書き
       - hostname: "test1.example.com"
         <<: *test

       # 参照先で部分的に上書き2
       - hostname: "test2.example.com"
         # list はlistごと上書き
         nics:
           - name: "net3"
         <<: *test

   tasks:
     - name: display vms data
       debug:
         var: vms

実行すると以下のようなデータとして扱われていることがわかります。

TASK [display vms data] **********************************************************************************************************************
ok: [localhost] => {
    "vms": [
        {
            "az": "az1",
            "flavor": "m1.tiny",
            "hostname": "test0.example.com",
            "image": "cirros",
            "nics": [
                {
                    "name": "net0"
                },
                {
                    "name": "net1"
                }
            ]
        },
        {
            "az": "az1",
            "flavor": "m1.tiny",
            "hostname": "test0.example.com",
            "image": "cirros",
            "nics": [
                {
                    "name": "net0"
                },
                {
                    "name": "net1"
                }
            ]
        },
        {
            "az": "az1",
            "flavor": "m1.tiny",
            "hostname": "test1.example.com",
            "image": "cirros",
            "nics": [
                {
                    "name": "net0"
                },
                {
                    "name": "net1"
                }
            ]
        },
        {
            "az": "az1",
            "flavor": "m1.tiny",
            "hostname": "test2.example.com",
            "image": "cirros",
            "nics": [
                {
                    "name": "net3"
                }
            ]
        }
    ]
}

test0.example.comの内容をひな形にして参照し、test1.example.com, test2.example.comのように参照先で定義している値で上書きされてる事がわかります。

データ構造全部まるごとAnchorに登録ではなく、部分的に使うこともできます。 部分的なAnchorを追加したサンプルは以下。

   vars:
     vms:
       # Anchorに登録。部分的なものも追加
       - &test
         hostname: "test0.example.com"
         image: &image "cirros"
         flavor: "m1.tiny"
         az: &az "az1"
         nics: &nics
           - name: "net0"
           - name: "net1"

       # 全く同じ内容を持ってくる
       - *test

       # 参照先で部分的に上書き
       - hostname: "test1.example.com"
         <<: *test

       # 参照先で部分的に上書き2
       - hostname: "test2.example.com"
         # list はlistごと上書き
         nics:
           - name: "net3"
         <<: *test

       # 部分的なAnchor を参照
       - hostname: "test3.example.com"
         image: *image
         flavor: "m1.tiny"
         az: *az
         nics: *nics

vars に限らず task の記述に対しても適用ができます。

---
 - name: yaml anchor and alias test
   hosts: all
   gather_facts: no
   tasks:
     - name: sample task block
       block: &task_block
         - &task
           name: sample task in block 1
           debug: &module
             msg: "sample message 1"
           ignore_errors: yes

         - name: sample task in block 2
           debug:
             msg: "sample message 2"
           <<: *task

     - name: sample task out of block
       debug:
         <<: *module

     - name: sample task block(alias)
       block: *task_block

とまぁ一見便利な記法なのですが、Ansible playbookで使う場合に関しては

  • 同じ値を一箇所で定義するだけであれば、varsの値に変数を定義すれば可能
  • 特定のホスト群でのデフォルト値を設定したい場合は group vars や role default 等が登録できる。
  • 複数task をまとめたものを再利用したいならファイルを分けて include_playbook などで読み出せる

と、Ansible側の機能で事足りる場合もあります。

GitLab CIでAnchor Aliasを使う

Ansible以外ですと、GitLab CIのYAMLファイルを記載する際、パイプラインの処理で共通の内容が多く定義するのにお勧めできます。 以下、GitLab CIのパイプラインの定義例です。 細かな説明はしませんが、パイプラインに必要な前処理(before_script)等が、どのパイプラインでも同じというケースが多いです。

.Unit_test_template: &unit_test_template
  stage: unit_test
  only:
    - staging
    - master
  image:
    name: unittest_container
  before_script:
    - eval $(ssh-agent -s)
    - echo "${CICD_SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null
    - echo -e "Host *\n\tPort 22\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
    - ansible -c local  tests/unit_test_presetup.yml
  after_script:
    - ansible -c local  tests/unit_test_cleanup.yml
  tags:
    - container-server

Unit_test1:
  <<: *unit_test_template
  script:
    - bash unit_test1.sh

Unit_test2:
  <<: *unit_test_template
  script:
    - bash unit_test2.sh

なお、GitLab側のドキュメントにもっと良いサンプルが載っています。
https://docs.gitlab.com/ce/ci/yaml/#special-yaml-features

おまけ

Anchor / Alias以外のYAMLの特徴で、Ansibleで使えるか試してみたもの紹介します。

変数の値の型を指定する

通常、シングルクォートやダブルクォート でくくっていれば文字列、そうでなければ数値として解釈されるので、あまり明示的に指定するケースはないと思われますが、以下のように値の前に !!str 等を書けば指定ができるようです。

---
 - name: yaml test
   hosts: all
   gather_facts: no
   vars:
     string_test: !!str 1234
     int_test: !!int 1234
   tasks:
     - name: step1 string_test
       debug:
         msg: "{{ string_test }} is {{ string_test | type_debug }}"

     - name: step2 int_test
       debug:
         msg: "{{ int_test }} is {{int_test | type_debug}}"
...

実行結果

# ansible-playbook -i hosts yaml-test.yml

PLAY [yaml test] *****************************************************************************************************************************

TASK [step1 string_test] *********************************************************************************************************************
ok: [localhost] => {
    "msg": "1234 is AnsibleUnicode"
}

TASK [step2 int_test] ************************************************************************************************************************
ok: [localhost] => {
    "msg": "1234 is int"
}

PLAY RECAP ***********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

... を使って好きな場所で終了(はできない)

YAMLの文法上、--- が開始 ... が終了となっています。 省略も可能なので、特にplaybookでは ... を書かないケースが多いです。

「playbookのデバッグ時に、止めたい箇所に ... を入れれば exit命令的に使えるのでは」と思って試しましたが、... より後にコメントではないものを書くとsyntax errorになります。

#  これは動きません
---
 - name: yaml test
   hosts: all
   gather_facts: no
   vars:
     string_test: !!str 1234
     int_test: !!int 1234
   tasks:
     - name: step1 string_test
       debug:
         msg: "{{ string_test }} is {{ string_test | type_debug }}"
... 
     - name: step2 int_test
       debug:
         msg: "{{ int_test }} is {{int_test | type_debug}}"

yamllintでも同様にNGだったので、YAML的はそういう使い方を想定してないのでしょう。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。