AWXまたはAnsible Tower + logstashでログアグリゲータを設定してみた＆ハマりポイント

# 概要

AWXやAnsible Tower（以下ではTowerとする）を使う上で監査目的に実行履歴の長期保管は非常に大事になります。
特にISO認証などを取得している（しようとしている）場合は、必須事項にもなります。
なので、ログを不足な事態に備えてリアルタイムに外部ストレージ（S3、ファイルサーバ、Elasticsearchなど）に退避できるように[ログアグリゲータ](https://docs.ansible.com/ansible-tower/latest/html_ja/administration/logging.html)という機能があります。
なお、一回こっきりのログバックアップ方法は「[AWX(Ansible Tower)の実行ログをバックアップする](https://qiita.com/comefigo/items/f184191434fb8494f529)」をご参照ください。

※AWXは15.0.1時点の仕様になります。

# 構成

ログアグリゲータはいくつかのサービスやツールにAWXログを転送することができますが、（汎用性を考慮して）今回はElastic社のログコレクターである[Logstash](https://www.elastic.co/jp/logstash)での設定を紹介したいと思います。なお、転送先はS3になりますが、そこでのハマりポイントも紹介したいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30522/3363df1a-bbb8-1850-1864-0be73c050d28.png)

## 処理の流れ
1. AWXからログアグリゲータ（ログ収集ツールであるLogstash）へhttpでログ転送します。
2. Logstashのinput http pluginで転送されたログを取得します。
3. Logstash内で必要に応じてfilter pluginを用いて加工し、output s3 pluginでS3に転送します。

# 出力されるログ

各種イベントで出力されるログのサンプルになります。

```json:activity_stream.json
{
         "stack_info" => nil,
            "object1" => "job",
               "host" => "172.1.2.3",
       "relationship" => "",
    "cluster_host_id" => "awx",
         "tower_uuid" => nil,
            "changes" => {
                 "diff_mode" => true,
          "job_slice_number" => 0,
                "scm_branch" => "",
        "webhook_credential" => nil,
                  "playbook" => "playbooks/test.yml",
             "start_at_task" => "",
                   "project" => "my-project",
                 "verbosity" => 0,
              "job_template" => "test-24",
           "webhook_service" => "",
                  "job_type" => "run",
                 "inventory" => "localhost",
                   "timeout" => 0,
           "job_slice_count" => 1,
              "webhook_guid" => "",
                        "id" => 19,
                     "limit" => "",
        "allow_simultaneous" => false,
            "use_fact_cache" => false,
                     "forks" => 0,
                "extra_vars" => "{}",
                      "name" => "test",
            "instance_group" => nil,
               "credentials" => [
            [0] "CWF-Vault (3)"
        ],
                    "labels" => [],
               "description" => "test",
                  "job_tags" => "",
            "force_handlers" => false,
                 "skip_tags" => ""
    },
         "@timestamp" => 2020-11-30T13:55:10.763Z,
            "message" => "Activity Stream update entry for job",
            "headers" => {
        "http_user_agent" => nil,
           "request_path" => "/",
         "content_length" => "1447",
           "content_type" => "application/json; charset=utf-8",
         "request_method" => "POST",
              "http_host" => "awx_logstash:15014",
           "http_version" => "HTTP/1.1",
            "http_accept" => "*/*"
    },
        "logger_name" => "awx.analytics.activity_stream",
          "operation" => "create",
              "level" => "INFO",
     "summary_fields" => {
               "actor" => {
            "first_name" => "xxx",
             "last_name" => "xxx",
              "username" => "xxx",
                    "id" => 8
        },
                 "job" => [
            [0] {
                     "failed" => false,
                       "name" => "test",
                         "id" => 19,
                     "status" => "new",
                "description" => "test",
                    "elapsed" => "0.000"
            }
        ],
        "job_template" => [
            [0] {
                "description" => "test",
                       "name" => "test",
                         "id" => 24
            }
        ]
    },
              "actor" => "xxx",
            "object2" => "",
           "@version" => "1"
}
```

```json:job_events.json
{
               "host" => "172.1.2.3",
             "stdout" => "\r\nPLAY [No log] ******************************************************************",
           "modified" => nil,
            "changed" => false,
         "@timestamp" => 2020-11-30T13:55:16.463Z,
            "headers" => {
        "http_user_agent" => nil,
           "request_path" => "/",
         "content_length" => "1037",
           "content_type" => "application/json; charset=utf-8",
         "request_method" => "POST",
              "http_host" => "awx_logstash:15014",
           "http_version" => "HTTP/1.1",
            "http_accept" => "*/*"
    },
               "role" => "",
               "play" => "No log",
             "failed" => false,
           "playbook" => "playbooks/test.yml",
          "host_name" => "",
              "level" => "INFO",
           "end_line" => 3,
                "job" => 19,
               "uuid" => "0242ac12-0005-0719-d022-000000000006",
          "verbosity" => 0,
              "event" => "playbook_on_play_start",
            "counter" => 3,
            "created" => "2020-11-30T13:55:16.458Z",
           "@version" => "1",
    "cluster_host_id" => "awx",
         "start_line" => 1,
                 "id" => nil,
         "tower_uuid" => nil,
            "message" => "Event data saved.",
         "event_data" => {
                 "play" => "No log",
             "playbook" => "playbooks/test.yml",
                 "name" => "No log",
              "pattern" => "localhost",
            "play_uuid" => "0242ac12-0005-0719-d022-000000000006",
        "playbook_uuid" => "31368fbc-7c13-4b5a-8f4e-7ad336cc32f4",
                 "uuid" => "0242ac12-0005-0719-d022-000000000006",
         "play_pattern" => "localhost"
    },
        "logger_name" => "awx.analytics.job_events",
        "parent_uuid" => "31368fbc-7c13-4b5a-8f4e-7ad336cc32f4",
               "task" => "",
      "event_display" => "Play Started (No log)"
}
```

# 設定

1. [ログアグリゲータ](https://docs.ansible.com/ansible-tower/latest/html_ja/administration/logging.html#set-up-logging-with-tower)を設定します。  
  - ログアグリゲータ・・・logstashへのURLもしくはIPになります。**httpは必須**
  - ログアグリゲータポート・・・利用されていない適当なポートでOK
  - ログアグリゲータのタイプ・・・logstash
  - ログアグリゲータのプロトコル・・・HTTPS/HTTP
  - ログアグリゲーターフォームにデータを送信するロガー・・・転送する[ログの種別](https://docs.ansible.com/ansible-tower/latest/html_ja/administration/logging.html#loggers)
  - 外部ログの有効化・・・上記の項目後を設定後に一度保存すると選択可能になります。  

    ※設定（ログアグリゲータのURLなど）が間違えていても「テスト」では成功になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30522/f573a30f-6133-5163-8ccb-b67366300cfd.png)
2. Logstashの設定  
    コンテナ版AWXをご利用の場合は、AWXのdocker-compose.ymlに以下を加えることで、AWX＋Logstashが1セットで導入することができます。なお、オンプレ版の場合は[Logstash公式ドキュメント](https://www.elastic.co/guide/en/logstash/current/installing-logstash.html)に従って導入してください。

    ```yaml:docker-compose.yml
    version: '3.7'
    services:

    ～中略～

      logstash:
        image: docker.elastic.co/logstash/logstash:7.9.2
        # コンテナ名もしくはサービス名をログアグリゲータのURLとして指定することができます。
        # IPではコンテナの再作成時に代わる可能性がありますので、ご注意！
        container_name: awx_logstash
        volumes:
          - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml
          - ./logstash/config/pipelines.yml:/usr/share/logstash/config/pipelines.yml
          - ./logstash/sincedb:/usr/share/logstash/sincedb
          - ./logstash/pipeline:/usr/share/logstash/pipeline
          - ./logstash/logs:/usr/share/logstash/logs
        environment:
          TZ: Asia/Tokyo
          # S3のプレフィックスにAWXのホスト名を環境変数として渡す。
          AWX_HOST_NAME: my_awx
        restart: unless-stopped
    ```

    ```yaml:/usr/share/logstash/config/logstash.yml
    # Logstash Configs
    # https://www.elastic.co/guide/en/logstash/current/logstash-settings-file.html
    http.host: "127.0.0.1"
    # デバック時にinfoにすると便利です。
    log.level: "warn"
    ```

    ```yaml:/usr/share/logstash/config/pipelines.yml
    - pipeline.id: awx_job
      path.config: "/usr/share/logstash/pipeline/awx_job.conf"
    ```

    余談ですが、終了行がコメント行の場合はエラーになります。

    ```config:/usr/share/logstash/pipeline/awx_job.conf
    # input httpモジュールを用います
    # データの取得設定
    input {
      http {
        id => "awx_job"
        # ログアグリゲータポートで設定した値と同じ
        port => 15014
      }
    }

    # データの加工設定
    filter {
      mutate {
        remove_field => [ "headers", "message" ]
      }
      # すべてのキーをmessageに再配置
      ruby {
        code => '
          event.to_hash.each{|k, v|
            unless [ "@timestamp", "@version"].include?(k)
              event.set("[message][#{k}]", v)
              event.remove(k)
            end
          }
        '
      }
    }

    # データの転送設定
    output {
      # https://www.elastic.co/guide/en/logstash/current/plugins-outputs-s3.html
      s3{
        region => "xxx"
        bucket => "xxx"
        # docker-composeで指定した環境変数がプレフィックスとして使用されている
        prefix => "awxlog/${AWX_HOST_NAME}/%{+YYYYMM}"
        # 以下の設定は好みに合わせて設定してください。
        #
        # S3のストレージクラスに応じては最小サイズの指定があるため、
        # それらの条件を満たす200KBを指定する
        # https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html#sc-compare
        size_file => 200000
        # 5分
        time_file => 5
        rotation_strategy => "size_and_time"
        storage_class => "STANDARD"
        canned_acl => "private"
        encoding => "none"
        codec => "json"
      }
    }
    ```

    S3に転送される[ファイル名(uuid名)](https://www.elastic.co/guide/en/logstash/current/plugins-outputs-s3.html#_s3_output_file)は残念ながら指定はできない。
    転送されたログは以下のようになっています。
    messageの配下にすべてのキーと値を再配置しています。

    ```json:ls.s3.xxxxxxxxxxxx.part48.txt
{
    "message": {
        "changed": false,
        "cluster_host_id": "awx",
        "event_data": {
            "res": {
                "changed": false,
                "stat": {
                    "exists": false
                },
                "invocation": {
                    "module_args": {
                        "path": "my_playbook.yml",
                        "get_mime": true,
                        "follow": false,
                        "checksum_algorithm": "sha1",
                        "get_md5": false,
                        "get_attributes": true,
                        "get_checksum": true
                    }
                },
                "_ansible_no_log": false,
                "_ansible_delegated_vars": {}
            },
            "event_loop": null,
            "task_args": "",
            "duration": 1.209951,
            "start": "2020-11-25T08:44:24.746640",
            "remote_addr": "10.0.12.159",
            "end": "2020-11-25T08:44:25.956591",
            "task_action": "stat",
            "task": "Check variable file is exist",
            "host": "10.0.12.159",
            "task_path": "/tmp/awx_7_f86yn0p0/project/tasks/hoge.yml:11",
            "play_pattern": "my_host",
            "play":"Hoge play book","playbook":"playbooks/my_playbook.yml"},"verbosity":0,"logger_name":"awx.analytics.job_events","play":"Hoge play book","start_line":6153,"playbook":"my_playbook.yml","level":"INFO","end_line":6154,"modified":null,"id":null,"job":7,"host_name":"172.10.1.191","event_display":"Host OK","counter":3693,"role":"","host":"172.1.1.2","task":"Check variable file is exist","stdout":"\u001B[
                0;32mok: [
                    172.10.1.191
                ]\u001B[
                    0m","tower_uuid":null,"failed":false,"event":"runner_on_ok","created":"2020-11-25T08: 44: 25.956Z"},"@timestamp":"2020-11-25T08: 44: 25.962Z","@version":"1"}
    ```

# ハマりポイント

## AWXから転送されるログはmessageキーの配下にはない！

公式ドキュメントでは以下のように設定値が紹介されていますが、これをみるとmessageキー配下にすべての値があると勘違いガチですが、前述の出力ログのサンプルを参照して頂くとお分かりかと思いますが、**messageキーの配下にログはありません!**
※注釈にはその旨についてさらっと書いてあるが・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30522/17b92c9a-dccd-d4cd-459b-7c9c37784775.png)
以下のようにデバック用に設定することで、Logstashの実行ログに転送時のデータが表示されるようになります。

```config:/usr/share/logstash/pipeline/awx_job.conf
output {
  stdout {}
}
```

## S3に転送する際はmessageキーにすべてのデータを格納しなければならない！

ここが一番ハマりました。
上記のawx_job.confのfilterの設定にも記載していますが、すべてのログをmessageキーに格納している理由はこれです。
私の読解力が間違えていなければ、公式ドキュメントにmessageに入れろと説明がないので気づくまでに無駄に時間がかかりました。
なので、すべてのキーをmessageキーに再配置しています。