構成図
やりたいこと
今回検証用のため、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 構造図
bin
└── CloudwatchLogsToFirehoseToS3.ts
lib
├── CloudwatchLogsToFirehoseToS3Construct.ts
├── CloudwatchLogsToFirehoseToS3Stack.ts
└── parameters.ts
CloudwatchLogsToFirehoseToS3Construct.ts
で Construct として作成し、
CloudwatchLogsToFirehoseToS3Stack
で
A と B
が作成されるようにしています。
CDK コード
環境ごとにデプロイできるようしておく。
#!/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 を定義する。
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 のスタックを作成。
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',
});
}
}
ロググループをパラメータにリストとして定義。
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 エージェント設定ファイルは以下の通りとします。
{
"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インスタンスは作成しませんでした。)
{
"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 にテストデータが格納されたことを確認できます。
念の為、Apache にリクエストを実行し、
s3://dev-XXX-s3bucket-a/access_log/A001/YYYY/MM/DD/hh/
に access_log が S3 へ格納されたことを確認します。
同じく、Apache で意図的にエラーを引き起こし、
s3://dev-XXX-s3bucket-b/error_log/B001/YYYY/MM/DD/hh/
に error_log が S3 へ格納されたことを確認します。