CloudWatchLogGroupsログのS3へのバックアップをAWS CDKで実装してみた
はじめに
急に寒くなってきて暖房の点け時に悩んでいるakiraです。
本記事ではCloudWatchLogGroupsに出力されたログのS3へのバックアップをCDKを用いて作成してみたのでご紹介いたします。
今回やることの概要
今回の構成としてはロググループに対してサブスクリプションフィルターを作成しKinesis Firehorseを経由してS3へと保存する構成を取っております。また、すでに作成済みの環境に導入できるよう、既存のCDKに組み込むのではなく独立してログの出力が設定できるにしております。
作業の大まかな流れは以下のとおりです。
大まかな作業の流れ
npm run
を利用してAWSコマンドを実行しログ一覧のリストを配列としてJSON形式で取得する
必要に応じて設定を加えて実行する
問題なく作成されていれば成功
実際にやってみる
スクリプトを用いてCloudWatchLogGroupsのログ一覧を取得
package.json
にスクリプトを追加する(9行目)。
ここでは全てのロググループ名を取得し、lib/constructors/loglist.json
に書き込むように指定している。
{
....
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk",
"log-listing": "aws logs describe-log-groups --query \"logGroups[*].logGroupName\" > ./lib/constructors/loglist.json"
},
"devDependencies": {
....
上記の追加ができたら以下のコマンドを実行し、ロググループ名を取得する。
npm run log-listing
> cloud_watch_logs-bkup@0.1.0 log-listing
> aws logs describe-log-groups --query "logGroups[*].logGroupName" > ./lib/constructors/loglist.json
成功すると、以下のようなJSONファイルが作成されます。
[
"/aws/ecs/containerinsights/log-backup-test-Cluster/performance",
"/aws/kinesisfirehose/log-backup-test-Firehose",
"/aws/lambda/log-backup-test-lmd-function",
"/aws/lambda/log-backup-test-logbkup-function",
"/aws/rds/cluster/log-backup-test-database/audit",
"/aws/rds/cluster/log-backup-test-database/error",
"/aws/rds/cluster/log-backup-test-database/general"
]
CDKの実装
今回の実行環境の構成概要は以下のイメージです。
デフォルトの状態から基本的にcloud_watch_logs-bkup-stack.ts
を編集するだけで動作する想定です。
.
├── bin
│ └── ...
├── cdk.json
├── cdk.out
│ ├── ...
├── lib
│ ├── cloud_watch_logs-bkup-stack.ts
│ ...
├── node_modules
└── tsconfig.json
早速cloud_watch_logs-bkup-stack.ts
を編集していきます。
基本的にはコードを確認いただければと思いますが、今回はリストしたロググループをFirehorseでバッファし300秒経過、もしくは5MBのログが溜まるとS3に出力されるようにしております。
S3では作成から90日経過したものはGLACIERへ移行し、1年経過したものは削除するライフサイクルポリシーを設定しております。
cloud_watch_logs-bkup-stack.ts
-
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as kinesisfirehose from "aws-cdk-lib/aws-kinesisfirehose"; import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; // 作成したロググループリストの読み込み import logGroups from './constructors/loglist.json'; export class CloudWatchLogsBkupStack extends cdk.Stack { constructor(scope: Construct, id: string, props: cdk.StackProps) { super(scope, id, props); // 任意の識別子を付与してください const environmentName = "test-logbackup"; // 保存先のS3 bucket const logBackupBucket = new s3.Bucket(this, 'logBackupBucket', { bucketName: `all-log-backup`, autoDeleteObjects: false, removalPolicy: cdk.RemovalPolicy.RETAIN, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, objectLockEnabled: true, enforceSSL: true, versioned: true, lifecycleRules:[ { id:`lifecycle-rule`, // 保持期間90日経過したものはGLACIER transitions: [{ storageClass: s3.StorageClass.GLACIER, transitionAfter: cdk.Duration.days(90), }], // 1年経過したものは削除 expiration: cdk.Duration.days(365), noncurrentVersionExpiration: cdk.Duration.days(1) } ] }); // 必要となるRoleの準備 // Firehorse to S3 const destinationRole = new iam.Role(this, 'Destination Role', { roleName:`DestinationRole`, assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'), }); destinationRole.attachInlinePolicy( new iam.Policy(this, 'S3Permission', { statements: [ new iam.PolicyStatement({ actions: [ 's3:AbortMultipartUpload', 's3:GetBucketLocation', 's3:GetObject', 's3:ListBucket', 's3:ListBucketMultipartUploads', 's3:PutObject' ], resources: [logBackupBucket.bucketArn, logBackupBucket.arnForObjects('*')], }), ], }), ); // CW Logs to Firehorse const cwLogIngestionRole = new iam.Role(this, 'cwlogIngestionRole', { roleName:`cwlogIngestionRole`, assumedBy: new iam.ServicePrincipal('logs.' + cdk.Stack.of(this).region + '.amazonaws.com'), }); cwLogIngestionRole.attachInlinePolicy( new iam.Policy(this, 'putLogsPermission', { statements: [ new iam.PolicyStatement({ actions: ['firehose:*'], resources: ['arn:aws:firehose:' + cdk.Stack.of(this).region + ':' + cdk.Stack.of(this).account + ':*'], }), ], }), ); // 配信先Firehoseの作成 const firehoseDeliveryStream = new kinesisfirehose.CfnDeliveryStream(this, 'FirehoseDeliveryStream', { deliveryStreamName: `Firehose`, extendedS3DestinationConfiguration: { bucketArn: logBackupBucket.bucketArn, roleArn: destinationRole.roleArn, errorOutputPrefix: `${environmentName}/error/`, prefix: `${environmentName}/`, bufferingHints: { intervalInSeconds: 300, sizeInMBs: 5, }, }, }); // 取得したログリストを用いた作業 logGroups.forEach(loglist=>{ // ログ名から"/"を削除する // 先頭の / を削除 const tmpLogList = loglist.startsWith('/') ? loglist.slice(1) : loglist; // その後、/ を - に変換 const replacedLogList = tmpLogList.replace(/\//g, '-'); // loglistを指定して取得 const myLogGroup = logs.LogGroup.fromLogGroupName(this, `${replacedLogList}-LogGroup`, loglist ); new logs.LogStream(this, `${replacedLogList}-LogStream`, { logStreamName: `${replacedLogList}-LogStream`, logGroup: myLogGroup, removalPolicy: cdk.RemovalPolicy.DESTROY }); // 各ログに対してサブスクリプションフィルターを作成する new logs.CfnSubscriptionFilter(this, `${replacedLogList}-LogGroupSubscription`, { filterName: `${replacedLogList}-LogGroupSubscription`, destinationArn: firehoseDeliveryStream.attrArn, filterPattern: "", logGroupName: myLogGroup.logGroupName, roleArn: cwLogIngestionRole.roleArn, }); }); } }
import logGroups from './constructors/loglist.json';
の箇所に対して上記のエラーが出てしまう場合があります。原因の1つとしてはtsconfig.json
の設定が不足している事が考えられます。私の環境では以下の2行を追加することで解決しました。
{
"compilerOptions": {
"target": "ES2020",
...
"resolveJsonModule": true,
"esModuleInterop": true,
...
},
"exclude": [
"node_modules",
"cdk.out"
]
}
バックアップ作成の確認
成功するとFirehorseが作成されます。
作成されたログはS3へと.gz
形式にて出力されます。
まとめ
今回の手法を取ることで既存のCDK環境等に手を加えずにCloudWatchLogGroupsのログをS3へと出力することが出来ました。こうすることでログの保管コストを低減しつつログを保管することが出来るかと思います。