Mountech(Mountai × Tech)

どこにでもいる一般的な男性の、たぶん技術的なブログ。投稿内容は私個人の意見であり、所属企業・部門を代表するものではありません。

Ansibleのloopキーワードで、任意のループ数以上の場合のみ処理を行いたい(小ネタ)

本記事の背景

AWSのEC2インスタンスのAMI取得をAnsibleで実施しようと実装を進めていたところ、AMI取得ついでに世代管理も合わせて行えるようにしてしまおう!と思い立ちました。
パッと思いついた流れは、

  • AMIの一覧を取得し、結果をregisterキーワードで変数に格納
  • 一覧から、定義した保持世代数以上に該当するAMIを登録解除

という、実にシンプルな流れです。
はて、「保持世代数以上に該当するAMI」をどうやって判別するか・・・?
loopキーワードを用いて実装した検証結果をご紹介させていただきます。

検証してみる

検証環境の情報です。

Playbook

---
- hosts: all
  connection: local
  gather_facts: no

  vars:
    ami_keep_generation: 3

  tasks:
    - name: Get AMI list
      ec2_ami_facts:
        filters:
          name: "{{ hostname }}*"
        region: "{{ aws_region }}"
      register: return_ami_list

    - name: Sort AMI list and set to variable
      set_fact:
        ami_list: "{{ return_ami_list.images | sort(attribute='creation_date',reverse=True) }}"

    - name: (debug) Display AMI list
      debug:
        msg: "Name: {{ item.name }}, Id: {{ item.image_id }}, CreationData: {{ item.creation_date }}"
      loop: "{{ ami_list }}"
      loop_control:
        label: "{{ item.name }}"

    - name: (debug) Loop AMI list
      debug:
        msg: "{{ item.name }}"
      when: ansible_loop.index > ami_keep_generation
      loop: "{{ ami_list }}"
      loop_control:
        label: "{{ item.name }}"
        extended: yes

varsセクション内で変数ami_keep_generationを定義し、保持したい世代数を指定します。
以下、一部のタスクを解説します。

Get AMI list

filterのnameにホスト名を指定し、指定したホスト名が含まれるAMIのみ取得するようにしています。
※AMI名は <ホスト名>-yyyymmdd-hhmm としています。

Sort AMI list and set to variable

ec2_ami_factsでAMIの一覧を取得し、作成日時順かつeverse=Trueを指定して逆順にソートするようにしています。
「指定した世代数以上の場合に処理を行う」と決めていたため、AMIが作成された日時順にすることを目的としています。

(debug) Loop AMI list

このタスクが本題です。
loop_controlにてextended: yesを指定すると、ループのカウントを取得できます。
ループカウントは変数ansible_loop.indexに格納されているため、これをwhenで評価します。
条件判定でTrueとなった場合(保持世代数以上に該当した場合)、メッセージでAMIを出力するようにしています。

実行結果

$ ansible-playbook -l webserver-01 GetAMIList.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Get AMI list] *************************************************************************************************************************************************************************************************************************
ok: [webserver-01]

TASK [Sort AMI list and set to variable] ****************************************************************************************************************************************************************************************************
ok: [webserver-01]

TASK [(debug) Display AMI list] *************************************************************************************************************************************************************************************************************
ok: [webserver-01] => (item=webserver-01-20191016-0512) => {
    "msg": "Name: webserver-01-20191016-0512, Id: ami-xxxxxxxxxxxxxxxxxxx, CreationData: 2019-10-16T05:12:25.000Z"
}
ok: [webserver-01] => (item=webserver-01-20191016-0509) => {
    "msg": "Name: webserver-01-20191016-0509, Id: ami-xxxxxxxxxxxxxxxxxxx, CreationData: 2019-10-16T05:09:11.000Z"
}
ok: [webserver-01] => (item=webserver-01-20191016-0500) => {
    "msg": "Name: webserver-01-20191016-0500, Id: ami-xxxxxxxxxxxxxxxxxxx, CreationData: 2019-10-16T05:00:40.000Z"
}
ok: [webserver-01] => (item=webserver-01-20191016-0459) => {
    "msg": "Name: webserver-01-20191016-0459, Id: ami-xxxxxxxxxxxxxxxxxxx, CreationData: 2019-10-16T05:59:54.000Z"
}
ok: [webserver-01] => (item=webserver-01-20191016-0441) => {
    "msg": "Name: webserver-01-20191016-0441, Id: ami-xxxxxxxxxxxxxxxxxxx, CreationData: 2019-10-16T05:41:40.000Z"
}

TASK [(debug) Loop AMI list] ****************************************************************************************************************************************************************************************************************
skipping: [webserver-01] => (item=webserver-01-20191016-0512)
skipping: [webserver-01] => (item=webserver-01-20191016-0509)
skipping: [webserver-01] => (item=webserver-01-20191016-0500)
ok: [webserver-01] => (item=webserver-01-20191016-0459) => {
    "msg": "webserver-01-20191016-0459"
}
ok: [webserver-01] => (item=webserver-01-20191016-0441) => {
    "msg": "webserver-01-20191016-0441"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
webserver-01        : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

AMI名は <ホスト名>-yyyymmdd-hhmm としています。(AMI IDはマスクしています)
TASK [(debug) Loop AMI list]の結果を見ると、3世代以上の場合のみタスクが実行されていることが確認できます。
今回は検証のため実際にAMIの登録解除は行っていませんが、やりたいことを実現できていることは確認できました!
whenとloopを同時に指定した場合、whenが優先されてしまい、動かないのでは・・・?と思い込んでいましたが・・・
実際に手を動かして検証をする大切さを、改めて実感しました。

最後に

実は以前にAnsibleでのAMI世代管理を実施していたのですが、その際はloopキーワードとwhenを組み合わせるという発想がありませんでした。
登録解除したいAMIのリストを作ってしまえばいいじゃない!と思いつき、取得したAMI一覧をjinja2 templateにて無理やりリスト形式に変えて、そのリストをモジュールのパラメータに与える・・・という力技で実装していました。
この情報が役にたつかは不明ですが、こういうこともできます!という意味合いも含めて紹介させていただきます。

- name: Set deregister AMI list to variable
  set_fact:
    deregister_ami_list: >-
      {%- set temp_ami_list = [] -%}
      {%- for count in range(ami_list | length) -%}
      {%-   if loop.index > ami_retain_generation -%}
      {%-     set _ = temp_ami_list.append(ami_list[count].image_id) -%}
      {%-   endif -%}
      {%- endfor -%}
      {{ temp_ami_list }}

行数からすると大したことないですが、少々プログラムチック?な書き方になってしまいました。
個人的には、Ansibleのシンプルさが失われてしまったな〜とモヤモヤしていましたが、その反面、こんなやり方もできるのか・・・と少し感動したのはナイショです。