AWS CloudTrailのログをAmazon Elasticsearch Serviceへ転送して可視化してみた

まえがき

おはようございます、加藤です。
最近、Amazon Elasticsearch ServiceがElasticsearch6.2に対応しましたね。
いい機会なので、AWS CloudTrailのログをElasticsearchに送ってKibanaで可視化してみました。

アーキテクト図

フロー

  1. CloudTrailで全リージョンを対象にログを取得
  2. ログをS3とCloudWatch Logsに保存
  3. CloudWatch LogsをトリガーにしてLambdaを起動
  4. LambdaからElasticsearchへログを転送

VPNServerの構築

今回の構成はElasticsearchをVPC内に配置します。その為、端末がVPCに接続できる必要があります。
私はSoftEtherVPNを構築してVPNを利用して接続しました。VPN接続方法は下記のブログをご参考ください。

SecurityGroupの作成

Lambda、Elasticsearch用のセキュリティグループを作成します。

Lambda用のSecurityGroup

ElasticsearchのSecurityGroupで送信元指定する為に使用します。つまりアウトバウンドのみ許可された初期状態でOKです。
名前は"lambda"としました。

方向 タイプ プロトコル ポート範囲 送信元/送信先
アウトバウンド すべてのトラフィック すべて すべて 0.0.0.0/0
アウトバウンド すべてのトラフィック すべて すべて ::/0

ElasticsearchのSecurityGroup

VPNServer、Lambdaからの接続を許可します。アウトバウンドは必要ありませんが、ワザワザ消さなくとも良いと考え残しておきました。
名前は"elasticsearch"としました。

方向 タイプ プロトコル ポート範囲 送信元/送信先
インバウンド すべてのトラフィック すべて すべて "VPNServerのSG"
インバウンド すべてのトラフィック すべて すべて "LambdaのSG"
アウトバウンド すべてのトラフィック すべて すべて 0.0.0.0/0
アウトバウンド すべてのトラフィック すべて すべて ::/0

Elasticsearchの作成

Amazon Elasticsearch Service Management Console

Elasticsearchは作成後、アクティブになるまでに少し時間がかかるので最初に作成しておきます。

Elasticsearchドメイン名を入力します。"cloudtrail"としました。

クラスター設定はデフォルトのままで行きます。求められる可用性に応じて変更してください。

VPC,Subnet,SecurityGroupを指定します。
SubnetはプライベートなSubnetを指定しました。
SecurityGroupは上記で作成したElasticsearch用のものを指定します。

アクセスポリシーは"IAM認証情報を使用した署名リクエストを要求しない"を選択してください。選択するとポリシーが自動生成されます。

最後に確認画面が表示されるので、問題がなければ完了してください。

CloudTrailとS3の作成

CloudTrail Management Console

CloudTrailを作成します。

証跡名とバケット名は"cloudtrail-all-region-?????????"としました。
?????????には命名要件に準拠して任意の文字列を設定してください。

次にCloudWatch Logsへ出力を設定します。
ロググループ名は"CloudTrail/SampleLogGroup"としました。存在しないロググループなので新規作成されます。

IAMロールは新規作成します。
ロール名は"CloudTrail_CloudWatchLogs_Role_Sample"としました。

再試行を要求されるので、再度設定します。

ポリシー名は"oneClick_CloudTrail_CloudWatchLogs_Role_XXXXXXXXXXXXX"を選択します。これは1回目の操作で作成されたポリシーです。

下記のように表示されれば正常に設定できています。

CloudWatch LogsからElasticsearchへログを転送設定

CloudWatch Management Console

作成したロググループを選択し、"Amazon Elasticsearch Service"へのストリーミング開始を選択します。

Amazon ES クラスターは作成した"cloudtrail"を選択します。
Lambda IAM実行ロールを新規作成します。"新しいIAMロールの作成"を選択します。選択するとロール作成の画面へ移行します。

自動生成されるポリシーではVPC内でLambdaを実行する為の権限が足りないので編集します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "es:ESHttpPost",
            "Resource": "arn:aws:es:*:*:*"
        }
    ]
}

ログの形式は"AWS CloudTrail"を選択します。

後は"次へ"で進み、"ストリーミングの開始"を選択します。

Lambda関数の修正

Lambda Management Console

関数のSubnet、SecurityGroupを修正します。
"LogsToElasticsearch_cloudtrail"という関数が対象です。

VPC,Subnet,SecurityGroupを設定し、保存します。

Kibanaの設定

ログの取り込み

Amazon Elasticsearch Service Management Console

作成されたElasticsearchの管理画面を開き、KibanaのURLを確認します。

VPN接続し、URLへアクセスします。
Kibanaの画面を確認できました!

IndexPatternを設定します。

"cwl-"YYYY.MM.DDという形式で出力されているので、パターンは"cwl-*"とします。

時刻のフィールドを"@timestamp"とします。

ログを確認してみましょう。表示されない場合は、右上に集計期間を設定する項目があるので"Today"などに設定してみてください。

ログを可視化する

さらにログを可視化します。 まずは下記のJSONをファイルとして保存してください。このファイルはKibanaのCloudTrail向けダッシュボードの設定をエクスポートしたものです。

[
  {
    "_id": "CloudTrail",
    "_type": "dashboard",
    "_source": {
      "title": "CloudTrail",
      "hits": 0,
      "description": "",
      "panelsJSON": "[{\"panelIndex\":\"1\",\"gridData\":{\"x\":6,\"y\":0,\"w\":6,\"h\":4,\"i\":\"1\"},\"id\":\"eventName_AreaChart\",\"type\":\"visualization\",\"embeddableConfig\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}},\"version\":\"6.2.2\"},{\"panelIndex\":\"2\",\"gridData\":{\"x\":3,\"y\":0,\"w\":3,\"h\":2,\"i\":\"2\"},\"id\":\"Region_PieChart\",\"type\":\"visualization\",\"embeddableConfig\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}},\"version\":\"6.2.2\"},{\"panelIndex\":\"4\",\"gridData\":{\"x\":6,\"y\":4,\"w\":6,\"h\":4,\"i\":\"4\"},\"id\":\"errorCode_AreaChart\",\"type\":\"visualization\",\"embeddableConfig\":{\"spy\":{\"mode\":{\"fill\":false,\"name\":null}},\"vis\":{\"legendOpen\":true}},\"version\":\"6.2.2\"},{\"panelIndex\":\"5\",\"gridData\":{\"x\":0,\"y\":2,\"w\":6,\"h\":2,\"i\":\"5\"},\"id\":\"userName_TagCloud\",\"type\":\"visualization\",\"version\":\"6.2.2\"},{\"panelIndex\":\"6\",\"gridData\":{\"x\":0,\"y\":4,\"w\":6,\"h\":3,\"i\":\"6\"},\"id\":\"sessionIssuer.userName_TagCloud\",\"type\":\"visualization\",\"version\":\"6.2.2\"},{\"panelIndex\":\"7\",\"gridData\":{\"x\":0,\"y\":7,\"w\":6,\"h\":3,\"i\":\"7\"},\"id\":\"InvokedBy_TagCloud\",\"type\":\"visualization\",\"version\":\"6.2.2\"},{\"panelIndex\":\"8\",\"gridData\":{\"x\":0,\"y\":0,\"w\":3,\"h\":2,\"i\":\"8\"},\"id\":\"Owner_PieChart\",\"type\":\"visualization\",\"version\":\"6.2.2\"}]",
      "optionsJSON": "{\"darkTheme\":false,\"useMargins\":false}",
      "version": 1,
      "timeRestore": false,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"filter\":[],\"query\":{\"query\":{\"query_string\":{\"analyze_wildcard\":true,\"query\":\"*\",\"default_field\":\"*\"}},\"language\":\"lucene\"},\"highlightAll\":true,\"version\":true}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":{},\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\",\"setYExtents\":false,\"defaultYExtents\":false},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"eventTime\",\"interval\":\"s\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"errorCode.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true,\"default_field\":\"*\"}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  },
  {
    "_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\":{},\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\",\"setYExtents\":false,\"defaultYExtents\":false},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}]},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"3\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"eventTime\",\"interval\":\"s\",\"customInterval\":\"2h\",\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"eventName.keyword\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}",
      "uiStateJSON": "{}",
      "description": "",
      "version": 1,
      "kibanaSavedObjectMeta": {
        "searchSourceJSON": "{\"index\":\"3132b4b0-2ccb-11e8-b880-b7ccac63ebdf\",\"query\":{\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true,\"default_field\":\"*\"}},\"language\":\"lucene\"},\"filter\":[]}"
      }
    },
    "_meta": {
      "savedObjectVersion": 2
    }
  }
]

このJSONファイルは下記の記事を参考にさせて頂きました。
複数AWSアカウントのCloudTrailをまとめてKibana(Elasticsearch Service)で可視化する - Qiita

Kibanaに保存したJSONをインポートします。

ダッシュボードを確認してみます。

ダッシュボードが表示されました!

あとがき

Lambdaのポリシーが自動生成されたものだと、VPC内に配置される場合に対応しておらず、少しハマりました...
ダッシュボードはElasticsearch6.2ではエラーになってしまった箇所を修正しただけで、引用元のQiitaの設定から変更していません。とても素晴らしい記事でした。