複数AWSアカウントのCloudTrailをまとめてKibana(Elasticsearch Service)で可視化する


# 注意点

ここに書かれている設計では、各AWSアカウントのLambdaがログ集約先のElasticsearch Serviceに直接書き込んでいる。
ログの出力量は調整できないのでElasticsearchの書き込み負荷を制御できない。
Elasticsearch Serviceの前段にFirehoseを挟むなどすれば負荷を監視・制御できる(試してない)。
クッションを入れることでElasticsearch Serviceをスケールさせる余裕が生まれるはず。
また、Firehoseを挟むことでLambdaでの変換処理や変換済みデータのS3へのバックアップなど柔軟に対応できる(試してない)。

（参考）
[Amazon Kinesis Data Firehose で Amazon Elasticsearch Service 7.x クラスターへのデータストリーム配信をサポート](https://aws.amazon.com/jp/about-aws/whats-new/2019/10/amazon-kinesis-data-firehose-adds-support-data-stream-delivery-amazon-elasticsearch-service/)

# 概要

いくつかのAWSアカウントでCloudTrailの設定を行い、そのログをまとめ用のAWSアカウントに送る。
送られたログは、S3とElasticsearch Serviceに蓄える。
Elasticsearch ServiceのKibanaを利用して可視化する。
まとめ用のAWSアカウントを「管理アカウント」と呼ぶことにする。

# 手順

1. 管理アカウントでCloudTrailの設定を行い、S3バケット作成とCloudWatch Logsへの出力を行う(最初のログ)
2. 管理アカウントでElasticsearch Serviceのドメインを作成する
3. ドメイン作成後、CloudWatch Logs → (Lambda) → Elasticsearch Serviceの出力設定を行う
4. その他のアカウントでCloudTrailの設定を行い、管理アカウントのS3バケットと自アカウントのCloudWatch Logsへの出力を行う(2つ目以降のログ)
5. CloudWatch Logs → (Lambda) → 管理アカウントのElasticsearch Serviceの出力設定を行う
6. 以降、4→5の手順を繰り返す

# 管理アカウント

CloudTrailのログを収集しKibanaで可視化するAWSアカウント。
1つ目のログ収集対象アカウントとしてCloudTrailの設定も行う。

## CloudTrailの初期設定

Trailの新規作成。
出力先としてS3バケットとCloudWatch Logsを選択する。

### S3への出力設定

#### バケット

アーカイブ用のバケットを新規作成する。

#### バケットポリシー

CloudTrailサービスに対してバケットへの書き込みを許可する。
ポリシーは自動的に作成される。

```json:bucket-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSCloudTrailAclCheck20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::BUCKET_NAME"
        },
        {
            "Sid": "AWSCloudTrailWrite20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::BUCKET_NAME/AWSLogs/0000000000000/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        }
    ]
}
```

#### ライフサイクル

バケット作成後、ライフサイクルの設定を行った。
見る機会はあまりないので、最短でStandard-IAに変更するようにした。
とはいえ、最低3ヶ月は検索可能にして、最低1年は残しておいたほうがいいと思う。

* 30日でStandard-IA
* 1年でGlacier
* 3年で削除

### CloudWatch Logsへの出力設定

ログをElasticsearch Serviceに流すにあたってCloudWatch Logs Subscriptionを利用するので、CloudTrailのCloudWatch Logs出力を有効にする。

#### IAMロール

CloudTrailサービスに対してCloudWatch Logsへの書き込みを許可する。
`CloudTrail_CloudWatchLogs_Role`ロールとポリシーが勝手に作成される。
既存ロールがある場合ポリシーだけ新規作成したり、そこは環境に応じて適当に。

```json:iam-policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AWSCloudTrailCreateLogStream20141101",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream"
      ],
      "Resource": [
        "arn:aws:logs:ap-northeast-1:000000000000:log-group:CloudTrail/DefaultLogGroup:log-stream: 000000000000_CloudTrail_ap-northeast-1*"
      ]
    },
    {
      "Sid": "AWSCloudTrailPutLogEvents20141101",
      "Effect": "Allow",
      "Action": [
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:ap-northeast-1: 000000000000:log-group:CloudTrail/DefaultLogGroup:log-stream: 000000000000_CloudTrail_ap-northeast-1*"
      ]
    }
  ]
}
```

#### ロググループ

デフォルトの`CloudTrail/DefaultLogGroup`にした。

## Elasticsearch Serviceのドメイン作成

ドメインを新規作成する。インスタンスタイプやノード数は規模に合わせて適当に。
ここではドメイン名を`cloudtrail`とした。
あとでARNとエンドポイントURLが必要。

### アクセスポリシー

あとで他のアカウントからの書き込み許可も追記するが、とりあえず社内からのアクセスのみ許可するようIP制限をかけた。

```json:access-policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "arn:aws:es:ap-northeast-1:0000000000:domain/cloudtrail/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "000.000.000.000/32"
        }
      }
    }
  ]
}
```

### インデックステンプレートの作成

デフォルトの設定だと時々 `illegal_argument_exception` が出るのでインデックス作成時のテンプレートを変更する。
`index.mapping.total_fields.limit` を3000くらいにするといいらしい。

```
Limit of total fields [1000] in index [cwl-yyyy.mm.dd] has been exceeded
```

ついでに `apiVersion` の型も `text` に固定する。`date` と `text` で混在するとのこと。

参考：[【Amazon Elasticsearch Service】CloudTrail、VPC Flowlogsを集約する](http://blog.serverworks.co.jp/tech/2019/06/10/cloudtrail-and-flowlogs-to-aes/)

設定方法の詳細は公式ドキュメント([IndexTemplates](https://www.elastic.co/guide/en/elasticsearch/reference/5.1/indices-templates.html))を参照。
バージョンによって形式に違いがあるので注意。下記の例は5.1の場合。
パスの最後にあるテンプレート名の `cloudtrail` の部分は何でもよさそう。

```sh
$ string trim '
       {
         "template": "cwl-*",
         "settings": {
           "index.mapping.total_fields.limit": 3000
         },
         "mappings": {
           "_default_": {
             "properties": {
               "apiVersion": {
                 "type": "text"
               }
             }
           }
         }
       }' | curl -X PUT -H 'Content-Type: application/json' -d @- 'https://xxx.ap-northeast-1.es.amazonaws.com/_template/cloudtrail'
```

## CloudWatch Logs → Elasticsearch Service

### 保持期間

CloudTrailのログ保持期間を無期限 → 1週間に変更した。
Elasticsearch Serviceに転送するので、ここでの保持期間は短めにした。

### CloudWatch Logs Subscription

`CloudTrail/DefaultLogGroup`のActionsメニューから`Stream to Amazon Elasticsearch Service`を選択すると勝手にLambdaが作成される。

![logs_to_es.png](https://qiita-image-store.s3.amazonaws.com/0/379/9e3bac23-a4b3-78cc-8d59-9738cb3e930a.png "logs_to_es.png")

ここでは同一アカウントのドメインを指定する。

![choose_es.png](https://qiita-image-store.s3.amazonaws.com/0/379/76b9bb32-8a68-2fbf-70fd-bfe8d3bec174.png "choose_es.png")

あとはLambdaの実行ロールを`lambda_elasticsearch_execution`にして、ログ形式からCloudTrailを選択して完了。
`LogsToElasticsearch_cloudtrail`というLambdaファンクションができているはず。

## Elasticsearch Serviceのインデックス掃除

このままだと爆発するのであとで考える。
`cwl-YYYY.MM.DD`のように日単位でインデックスが作成されるので削除は簡単にできると思う。
`.kibana`インデックスが大きくなることはなさそう。

# その他のアカウント

管理アカウントにCloudTrailのログを送るアカウント。
CloudTrail、CloudWatch Logs、Lambdaの設定が必要。

## CloudTrailの初期設定

Trailの新規作成。
出力先として管理アカウントのS3バケットと自アカウントのCloudWatch Logsを選択する。
ファイルの暗号化はしない、バリデーションはする、に設定した。どちらもデフォルトの設定。

### S3への出力設定

#### バケット

管理アカウントのバケットを指定する。
あらかじめバケットポリシーを設定しておかないと、Trail作成時にエラーになる。

#### バケットポリシー

管理アカウントのバケットポリシーを編集する。
`Resource`に`arn:aws:s3:::BUCKET_NAME/AWSLogs/1111111111111/*`を追記する。
`1111111111111`は追加対象のAWSアカウントID。

```json:bucket-policy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSCloudTrailAclCheck20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::BUCKET_NAME"
        },
        {
            "Sid": "AWSCloudTrailWrite20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/AWSLogs/0000000000000/*",
                "arn:aws:s3:::BUCKET_NAME/AWSLogs/1111111111111/*"
            ],
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        }
    ]
}
```

### CloudWatch Logsへの出力設定

管理アカウントと同様に出力設定を行う。

## CloudWatch Logs → Elasticsearch Service

### 保持期間

管理アカウントと同様に1週間にした。

### CloudWatch Logs Subscription

ドメインとして管理アカウントのElasticsearch Serviceを指定する。
ARNとエンドポイントURLが必要。

![logs_to_other_es.png](https://qiita-image-store.s3.amazonaws.com/0/379/463ba84f-fe8d-2e52-1cf2-656884edb9ad.png "logs_to_other_es.png")


設定完了の時点で`lambda_elasticsearch_execution`ロールからElasticsearch Serviceドメインへのアクセスを許可してないのでエラーになるはず。
なので、`lambda_elasticsearch_execution`ロールが作られた時点で設定完了前に後述のアクセス許可設定を行った。

設定完了後`LogsToElasticsearchEx_cloudtrail_000000000000`というLambdaファンクションができているはず。

## Elasticsearch Serviceへのアクセス許可

### アクセスポリシー

管理アカウントのElasticsearch Serviceでアクセスポリシーを変更し、
自分のLambda実行ロール(`lambda_elasticsearch_execution`)からの`es:ESHttpPost`アクションを許可する。
アカウントを追加したら`Principal`を配列にすればいいと思う。

```json:access-policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Resource": "arn:aws:es:ap-northeast-1:0000000000:domain/cloudtrail/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "000.000.000.000/32"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::1111111111:role/lambda_elasticsearch_execution"
      },
      "Action": "es:ESHttpPost",
      "Resource": "arn:aws:es:ap-northeast-1:0000000000:domain/cloudtrail/*"
    }
  ]
}
```

## Kibanaの設定

ここまでの設定でKibanaの設定をしてみる。
Kibanaは最初から利用可能になっていて、コンソールにURLが表示されている。

このJSONをインポートすれば誰がException出してるかなどを追えると思う。

ダッシュボードの設定をして、グラフからドラッグで期間を絞ったり、AWSアカウントIDやエラーコードを絞ったあと、その条件をピン留めすることで、クエリー画面に検索条件を持ち越しできる。
クエリー画面では実際のログを詳細に見ることができる。

![kibana.png](https://qiita-image-store.s3.amazonaws.com/0/379/ed6eae09-4b7d-91bb-017a-baf2dacafe46.png "kibana.png")

<details>
<summary>可視化ダッシュボード用設定のJSON</summary>
<div>

```json
[
  {
    "_id": "CloudTrail",
    "_type": "dashboard",
    "_source": {
      "title": "CloudTrail",
      "hits": 0,
      "description": "",
      "panelsJSON": "[{\"col\":7,\"id\":\"eventName_AreaChart\",\"panelIndex\":1,\"row\":1,\"size_x\":6,\"size_y\":4,\"type\":\"visualization\"},{\"col\":4,\"id\":\"Region_PieChart\",\"panelIndex\":2,\"row\":1,\"size_x\":3,\"size_y\":1,\"type\":\"visualization\"},{\"col\":7,\"id\":\"errorCode_AreaChart\",\"panelIndex\":4,\"row\":5,\"size_x\":6,\"size_y\":4,\"type\":\"visualization\"},{\"col\":1,\"id\":\"userName_TagCloud\",\"panelIndex\":5,\"row\":2,\"size_x\":6,\"size_y\":2,\"type\":\"visualization\"},{\"col\":1,\"id\":\"sessionIssuer.userName_TagCloud\",\"panelIndex\":6,\"row\":4,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"col\":1,\"id\":\"InvokedBy_TagCloud\",\"panelIndex\":7,\"row\":7,\"size_x\":6,\"size_y\":3,\"type\":\"visualization\"},{\"col\":1,\"id\":\"Owner_PieChart\",\"panelIndex\":8,\"row\":1,\"size_x\":3,\"size_y\":1,\"type\":\"visualization\"}]",
      "optionsJSON": "{\"darkTheme\":true}",
      "uiStateJSON": "{\"P-1\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}},\"P-2\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}},\"P-3\":{\"vis\":{\"legendOpen\":false}},\"P-4\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}}}",
      "version": 1,
      "timeRestore": false,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"filter\":[{\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\"}}}]}"
      }
    }
  },
  {
    "_id": "eventName_AreaChart",
    "_type": "visualization",
    "_source": {
      "title": "eventName_AreaChart",
      "visState": "{\"title\":\"eventName_AreaChart\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"3\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"eventTime\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"eventName.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "Region_PieChart",
    "_type": "visualization",
    "_source": {
      "title": "Region_PieChart",
      "visState": "{\"title\":\"Region_PieChart\",\"type\":\"pie\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"awsRegion.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "errorCode_AreaChart",
    "_type": "visualization",
    "_source": {
      "title": "errorCode_AreaChart",
      "visState": "{\"title\":\"errorCode_AreaChart\",\"type\":\"area\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"smoothLines\":false,\"scale\":\"linear\",\"interpolate\":\"linear\",\"mode\":\"stacked\",\"times\":[],\"addTimeMarker\":false,\"defaultYExtents\":false,\"setYExtents\":false,\"yAxis\":{}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"eventTime\",\"interval\":\"auto\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"errorCode.keyword\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "InvokedBy_TagCloud",
    "_type": "visualization",
    "_source": {
      "title": "InvokedBy_TagCloud",
      "visState": "{\"title\":\"InvokedBy_TagCloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":42},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"userIdentity.invokedBy.keyword\",\"size\":20,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "userName_TagCloud",
    "_type": "visualization",
    "_source": {
      "title": "userName_TagCloud",
      "visState": "{\"title\":\"userName_TagCloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":18,\"maxFontSize\":38},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"userIdentity.userName.keyword\",\"exclude\":{\"pattern\":\"homes-dev\"},\"size\":15,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "sessionIssuer.userName_TagCloud",
    "_type": "visualization",
    "_source": {
      "title": "sessionIssuer.userName_TagCloud",
      "visState": "{\"title\":\"sessionIssuer.userName_TagCloud\",\"type\":\"tagcloud\",\"params\":{\"scale\":\"linear\",\"orientation\":\"single\",\"minFontSize\":12,\"maxFontSize\":24},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"userIdentity.sessionContext.sessionIssuer.userName.keyword\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  },
  {
    "_id": "Owner_PieChart",
    "_type": "visualization",
    "_source": {
      "title": "Owner_PieChart",
      "visState": "{\"title\":\"Owner_PieChart\",\"type\":\"pie\",\"params\":{\"shareYAxis\":true,\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"@owner.keyword\",\"size\":10,\"order\":\"desc\",\"orderBy\":\"1\"}}],\"listeners\":{}}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"*\",\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"filter\":[]}"
      }
    }
  }
]
```
</div>
</details>