AWS CDK : 特定のAPIコールを監視してみた (CloudTrail, EventBridge)

# Terraform版はZennにあげてます
https://zenn.dev/wakaka23/articles/cab6da21d5ea0e

# 概要
CloudTrailを用いて特定のAPIコールを監視する方法として下記の2パターンがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/63af7343-8b20-d988-e04b-d100cf912651.png)

今回は「1. EventBridgeのイベントソースとしてAPIを指定」のような「CloudTrailを用いて管理されたAPIコールをEventBridgeでフィルタリングし、特定のAPIが呼び出された時にSNS通知を行う」という構成をCDKで検証してまとめてみました。基本的にはBLEAテンプレートをベースにしております。

https://github.com/aws-samples/baseline-environment-on-aws/tree/main

# 構成図
以下に示したようなAPIコールを監視する設計としています。
- IAM Policyの作成・削除、アタッチ・デタッチ
- Rootユーザのコンソールサインイン
- Access Keyの新規作成
- SGルールの作成・削除
- NACLの作成・削除、NACLルールの作成・削除、NACLのサブネットへの関連付け
- CloudTrailのロギング停止、証跡削除、証跡の設定変更

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/00254a90-3c4a-68c8-9dbf-8a72f99db019.png)

IAMリソースの操作ログはus-east-1に記録されるため、IAMの操作ログをSNSと連携するためのEventBridgeをus-east-1に、それ以外の操作ログをSNSと連携するためのEventBridgeをap-northeast-1に構築する必要があることに注意が必要です。

https://dev.classmethod.jp/articles/check-iam-operation-by-aws-cloudtrail/

# コンソール画面

### ap-northeast-1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/42f91087-b560-f2ec-84f1-0ca03b2f8fda.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/06f0c590-b4d9-40b2-9edf-57635fdf4d0d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/2e126d34-4a08-bf22-a127-93f4de23c3b1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/22a03ddb-7b91-bad1-a586-9154efdabd58.png)

### us-east-1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/cce7783b-08ec-eadc-773c-ac4d248fad97.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2746031/e183e1a3-4cb5-f183-8436-fe35062a9b5e.png)

# ディレクトリ構造
本記事ではbin, lib配下のファイルを紹介します。
```
├── bin
│   └── aws_cloud_trail_sample.ts
├── lib
│   └── Stack
│       ├── tokyo-stack.ts
│       └── us-stack.ts
│   └── Construct
│       ├── cloudtrail.ts
│       ├── eventbridge.ts
│       └── sns.ts
├── node_modules
├── test
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
```

# CDK実装コード

## bin

### aws_cloud_trail_sample.ts

Stackの`env.region`プロパティを指定することにより、Stack毎にDeploy先のリージョンを指定する必要があります。指定しないとAWSの認証情報で設定したリージョンにデプロイされてしまいます。（aws configureで確認可能）

```typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsTokyoStack } from '../lib/Stack/tokyo-stack';
import { AwsUsStack } from '../lib/Stack/us-stack';

const app = new cdk.App();

new AwsTokyoStack(app, 'AwsTokyoStack', {
    env: {region: "ap-northeast-1"},
});

new AwsUsStack(app, 'AwsUsStack', {
    env: {region: "us-east-1"},
});
```

## lib/Stack
- tokyo-stack.ts : ap-northeast-1に構築するCloudTrail証跡、EventBridgeルール、SNSトピックを定義
- us-stack.ts : us-east-1に構築するEventBridgeルール、SNSトピックを定義

今回の構成では証跡のホームリージョンはap-northeast-1に設定し、マルチリージョン証跡を有効化しています。そのため、全てのリージョンの証跡ログを格納するS3バケットはap-northeast-1に配置されます。

### tokyo-stack.ts

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SnsConstruct } from '../Construct/sns';
import { TrailConstruct } from '../Construct/cloudtrail';
import { EventsConstruct } from '../Construct/eventbridge';

export class AwsTokyoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ----- SNS Topic -----
    const Topic_for_Alarm = new SnsConstruct(this, "Topic1", {
      notifyEmail: "xxxxxxx@gmail.com"
    }).createTopic();
    
    // ----- CloudTrail Trail -----
    new TrailConstruct(this, "Test", {})

    // -----EventBridge Rules -----
    new EventsConstruct(this, "Rules", {
      region: "ap-northeast-1",
      topic: Topic_for_Alarm,
    })
    
  }
}
```

### us-stack.ts

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SnsConstruct } from '../Construct/sns';
import { EventsConstruct } from '../Construct/eventbridge';

export class AwsUsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ----- SNS Topic -----
    const Topic_for_Alarm = new SnsConstruct(this, "Topic1", {
      notifyEmail: "xxxxxxx@gmail.com"
    }).createTopic();

    // -----EventBridge Rules -----
    new EventsConstruct(this, "Rules", {
      region: "us-east-1",
      topic: Topic_for_Alarm,
    })
    
  }
}
```

## lib/Construct

### cloudtrail.ts

証跡ログを格納するS3バケットとCloudTrail証跡を定義しています。今回はCloudWatch Logsに配信はしていません。（Logsは高額になりかねませんからね...）

```typescript
import { Construct } from "constructs";
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as trail from 'aws-cdk-lib/aws-cloudtrail';

export interface TrailProps{}

export class TrailConstruct extends Construct{

    constructor(scope: Construct, id: string, props: TrailProps) {
        super(scope, id);

        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html
        const TrailBucket = new s3.Bucket(this, "TrailBucket", {
            blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
            encryption: s3.BucketEncryption.S3_MANAGED,
        });

        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudtrail.Trail.html#sendtocloudwatchlogs
        new trail.Trail(this, "TestTrail", {
            bucket: TrailBucket,
            enableFileValidation: true,
            isMultiRegionTrail: true,
            sendToCloudWatchLogs: false,
        });
    }

}
```

### eventbridge.ts

`env.region`プロパティの値がap-northeast-1なのかus-east-1なのかに応じて呼び出す関数を変えることで、リージョンごとに異なるEventBridgeルールを一つのConstructでデプロイすることを実現しています。

```typescript
import { Construct } from "constructs";
import * as events from 'aws-cdk-lib/aws-events'
import * as sns from "aws-cdk-lib/aws-sns"
import * as eventsTarget from 'aws-cdk-lib/aws-events-targets';

export interface EventsProps {
    region: string,
    topic: sns.ITopic,
}

export class EventsConstruct extends Construct {

    constructor(scope: Construct, id: string, props: EventsProps) {
        super(scope, id);

        if (props.region === "ap-northeast-1") {
            this.createRulesInTokyo(props);
        } else if (props.region === "us-east-1") {
            this.createRuleInUs(props);
        }

    }

    // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events-readme.html
    // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events_targets-readme.html
    private createRulesInTokyo(props: EventsProps): void{
        new events.Rule(this, "SgChangeEventRule", {
            description: 'Notify to create, update or delete a Security Group.',
            enabled: true,
            eventPattern: {
                source: ['aws.ec2'],
                detailType: ['AWS API Call via CloudTrail'],
                detail: {
                    eventSource: ['ec2.amazonaws.com'],
                    eventName: [
                        'AuthorizeSecurityGroupIngress',
                        'AuthorizeSecurityGroupEgress',
                        'RevokeSecurityGroupIngress',
                        'RevokeSecurityGroupEgress',
                    ],
                },
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });

        new events.Rule(this, "NACLChangeEventRule", {
            description: 'Notify to create, update or delete a Network ACL.',
            enabled: true,
            eventPattern: {
                source: ['aws.ec2'],
                detailType: ['AWS API Call via CloudTrail'],
                detail: {
                    eventSource: ['ec2.amazonaws.com'],
                    eventName: [
                        'CreateNetworkAcl',
                        'CreateNetworkAclEntry',
                        'DeleteNetworkAcl',
                        'DeleteNetworkAclEntry',
                        'ReplaceNetworkAclEntry',
                        'ReplaceNetworkAclAssociation',
                    ],
                },
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });

        new events.Rule(this, "CloudTrailChangeEventRule", {
            description: 'Notify to change on CloudTrail log configuration',
            enabled: true,
            eventPattern: {
                detailType: ['AWS API Call via CloudTrail'],
                detail: {
                    eventSource: ['cloudtrail.amazonaws.com'],
                    eventName: ['StopLogging', 'DeleteTrail', 'UpdateTrail'],
                },
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });
    }
        
    private createRuleInUs(props: EventsProps): void{
        new events.Rule(this, "IAMPolicyChangeEventRule", {
            description: 'Notify to change IAM Configuration.',
            enabled: true,
            eventPattern: {
                source: ['aws.iam'],
                detailType: ['AWS API Call via CloudTrail'],
                detail: {
                    eventSource: ['iam.amazonaws.com'],
                    eventName: [
                        'DeleteGroupPolicy',
                        'DeleteRolePolicy',
                        'DeleteUserPolicy',
                        'PutGroupPolicy',
                        'PutRolePolicy',
                        'PutUserPolicy',
                        'CreatePolicy',
                        'DeletePolicy',
                        'CreatePolicyVersion',
                        'DeletePolicyVersion',
                        'AttachRolePolicy',
                        'DetachRolePolicy',
                        'AttachUserPolicy',
                        'DetachUserPolicy',
                        'AttachGroupPolicy',
                        'DetachGroupPolicy'
                    ]
                }
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });

        new events.Rule(this, "RootSigninRule", {
            description: "Events rule for monitoring root AWS Console Sign In activity",
            enabled: true,
            eventPattern: {
                source: ["aws.signin"],
                detailType: ["AWS Console Sign In via CloudTrail"],
                detail: {
                    userIdentity: {
                        type: ["Root"],
                    },
                    responseElements: {
                        ConsoleLogin: ["Success"],
                    }
                }
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });

        new events.Rule(this, "NewAccessKeyCreatedRule", {
            description: "Events rule for monitoring access key creation",
            enabled: true,
            eventPattern: {
                source: ["aws.iam"],
                detailType: ["AWS Console Sign In via CloudTrail"],
                detail: {
                    eventSource: ["iam.amazonaws.com"],
                    eventName: ["CreateAccessKey"]
                }
            },
            targets: [new eventsTarget.SnsTopic(props.topic)],
        });
    }

}
```

### sns.ts

```typescript
import { Construct } from "constructs";
import * as sns from "aws-cdk-lib/aws-sns"

export interface SnsProps {
    notifyEmail: string;
}

export class SnsConstruct extends Construct {
    private readonly topic: sns.ITopic

    constructor(scope: Construct, id: string, props: SnsProps) {
        super(scope, id);

        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sns-readme.html
        const topic = new sns.Topic(this, "Topic for Alarm",{});

        new sns.Subscription(this, "MonitorAlarmEmail", {
            endpoint: props.notifyEmail,
            protocol: sns.SubscriptionProtocol.EMAIL,
            topic: topic,
        });
        this.topic = topic;
    }

    // not SnsConstruct but sns.ITopic
    public createTopic(): sns.ITopic {
        return this.topic;
    }
}
```

# 最後に
この構成にすると複数リージョンにEventBridgeやSNSをデプロイする必要があるので、少し管理も面倒ですね。CloudTrail→CloudWatch Logs→メトリクスフィルタ→Alarm→SNSの構成の方が柔軟かつシンプルに管理できそうです。