0
2

CloudwatchLogs を Kinesis Firehose 経由で S3 に保存する for CDK

Last updated at Posted at 2024-08-16

構成図

CloudWatchLogsToFirefoseToS3.png

やりたいこと

今回検証用のため、EC2 に Apache を起動し、
access_log を S3 bucket-A に格納し、
error_log を S3 bucket-B に格納
できるようする。

なお、S3 に格納するとき、CloudWatchLogs を S3 のプレフィックスにして保存する。

以降の説明では、A は access_log、B は error_log とする。

IAM ロールは CloudWatchLogs ごと、Firefose ごとに作成されるとリソース数が増えるため、今回は A と B それぞれ共通のものを利用する。

CDK 構造図

tree
bin
└── CloudwatchLogsToFirehoseToS3.ts
lib
├── CloudwatchLogsToFirehoseToS3Construct.ts
├── CloudwatchLogsToFirehoseToS3Stack.ts
└── parameters.ts

CloudwatchLogsToFirehoseToS3Construct.ts で Construct として作成し、
CloudwatchLogsToFirehoseToS3Stack
 A と B
が作成されるようにしています。

CDK コード

環境ごとにデプロイできるようしておく。

bin/CloudwatchLogsToFirehoseToS3.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CloudwatchLogsToFirehoseToS3Stack } from '../lib/CloudwatchLogsToFirehoseToS3Stack';

const app = new cdk.App();
new CloudwatchLogsToFirehoseToS3Stack(app, 'Dev-CloudwatchLogsToFirehoseStack', {
  environmentName: 'dev',
  // env: { account: '123456789012', region: 'ap-northeast-1' }, 
});

new CloudwatchLogsToFirehoseToS3Stack(app, 'Stg-CloudwatchLogsToFirehoseStack', {
  environmentName: 'stg',
  // env: { account: '123456789012', region: 'ap-northeast-1' }, 
});

new CloudwatchLogsToFirehoseToS3Stack(app, 'Prod-CloudwatchLogsToFirehoseStack', {
  environmentName: 'prd',
  // env: { account: '123456789012', region: 'ap-northeast-1' }, 
});

A,B ごとに作成したいため、Construct を定義する。

lib/CloudwatchLogsToFirehoseToS3Construct.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as firehose from 'aws-cdk-lib/aws-kinesisfirehose';
import * as logs from 'aws-cdk-lib/aws-logs';

interface CloudwatchLogsToFirehoseToS3ConstructProps {
  readonly environmentName: string;
  readonly productNames: string;
  readonly bucketName: string;
  readonly logGroupNames: string[];
  readonly iD: string;
}

export class CloudwatchLogsToFirehoseToS3Construct extends Construct {
  constructor(scope: Construct, id: string, props: CloudwatchLogsToFirehoseToS3ConstructProps) {
    super(scope, id);

    const { environmentName, productNames, bucketName, logGroupNames, iD } = props;

    // S3バケットを作成
    const s3Bucket = new s3.Bucket(this, `S3Bucket`, {
      bucketName,
      removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
    });

    // Firefose用のエラーロググループを作成
    const firehoseErrorLogGroup = new logs.LogGroup(this, `FirehoseErrorLogGroup`, {
      logGroupName: `/aws/kinesisfirehose/${environmentName}-${productNames}-firehose-error-log-${iD}`,
      retention: logs.RetentionDays.ONE_MONTH,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Firehose用のIAMロールを作成
    const firehoseRole = this.createFirehoseRole(environmentName, productNames, s3Bucket, iD, firehoseErrorLogGroup);

    // CloudWatch LogsからFirehoseへの送信を許可するIAMロールを作成
    const logsToFirehoseRole = this.createLogsToFirehoseRole(environmentName, productNames, iD);

    // CloudWatch LogsグループとFirehoseを作成
    this.createCloudWatchLogGroupsAndFirehose(environmentName, productNames, s3Bucket, logGroupNames, firehoseRole, logsToFirehoseRole, firehoseErrorLogGroup);
  }

  // Firehose用のIAMロールを作成するメソッド
  private createFirehoseRole(environmentName: string, productNames: string, bucket: s3.Bucket, iD: string, firehoseErrorLogGroup: logs.LogGroup): iam.Role {
    const firehoseRole = new iam.Role(this, `FirehoseRole-${iD}`, {
      roleName: `${environmentName}-${productNames}-firehose-role-${iD}`,
      assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'),
    });

    firehoseRole.addToPolicy(new iam.PolicyStatement({
      actions: [
        's3:AbortMultipartUpload',
        's3:GetBucketLocation',
        's3:GetObject',
        's3:ListBucket',
        's3:ListBucketMultipartUploads',
        's3:PutObject'
      ],
      resources: [
        bucket.bucketArn,
        `${bucket.bucketArn}/*`
      ],
    }));

    firehoseRole.addToPolicy(new iam.PolicyStatement({
      actions: [
        'logs:PutLogEvents',
        'logs:CreateLogStream'
      ],
      resources: [
        firehoseErrorLogGroup.logGroupArn,
        `${firehoseErrorLogGroup.logGroupArn}:*`
      ],
    }));

    return firehoseRole;
  }

  // CloudWatch LogsからFirehoseへの送信を許可するIAMロールを作成するメソッド
  private createLogsToFirehoseRole(environmentName: string, productNames: string, iD: string): iam.Role {
    const logsToFirehoseRole = new iam.Role(this, `LogsToFirehoseRole-${iD}`, {
      roleName: `${environmentName}-${productNames}-logs-to-firehose-role-${iD}`,
      assumedBy: new iam.ServicePrincipal('logs.amazonaws.com'),
    });

    return logsToFirehoseRole;
  }

  // CloudWatch LogsとFirehoseを作成するメソッド
  private createCloudWatchLogGroupsAndFirehose(
    environmentName: string,
    productNames: string,
    bucket: s3.Bucket,
    logGroupNames: string[],
    firehoseRole: iam.Role,
    logsToFirehoseRole: iam.Role,
    firehoseErrorLogGroup: logs.LogGroup,
  ): void {
    // ロググループごとにサブスクリプションフィルターとFirehoseを作成
    for (const logGroupName of logGroupNames) {
      // ロググループ名の '/' を '-' に置き換える
      const sanitizedLogGroupName = logGroupName.replace(/\//g, '-');

      new logs.LogStream(this, `firehoseLogStream-${sanitizedLogGroupName}`, {
        logGroup: firehoseErrorLogGroup,
        logStreamName:sanitizedLogGroupName,
      });

      // Firehoseのデリバリーストリームを作成
      const firehoseStream = new firehose.CfnDeliveryStream(this, `FirehoseStream-${sanitizedLogGroupName}`, {
        deliveryStreamName: `${environmentName}-${productNames}-firehose-stream-${sanitizedLogGroupName}`,
        deliveryStreamType: 'DirectPut',
        extendedS3DestinationConfiguration: {
          bucketArn: bucket.bucketArn,
          prefix: `${logGroupName}/`,
          errorOutputPrefix: `${logGroupName}/error/`,
          roleArn: firehoseRole.roleArn,
          bufferingHints: { intervalInSeconds: 300, sizeInMBs: 5 },
          cloudWatchLoggingOptions: {
            enabled: true,
            logGroupName: firehoseErrorLogGroup.logGroupName,
            logStreamName: `${sanitizedLogGroupName}`,
          },
        },
      });

      // CloudWatch LogsのIAMロールにポリシーを追加
      logsToFirehoseRole.addToPolicy(new iam.PolicyStatement({
        actions: ['firehose:PutRecord'],
        resources: [firehoseStream.attrArn],
      }));

      // ロググループを作成
      const logGroup = new logs.LogGroup(this, `LogGroup-${sanitizedLogGroupName}`, {
          logGroupName,
          retention: logs.RetentionDays.ONE_MONTH,
          removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
        });

      // サブスクリプションフィルターを作成してログをFirehoseに送信
      const subscriptionFilter = new logs.CfnSubscriptionFilter(this, `SubscriptionFilter-${sanitizedLogGroupName}`, {
        filterName: `${environmentName}-${productNames}-subscription-filter-${sanitizedLogGroupName}`,
        logGroupName: logGroup.logGroupName,
        filterPattern: '',
        destinationArn: firehoseStream.attrArn,
        roleArn: logsToFirehoseRole.roleArn,
      });

      subscriptionFilter.node.addDependency(firehoseStream);
      subscriptionFilter.node.addDependency(logsToFirehoseRole);
    }
  }
}

A、B のスタックを作成。

lib/CloudwatchLogsToFirehoseToS3Stack.ts
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { CloudwatchLogsToFirehoseToS3Construct } from './CloudwatchLogsToFirehoseToS3Construct';
import * as parameters from './parameters';

interface CloudwatchLogsToFirehoseToS3StackProps extends StackProps {
  readonly environmentName: string;
}

export class CloudwatchLogsToFirehoseToS3Stack extends Stack {
  constructor(scope: cdk.App, id: string, props: CloudwatchLogsToFirehoseToS3StackProps) {
    super(scope, id, props);

    const environmentName = props.environmentName;
    const productNames = parameters.productNames;

    // Aのスタックを作成
    new CloudwatchLogsToFirehoseToS3Construct(this, 'CloudwatchLogsToFirehoseToS3ConstructA', {
      environmentName,
      productNames,
      bucketName: `${environmentName}-${productNames}-s3bucket-a`,
      logGroupNames: parameters.logGroupNamesA,
      iD: 'A',
    });

    // Bのスタックを作成
    new CloudwatchLogsToFirehoseToS3Construct(this, 'CloudwatchLogsToFirehoseToS3ConstructB', {
      environmentName,
      productNames,
      bucketName: `${environmentName}-${productNames}-s3bucket-b`,
      logGroupNames: parameters.logGroupNamesB,
      iD: 'B',
    });
  }
}

ロググループをパラメータにリストとして定義。

lib/parameters.ts
export const productNames = "<プロダクト名>";

export const logGroupNamesA = [
  "access_log/A001",
  "access_log/A002",
];

export const logGroupNamesB = [
  "error_log/B001",
  "error_log/B002",
];

CDK デプロイ

開発環境にのみデプロイする。

cdk deploy Dev-CloudwatchLogsToFirehoseStack

CloudWatch エージェント設定ファイル

001 の EC2インスタンスの CloudWatch エージェント設定ファイルは以下の通りとします。

001 opt/aws/amazon-cloudwatch-agent/bin/config.json
{
        "agent": {
                "run_as_user": "root"
        },
        "logs": {
                "logs_collected": {
                        "files": {
                                "collect_list": [
                                        {
                                                "file_path": "/var/log/httpd/access_log",
                                                "log_group_class": "STANDARD",
                                                "log_group_name": "access_log/A001",
                                                "log_stream_name": "{instance_id}",
                                                "retention_in_days": 30
                                        },
                                        {
                                                "file_path": "/var/log/httpd/error_log",
                                                "log_group_class": "STANDARD",
                                                "log_group_name": "error_log/B001",
                                                "log_stream_name": "{instance_id}",
                                                "retention_in_days": 30
                                        }
                                ]
                        }
                }
        }
}

002 の EC2インスタンスの CloudWatch エージェント設定ファイルは以下の通りとします。
(今回は、検証のため当方では、002 の EC2インスタンスは作成しませんでした。)

002 opt/aws/amazon-cloudwatch-agent/bin/config.json
{
        "agent": {
                "run_as_user": "root"
        },
        "logs": {
                "logs_collected": {
                        "files": {
                                "collect_list": [
                                        {
                                                "file_path": "/var/log/httpd/access_log",
                                                "log_group_class": "STANDARD",
                                                "log_group_name": "access_log/A002",
                                                "log_stream_name": "{instance_id}",
                                                "retention_in_days": 30
                                        },
                                        {
                                                "file_path": "/var/log/httpd/error_log",
                                                "log_group_class": "STANDARD",
                                                "log_group_name": "error_log/B002",
                                                "log_stream_name": "{instance_id}",
                                                "retention_in_days": 30
                                        }
                                ]
                        }
                }
        }
}

確認

CDK をデプロイした時点で、S3 にテストデータが格納されたことを確認できます。

スクリーンショット 2024-08-16 12.52.32.png

念の為、Apache にリクエストを実行し、
s3://dev-XXX-s3bucket-a/access_log/A001/YYYY/MM/DD/hh/
に access_log が S3 へ格納されたことを確認します。

スクリーンショット 2024-08-16 13.03.57.png

同じく、Apache で意図的にエラーを引き起こし、
s3://dev-XXX-s3bucket-b/error_log/B001/YYYY/MM/DD/hh/
に error_log が S3 へ格納されたことを確認します。

スクリーンショット 2024-08-16 13.49.29.png

0
2
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
0
2