LoginSignup
2
1

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

Last updated at Posted at 2024-04-29

概要

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

image.png

今回は「2. CloudTrailログのCloudWatch Logsへの格納及びメトリクスフィルタ・アラームの指定」のような「CloudTrailを用いて管理されたAPIコールログをCloudWatch Logsに転送し、メトリクスフィルタでフィルタリング、特定のAPIが呼び出された時にSNS通知を行う」という構成をCDKで検証してまとめてみました。「1. EventBridgeのイベントソースとしてAPIを指定」の構成は以下の記事をご覧ください。

構成図

以下に示したようなAPIコールを監視する設計としています。

  • IAM Policyの作成・削除、アタッチ・デタッチ
  • Rootユーザのコンソールサインイン
  • Access Keyの新規作成
  • SGルールの作成・削除
  • NACLの作成・削除、NACLルールの作成・削除、NACLのサブネットへの関連付け
  • CloudTrailのロギング停止、証跡削除、証跡の設定変更

image.png

コンソール画面

メトリクスフィルタ

image.png
image.png
image.png

CloudWatch Alarm

image.png
image.png
image.png
image.png
image.png
image.png
image.png

ディレクトリ構造

本記事ではbin, lib配下のファイルを紹介します。

├── bin
│   └── aws_cloud_trail_sample2.ts
├── lib
│   └── Stack
│       ├── aws_cloud_trail_sample2-stack.ts
│   └── Construct
│       ├── cloudtrail.ts
│       ├── cloudwatch.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_sample2.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsCloudTrailSample2Stack } from '../lib/Stack/aws_cloud_trail_sample2-stack';

const app = new cdk.App();
new AwsCloudTrailSample2Stack(app, 'AwsCloudTrailSample2Stack', {});

lib/Stack

  • aws_cloud_trail_sample2-stack.ts : CloudTrail証跡、Logsロググループ、Alarmを定義

今回は「ap-northeast-1に存在する単一のロググループに全てのリージョンのAPIコールを格納し、メトリクスフィルタを適用することでメトリクスを吐き出す」という構成になっているため、「CloudTrail→EventBridge→SNS」の構成とは異なり、ap-northeast-1のみにStackをデプロイするだけで十分となります。

tokyo-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { TrailConstruct } from '../Construct/cloudtrail';
import { SnsConstruct } from '../Construct/sns';
import { CwConstruct } from '../Construct/cloudwatch';

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

    const FirstTrail = new TrailConstruct(this, "CloudTrail", {}).createLogsGroup()

    const TopicForAlarm = new SnsConstruct(this, "SNSTopic", {
      notifyEmail: "xxxxxxx@gmail.com"
    }).createTopic();

    const LogsAndAlarm = new CwConstruct(this, "CloudWatch", {
      TrailLogGroup: FirstTrail,
      AlarmTopic: TopicForAlarm,
    })

  }
}

lib/Construct

cloudtrail.ts

証跡ログを格納するS3バケット、配信先のCloudWatch Logsロググループ、CloudTrail証跡を定義しています。監視することを目的としているため、ロググループの保持期間は1週間と短めに設計しています。

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

export interface TrailProps{}

export class TrailConstruct extends Construct{
    private readonly trailLogGroup_att: logs.LogGroup;

    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_logs-readme.html
        const cloudTrailLogGroup = new logs.LogGroup(this, 'CloudTrailLogGroup', {
            retention: logs.RetentionDays.ONE_WEEK,
        });
        this.trailLogGroup_att = cloudTrailLogGroup;

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

    public createLogsGroup(): logs.LogGroup {
        return this.trailLogGroup_att;
    }

}

cloudwatch.ts

メトリクスフィルタとCloudWatch Alarmを定義しています。今回もBLEAテンプレートをベースにフィルタやAlarmを設計しています。

import { Construct } from "constructs";
import * as cdk from 'aws-cdk-lib';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as sns from "aws-cdk-lib/aws-sns";
import * as cw from 'aws-cdk-lib/aws-cloudwatch';
import * as cwa from "aws-cdk-lib/aws-cloudwatch-actions";

export interface CloudwatchProps {
    TrailLogGroup: logs.LogGroup;
    AlarmTopic: sns.ITopic;
}

export class CwConstruct extends Construct {

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

        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.MetricFilter.html
        const RootLoginMetricsFilter = new logs.MetricFilter(this, "RootLoginMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{($.userIdentity.type = "Root")&&($.eventName = "ConsoleLogin")}',
            },
            filterName: "Metrics Filter to detect console login by root",
            metricNamespace: "CWLogs",
            metricName: "console-login-by-root",
            metricValue: "1",
        });

        const IAMChangeMetricsFilter = new logs.MetricFilter(this, "IAMChangeMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{($.eventName = DeleteGroupPolicy) || ($.eventName = DeleteRolePolicy) || ($.eventName = DeleteUserPolicy) || \
                        ($.eventName = PutGroupPolicy) || ($.eventName = PutRolePolicy) || ($.eventName = PutUserPolicy) || \
                        ($.eventName = CreatePolicy) || ($.eventName = DeletePolicy) || ($.eventName = CreatePolicyVersion) || \
                        ($.eventName = DeletePolicyVersion) || ($.eventName = AttachRolePolicy) || ($.eventName = DetachRolePolicy) || \
                        ($.eventName = AttachUserPolicy) || ($.eventName = DetachUserPolicy) || ($.eventName = AttachGroupPolicy) || \
                        ($.eventName = DetachGroupPolicy)}'
            },
            filterName: "Metrics Filter to detect changes in IAM policy",
            metricNamespace: "CWLogs",
            metricName: "changes-in-IAM-policy",
            metricValue: "1",
        });

        const CreateAccessKeyMetricsFilter = new logs.MetricFilter(this, "CreateAccessKeyMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{$.eventName = "CreateAccessKey"}',
            },
            filterName: "Metrics Filter to detect creation of access keys",
            metricNamespace: "CWLogs",
            metricName: "creation-of-access-key",
            metricValue: "1",
        });

        const SGChangeMetricsFilter = new logs.MetricFilter(this, "SGChangeMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{($.eventName = "AuthorizeSecurityGroupIngress") || ($.eventName = "AuthorizeSecurityGroupEgress") || \
                        ($.eventName = "RevokeSecurityGroupIngress") || ($.eventName = "RevokeSecurityGroupEgress")}',
            },
            filterName: "Metrics Filter to detect changes in SG rules",
            metricNamespace: "CWLogs",
            metricName: "changes-in-SG-rules",
            metricValue: "1",
        });

        const NACLChangeMetricsFilter = new logs.MetricFilter(this, "NACLChangeMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{($.eventName = "CreateNetworkAcl") || ($.eventName = "CreateNetworkAclEntry") || \
                        ($.eventName = "DeleteNetworkAcl") || ($.eventName = "DeleteNetworkAclEntry") || \
                        ($.eventName = "ReplaceNetworkAclEntry") || ($.eventName = "ReplaceNetworkAclAssociation")}'
            },
            filterName: "Metrics Filter to detect changes in NACL",
            metricNamespace: "CWLogs",
            metricName: "changes-in-NACL",
            metricValue: "1",
        });

        const TrailChangeMetricsFilter = new logs.MetricFilter(this, "TrailChangeMetricsFilter", {
            logGroup: props.TrailLogGroup,
            filterPattern: {
                logPatternString:
                    '{($.eventName = "StopLogging") || ($.eventName = "DeleteTrail") || ($.eventName = "UpdateTrail")}'
            },
            filterName: "Metrics Filter to detect changes in CloudTrail",
            metricNamespace: "CWLogs",
            metricName: "changes-in-CloudTrail",
            metricValue: "1",
        });

        // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html
        new cw.Alarm(this, "RootLoginAlarm", {
            metric: RootLoginMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "RootLoginAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));

        new cw.Alarm(this, "IAMChangeAlarm", {
            metric: IAMChangeMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "IAMChangeAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));

        new cw.Alarm(this, "CreateAccessKeyAlarm", {
            metric: CreateAccessKeyMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "CreateAccessKeyAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));

        new cw.Alarm(this, "SGChangeAlarm", {
            metric: SGChangeMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "SGChangeAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));

        new cw.Alarm(this, "NACLChangeAlarm", {
            metric: NACLChangeMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "NACLChangeAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));

        new cw.Alarm(this, "TrailChangeAlarm", {
            metric: TrailChangeMetricsFilter.metric({
                statistic: cw.Stats.SUM,
                period: cdk.Duration.seconds(60)
            }),
            comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
            threshold: 1,
            evaluationPeriods: 1,
            datapointsToAlarm: 1,
            treatMissingData: cw.TreatMissingData.NOT_BREACHING,
            alarmName: "TrailChangeAlarm",
        }).addAlarmAction(new cwa.SnsAction(props.AlarmTopic));
    }

}

sns.ts

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, "TopicForAlarm",{});

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

    public createTopic(): sns.ITopic {
        return this.topic;
    }
}

最後に

今回は一回でもAPIが呼び出されたら通知するような設計としましたが、Alarmを調整することで単位時間あたりに複数回APIが呼び出されたら通知するみたいな設計にもできるので、「1. EventBridgeのイベントソースとしてAPIを指定」のパターンよりこっちの構成の方が個人的には好みですね。IAMのAPIコールのためにus-east-1にEventBridgeやSNSトピックを作成する必要がなく、単一のリージョンで完結させることができますし。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1