注意点
ここに書かれている設計では、各AWSアカウントのLambdaがログ集約先のElasticsearch Serviceに直接書き込んでいる。
ログの出力量は調整できないのでElasticsearchの書き込み負荷を制御できない。
Elasticsearch Serviceの前段にFirehoseを挟むなどすれば負荷を監視・制御できる(試してない)。
クッションを入れることでElasticsearch Serviceをスケールさせる余裕が生まれるはず。
また、Firehoseを挟むことでLambdaでの変換処理や変換済みデータのS3へのバックアップなど柔軟に対応できる(試してない)。
(参考)
Amazon Kinesis Data Firehose で Amazon Elasticsearch Service 7.x クラスターへのデータストリーム配信をサポート
概要
いくつかのAWSアカウントでCloudTrailの設定を行い、そのログをまとめ用のAWSアカウントに送る。
送られたログは、S3とElasticsearch Serviceに蓄える。
Elasticsearch ServiceのKibanaを利用して可視化する。
まとめ用のAWSアカウントを「管理アカウント」と呼ぶことにする。
手順
- 管理アカウントでCloudTrailの設定を行い、S3バケット作成とCloudWatch Logsへの出力を行う(最初のログ)
- 管理アカウントでElasticsearch Serviceのドメインを作成する
- ドメイン作成後、CloudWatch Logs → (Lambda) → Elasticsearch Serviceの出力設定を行う
- その他のアカウントでCloudTrailの設定を行い、管理アカウントのS3バケットと自アカウントのCloudWatch Logsへの出力を行う(2つ目以降のログ)
- CloudWatch Logs → (Lambda) → 管理アカウントのElasticsearch Serviceの出力設定を行う
- 以降、4→5の手順を繰り返す
管理アカウント
CloudTrailのログを収集しKibanaで可視化するAWSアカウント。
1つ目のログ収集対象アカウントとしてCloudTrailの設定も行う。
CloudTrailの初期設定
Trailの新規作成。
出力先としてS3バケットとCloudWatch Logsを選択する。
S3への出力設定
バケット
アーカイブ用のバケットを新規作成する。
バケットポリシー
CloudTrailサービスに対してバケットへの書き込みを許可する。
ポリシーは自動的に作成される。
{
"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
ロールとポリシーが勝手に作成される。
既存ロールがある場合ポリシーだけ新規作成したり、そこは環境に応じて適当に。
{
"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制限をかけた。
{
"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を集約する
設定方法の詳細は公式ドキュメント(IndexTemplates)を参照。
バージョンによって形式に違いがあるので注意。下記の例は5.1の場合。
パスの最後にあるテンプレート名の cloudtrail
の部分は何でもよさそう。
$ 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が作成される。
ここでは同一アカウントのドメインを指定する。
あとは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。
{
"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が必要。
設定完了の時点でlambda_elasticsearch_execution
ロールからElasticsearch Serviceドメインへのアクセスを許可してないのでエラーになるはず。
なので、lambda_elasticsearch_execution
ロールが作られた時点で設定完了前に後述のアクセス許可設定を行った。
設定完了後LogsToElasticsearchEx_cloudtrail_000000000000
というLambdaファンクションができているはず。
Elasticsearch Serviceへのアクセス許可
アクセスポリシー
管理アカウントのElasticsearch Serviceでアクセスポリシーを変更し、
自分のLambda実行ロール(lambda_elasticsearch_execution
)からのes:ESHttpPost
アクションを許可する。
アカウントを追加したらPrincipal
を配列にすればいいと思う。
{
"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やエラーコードを絞ったあと、その条件をピン留めすることで、クエリー画面に検索条件を持ち越しできる。
クエリー画面では実際のログを詳細に見ることができる。
可視化ダッシュボード用設定の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\":[]}"
}
}
}
]