Ansibleのタスクを適度に待たせて適切なタイミングで実行したい

この記事は #インフラ勉強会 Advent Calendar 10日目の記事です

https://adventar.org/calendars/3022#list-2018-12-10

技術系の話をしようかエモい系の話をしようかと迷いましたが、
エモい話で1枚書き切るほどネタが出てこなさそうだし、
気の利いたインフラエンジニアのための〇〇もパッとは浮かばないので、
フツーに最近よく使ってるAnsibleについて書きます。（なのでQiitaにしました）
一応最後にポエムにならない程度にインフラ勉強会のことについて書きました。

# 概要
> Ansibleのタスクを適度に待たせて適切なタイミングで実行したい

というタイトルはざっくりしてしまっていますが、各モジュールの例で具体的にどういうことをしたいか書いています。

* [特定の時間待つ/手動で再開を支持する](#特定の時間待つ手動で再開を指示する)
* [ポートの応答を待つ](#ポートの応答を待つ)
* [取得した値が特定の値になるのを待つ](#取得した値が特定の値になるのを待つ)

適度なタイミング、というのは色々ありますが、特に3つ目の[取得した値が特定の値になるのを待つ](#取得した値が特定の値になるのを待つ)が今回お伝えしたいことです。

最初はshell/commandモジュールで`sleep 60`とか書いていたのですが、
60秒というマジックナンバーの根拠は？？と言われるとないので答えに詰まりますし、
指定時間以上にかかったら事故るし逆に時間を伸ばしすぎて早く処理が終わってたら時間がもったいない、
とイケてないのでなんとか適切なタイミングで実行できるようにしたいと思いました。

## 特定の時間待つ/手動で再開を指示する
[pauseモジュール](https://docs.ansible.com/ansible/latest/modules/pause_module.html)を使います。名前の通り、`待つ`モジュールです。
じゃあ`sleep`コマンドと一緒じゃん、と思いましたが、
使ってみると意外といろいろできたので紹介します。
プレイブックはこんな感じにしました。

```yaml:play.yml
- hosts: all
  connection: local
  gather_facts: false
  tasks:
  - name: pause
    pause:
      seconds: 5
    register: result
  - name: show result
    debug:
      var: result
```
普通に実行してみると

```shell
$ ansible-playbook -i localhost, play.yml

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

TASK [pause] ********************************************************************************************************
Pausing for 5 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [localhost]

TASK [show result] **************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "delta": 5,
        "echo": true,
        "failed": false,
        "rc": 0,
        "start": "2018-12-09 20:32:19.363536",
        "stderr": "",
        "stdout": "Paused for 5.0 seconds",
        "stop": "2018-12-09 20:32:24.364480",
        "user_input": ""
    }
}

PLAY RECAP **********************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0
```

`result`にあるように5秒待って次のタスクが実行されているようです。
そして出力に`(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)`とあるように
`Ctrl+C`を押してみると、以下のように割り込みをかけられます。

```shell:'C'continueの場合
TASK [pause] ********************************************************************************************************
Pausing for 5 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
Press 'C' to continue the play or 'A' to abort
ok: [localhost]

TASK [show result] **************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "delta": 13,
        "echo": true,
        "failed": false,
        "rc": 0,
        "start": "2018-12-09 20:32:45.888910",
        "stderr": "",
        "stdout": "Paused for 13.95 seconds",
        "stop": "2018-12-09 20:32:59.837578",
        "user_input": ""
    }
}

PLAY RECAP **********************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0
```

`Ctrl+C`で割り込んだ場合は、本来何秒待つ設定だったかは関係なくなり、
すぐ`c`を押せば即次のタスクが実行されますし、逆に設定以上に待たせてから実行させることもできます。
`a`を押せば中断になります。

```shell:'A'abortの場合
TASK [pause] ********************************************************************************************************
Pausing for 5 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
Press 'C' to continue the play or 'A' to abort
fatal: [localhost]: FAILED! => {"msg": "user requested abort!"}

NO MORE HOSTS LEFT **************************************************************************************************

PLAY RECAP **********************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1
```

ちなみに冒頭のプレイブックでは待機する秒数を指定していましたが、時間指定は必須ではありません。
その場合は、

```shell
TASK [pause] ********************************************************************************************************
[pause]
Press enter to continue, Ctrl+C to interrupt:
```

となり、`Enter`で次のタスク実行されます（`Ctrl+C`の挙動は上と同じです）

ちなみに私自身はターミナルからではなく、JenkinsのPipelineで実行しているので、
プレイブックを分割して、Jenkinsの[input](https://jenkins.io/doc/pipeline/steps/pipeline-input-step/)で別作業の完了待ちをしています。

## ポートの応答を待つ
[wait_forモジュール](https://docs.ansible.com/ansible/latest/modules/wait_for_module.html)
個人的には待機系の方法として一番よく使われているんじゃないかと思ってる方法です。
主な使用用途としてはssh(22)やhttp(80)などのポートが応答あるまで待つ、というものです。
クラウドや仮想環境では仮想マシンの立ち上げタスクが完了したばかりではsshはまだ起動しておらず、
きちんと待機させないと後続のAnsibleタスクにコケることが多いのでよく使われているかと思います。

```yaml
- name: wake up ec2
  ec2:
    key_name: mykey
    instance_type: t2.micro
    image: ami-123456
    wait: yes
    group: webserver
    count: 1
    vpc_subnet_id: subnet-123456
    assign_public_ip: yes
    register: ec2

- name: Wait 300 seconds for port 22
  wait_for:
    port: 22
    host: '{{ ec2.instances.0.public_ip }}'
```

ちなみにec2モジュールのwaitはあくまでEC2のステート変化（この場合は`running`）を待つもののようです。

`wait_for`モジュールの使い方はもう一つあり、ファイルの監視が可能です。
ポート待ちの場合は`port`パラメータを入れましたが、ファイルの場合は`path`パラメータを設定します。
（両者は排他的なので同時に設定することはできません）
また、`search_regex`で正規表現を設定でき、ファイルならファイルの中身、ポートなら応答メッセージの中身のマッチングまでやってくれます。

ちなみに調べていたときに初めて知ったのですが、
[wait_for_connection](
https://docs.ansible.com/ansible/2.6/modules/wait_for_connection_module.html)モジュールというAnsibleのためのsshポート待ちに特化したモジュールもありました。
使ってみたことはないですが、Ansible2.3から追加されたモジュールのようなので知名度が低いのかもしれませんね。

## 取得した値が特定の値になるのを待つ

前置きが長くなりましたが、今回のメインはこちらです。
[Do-Until Loops](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#do-until-loops)というループ機構を使用します。

例では、AMIの取得が完了するまで待って次のタスク（EC2のTerminateなど）に進みたい、ということをやっています。
最初はAnsibleでの書き方がわからずシェルスクリプトでwhile文でも書こうと思ったのですが、
なんかうまい方法ないかなーと調べているうちに見つけました。
こちらもプレイブックを見てもらったほうがわかりやすいと思います。

```yaml
- name: create AMI
  ec2_ami:
    instance_id: i-xxxxxx
  register: ami

- name: wait for finish creating AMI
  ec2_ami_facts:
    image_ids: "{{ ami.ami_id }}"
  register: result
  until:  "{{ result.images.0.state }}" == "available"
  retries: 100
  delay: 10
```

2つ目のタスクの`until`の箇所にあるように、
条件式を書くことで結果が`True`になるまでタスクを繰り返してくれる、というものになります。
固有のモジュールではなく、`with_items`や`loop`のような繰り返しの制御機構なので、
様々なモジュールのタスクと組み合わせることができます。
特に今回のような`****_facts`モジュールや`url`モジュールなどでAPIをPOSTした後にGETで完了待ちするなど、使い所は結構ありそうです。
`retries`のデフォルト値が`1`なので、有限の回数を指定しなければいけないのがちょっと辛いですが、
`until`の条件をしっかり書いておけば大きすぎる数値を入れても基本的には問題ないと思います。

ちなみに`ec2_ami`モジュールにも`wait`があり、これを`yes`にしてあげれば要件を満たせそうですが、
以下のようなエラーが出てしまったので`until`のような待ち方を調べた、というのが経緯だったりします。
https://github.com/ansible/ansible/issues/40303
（ちなみにAmazonLinux1ではこのエラーが出たけど、WSLのUbuntu18.04ではエラーが出なかったりで何かしらのバージョン依存の問題かも？）

# まとめ

例を見て、カンのいい人は「これ`Packer`で良くね？」と思われるかと思います。
はい、最初はPackerで書いてたんですよ…でもね・・・あとはお察しください。

Ansibleのモジュールレベルの機能は調べてくるとポンポン出てくるの学習しやすいのですが、
Ansibleの動きを制御する方法の知識ってイマイチ増えてないなあ、
と今回の課題にぶち当たったときに感じました。
人間勉強している気になっても、なかなか問題が起きないと新しいものには目が行かないものですね。

# インフラ勉強会について

せっかくなのでポエムにならない程度にインフラ勉強会について書きたいと思います。

インフラ勉強会に参加したのは確か今年の1月半ばぐらいだったはず（記憶怪しい）なのでもう約1年経つのですが、
オフラインの勉強会にはそれ以前からちょくちょく参加していました。
しかし、登壇することはない聞き専でしたし、コミュ力もお察しなので懇親会でもあまり交流できていませんでした。
一応何らかのユーザグループではありますが、登壇者や他の参加者と **"壁"** みたいなものを感じていたんだと思います。

それに対してインフラ勉強会は、

* セッション中にリスナー同士で会話できる
* その流れで登壇者への質問もしやすい
* セッション以外の時間も様々なトピックや雑談で会話してる
* 開催ペースがDaily（オフラインだとハイペースでもMonthly）

という状況もあって、色んな人が初登壇に挑戦しやすい流れができていて、私もその流れに乗ることができました。
インフラ勉強会の初期の段階では入れたのも良かったと思いますが、
オフラインの勉強会で強く感じていた**"壁"**がない！というのがインフラ勉強会の最大の強みだと私は感じてます。
最近ちょっとリアルイソガシイであまり参加できてませんが、1周年イベントもありますし、
コミュニティに貢献！とまではいかないまでも、またちょくちょく顔を出したいと思っています。

以上です！
明日のカレンダーはanchor_cableさんです！