AWXの通知をカスタマイズする

この記事は[ニフティグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nifty)の3日目の記事です。
昨日は @johnFkennedy さんで「[teachablemachineを使って、なんかやってみた。](https://qiita.com/johnFkennedy/items/94c83a54b0e0a98b1f3c)」でした。コードなしで分類タスクを解けるのは魅力的ですね。

# はじめに
この記事ではAWX上で実行した結果をカスタマイズしてslackに通知することをゴールとしています。

多人数でAnsibleを使う場合にはGUI上でタスクの実行、結果の確認ができるAWXが便利です。
AWXを管理する側としては誰が、いつ、どんなタスクを実行して、結果はどうだったのかを通知してもらいたいですよね。
そこで、実行結果を投げる設定をAWXの`notification_template`で行い、slack上でいい感じの通知をしてもらいます。

※slackへの通知はwebhook経由で行います。通知設定は[こちらの記事](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)などを参考に各自行ってください。AWXとの連携には[こちらの記事](https://qiita.com/tonishy/items/ee62da3aec34218d24f6)が参考になると思います

以下の作業はすべて`通知`の管理者権限を持っているものとします。

## 環境
```
 _______________ 
<  AWX 8.0.0.0  >
 --------------- 
        \   ^__^
         \  (oo)\_______
            (__)      A )\/\
                ||----w |
                ||     ||

Ansible 2.8.6
```

# カスタムメッセージの追加方法
1. `通知`メニューからカスタムメッセージを追加したい通知テンプレートを選択します。
2. 下の方にある`CUSTOMIZE MESSAGES...`を`ON`にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/0d3b8205-9f8f-d77b-5a72-edbdb67771b4.png)

以上でカスタムメッセージの追加は完了です

しかし、このままだとテンプレートを実行してもジョブが成功しても失敗しても以下のような無機質なメッセージしか流れません。  

```
Job #3 'Demo Job Template' running: https://towerhost/#/jobs/playbook/3
```
これを温かみのあるものへと変更していきます。

# メッセージのカスタマイズ
## カスタムメッセージで使用できる変数
通知にはいつ、誰が、どのプレイブックを実行したか、などの情報が必要です。これらの情報はジョブ実行時にAnsible変数に格納され、流れていきます。
格納される変数のリストは[Ansible Tower Documentation](https://docs.ansible.com/ansible-tower/3.6.0/html/installandreference/notification_parameters_supported.html#ir-notifications-reference)に記載されています。[^1]
この変数を参照することで、各種ジョブの状態を確認することができます。

[^1]: 11/13のVersion 3.6.0 updateに伴って、Tower Documentationにvariablesについての記述が追加されました。

カスタムメッセージで変数を参照する場合には基本、`{{ job.<使いたい変数> }}`の形で記述していきます。
例えば、テンプレートの起動者をメッセージに追加する場合には`{{ job.summary_fields.created_by.username }}`と記述します。

具体的にどんな値が格納されているのか知りたいときには、一度`{{ job }}`のみを記述して、通知を流してみるのが良いです。
sample job templateを実行すると以下のようなjsonが返ってきます。

<details><summary>（折りたたみ）`{{ job }}`で流れてくる内容</summary><div>

```json
{
    "id": 17, 
    "type": "job", 
    "url": "/api/v2/jobs/17/", 
    "created": datetime.datetime(2019, 11, 5, 4, 46, 2, 551213, tzinfo=), 
    "modified": datetime.datetime(2019, 11, 5, 4, 46, 16, 849290, tzinfo=), 
    "name": "Demo Job Template", 
    "description": "",
    "job_type": "run",
    "playbook": "hello_world.yml",
    "forks": 0, 
    "limit": "",
    "verbosity": 0, 
    "job_tags": "",
    "force_handlers": False, 
    "skip_tags": "",
    "start_at_task": "",
    "timeout": 0, 
    "use_fact_cache": False, 
    "launch_type": "manual",
    "status": "running",
    "failed": False, 
    "started": "2019-11-05T04:46:16.912606Z",
    "finished": None, 
    "elapsed": 0.148594, 
    "job_explanation": "",
    "execution_node": "awx",
    "controller_node": "",
    "allow_simultaneous": False, 
    "scm_revision": "",
    "diff_mode": False, 
    "job_slice_number": 0, 
    "job_slice_count": 1, 
    "summary_fields": {
        "inventory": {
            "id": 1, "name": "Demo Inventory",
            "description": "",
            "has_active_failures": False,
            "total_hosts": 1, 
            "hosts_with_active_failures": 0, 
            "total_groups": 0, 
            "groups_with_active_failures": 0, 
            "has_inventory_sources": False, 
            "total_inventory_sources": 0, 
            "inventory_sources_with_failures": 0, 
            "organization_id": 1, 
            "kind": ""
        }, 
        "project": {
            "id": 6, 
            "name": "Demo Project",
            "description": "",
            "status": "successful",
            "scm_type": "git"
        }, 
        "job_template": {
            "id": 7, 
            "name": "Demo Job Template",
            "description": ""
        }, 
        "unified_job_template": {
            "id": 7, 
            "name": "Demo Job Template",
            "description": "",
            "unified_job_type": "job"
        }, 
        "instance_group": {
            "name": "tower",
            "id": 1
        }, 
        "created_by": {
            "id": 1, 
            "username": "admin",
            "first_name": "",
            "last_name": ""
        }, 
        "labels": {
            "count": 0, 
            "results": []
        }
    }
}
```
</div></details>

## jinja2 テンプレート
記述方法から気づく方もいらっしゃるかと思いますが、カスタムメッセージの変数はjinja2 テンプレートで記述します。
これにより、変数から得られる文字を自由に書き換えることが可能です。

例えば、`{{ job.started }}`はUTCだったり秒以下小数点が並んでいたりと見づらいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/17f19ebb-124a-74a7-6923-274d36e6e434.png)

そこで、次のように書くことで、日本標準時に直すことができます。

```
"{{ job.started.split('T')[0] }} {{ job.started.split('T')[-1].split(':')[0] | int + 9 }}:{{ job.started.split('.')[0].split(':')[1:] | join(':') }}"
```

※`{{ job.finished }}`はなぜかはじめに`string`への変換が必要でした。

```
"{% set f=job.finished ~ '' %}  {{ f.split('T')[0] }}  {{ f.split('T')[-1].split(':')[0] | int + 9 }}:{{ f.split('.')[0].split(':')[1:] | join(':') }}"
```

変更後はこうなりました。スッキリしましたね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/6f6c5916-d30a-311f-ae7f-5fe1586b62b4.png)

## slack投稿のカスタマイズ
webhook経由でのslack通知の場合には、ペイロードをjsonで記述することができます。つまり、投稿するメッセージのフォーマットをある程度いじることができます。
slackへ投げるjsonの形式は[slack Formatting messages](https://api.slack.com/docs/message-formatting)を参考にしてください。
[slack Message Builder](https://api.slack.com/docs/messages/builder)で記述した内容のチェックができて便利です。

最終的に完成した通知がこちらです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395053/5f7d5216-7d0b-5a0e-75c1-046339ef2a29.png)

以下通知に設定した内容になります。

<details><summary>START MESSAGE BODY</summary><div>

```
{
  "channel": "#test_notification", 
  "username": "AnsibleNotice",
  "text": "{{ job.summary_fields.created_by.username }} が {{ job.name }} を開始しました", 
  "icon_emoji": ":passport_control:"
}
```
</div></details>

<details><summary>SUCCESS MESSAGE BODY</summary><div>

```
{
  "channel": "#test_notification", 
  "username": "AnsibleNotice",
  "attachments": [
      {
          "fallback": "{{ job.name }} is {{ job.status }}",
          "color": "good",
          "author_name": "{{ job.summary_fields.created_by.username }}",
          "title": "{{ job.name }}",
          "title_link": "{{ url }}",
          "pretext": "{{ job.summary_fields.created_by.username }} が {{ job.name }} を {{ job.status }} で終了しました",
          "fields": [
              {
                "title": "started",
                "value": "{{ job.started.split('T')[0] }} {{ job.started.split('T')[-1].split(':')[0] | int + 9 }}:{{ job.started.split('.')[0].split(':')[1:] | join(':') }}",
                "short": "true"
              },
              {
                "title": "finished",
                "value": "{% set f=job.finished ~ '' %}  {{ f.split('T')[0] }}  {{ f.split('T')[-1].split(':')[0] | int + 9 }}:{{ f.split('.')[0].split(':')[1:] | join(':') }}",
                "short": "true"
              },
              {
                "title": "project",
                "value": "{{ job.summary_fields.project.name }}",
                "short": "true"
              },
              {
                "title": "inventory",
                "value": "{{ job.summary_fields.inventory.name }}",
                "short": "true"
              },
              {
                "title": "job_template",
                "value": "{{ job.summary_fields.job_template.name }}",
                "short": "true"
              },
              {
                "title": "instance_group",
                "value": "{{ job.summary_fields.instance_group.name }}",
                "short": "true"
              },
              {
                "title": "playbook",
                "value": "{{ job.playbook }}",
                "short": "true"
              }
          ],
          "footer": "AWX Notification"
      }
  ],
  "icon_emoji": ":passport_control:"
}
```
</div></details>

<details><summary>ERROR MESSAGE BODY</summary><div>

```
{
  "channel": "#test_notification", 
  "username": "AnsibleNotice",
  "attachments": [
      {
          "fallback": "{{ job.name }} is {{ job.status }}",
          "color": "danger",
          "author_name": "{{ job.summary_fields.created_by.username }}",
          "title": "{{ job.name }}",
          "title_link": "{{ url }}",
          "pretext": "{{ job.summary_fields.created_by.username }} が {{ job.name }} を {{ job.status }} で終了しました",
          "fields": [
              {
                "title": "started",
                "value": "{{ job.started.split('T')[0] }} {{ job.started.split('T')[-1].split(':')[0] | int + 9 }}:{{ job.started.split('.')[0].split(':')[1:] | join(':') }}",
                "short": "true"
              },
              {
                "title": "finished",
                "value": "{% set f=job.finished ~ '' %}  {{ f.split('T')[0] }}  {{ f.split('T')[-1].split(':')[0] | int + 9 }}:{{ f.split('.')[0].split(':')[1:] | join(':') }}",
                "short": "true"
              },
              {
                "title": "project",
                "value": "{{ job.summary_fields.project.name }}",
                "short": "true"
              },
              {
                "title": "inventory",
                "value": "{{ job.summary_fields.inventory.name }}",
                "short": "true"
              },
              {
                "title": "job_template",
                "value": "{{ job.summary_fields.job_template.name }}",
                "short": "true"
              },
              {
                "title": "instance_group",
                "value": "{{ job.summary_fields.instance_group.name }}",
                "short": "true"
              },
              {
                "title": "playbook",
                "value": "{{ job.playbook }}",
                "short": "true"
              }
          ],
          "footer": "AWX Notification"
      }
  ],
  "icon_emoji": ":passport_control:"
}
```
</div></details>

# あとがき
AWXから送信される通知を無機質なものから、温かみのあるものへと変更しました。通知を見るたびに柔らかなbotらしさを感じることができます。
皆様もよいAWXライフを送ってください。