AWS

CloudWatchLogGroupsログのS3へのバックアップをAWS CDKで実装してみた

akira

はじめに

急に寒くなってきて暖房の点け時に悩んでいるakiraです。
本記事ではCloudWatchLogGroupsに出力されたログのS3へのバックアップをCDKを用いて作成してみたのでご紹介いたします。

今回やることの概要

今回の構成としてはロググループに対してサブスクリプションフィルターを作成しKinesis Firehorseを経由してS3へと保存する構成を取っております。また、すでに作成済みの環境に導入できるよう、既存のCDKに組み込むのではなく独立してログの出力が設定できるにしております。

作業の大まかな流れは以下のとおりです。

大まかな作業の流れ

スクリプトを用いてCloudWatchLogGroupsのログ一覧を取得

npm runを利用してAWSコマンドを実行しログ一覧のリストを配列としてJSON形式で取得する

CDKの実装

必要に応じて設定を加えて実行する

バックアップ作成の確認

問題なく作成されていれば成功

実際にやってみる

スクリプトを用いて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年経過したものは削除するライフサイクルポリシーを設定しております。

Q
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,
        });
    });
  }
}

「'--resolveJsonModule' を使用して '.json' 拡張子を持つモジュールをインポートすることをご検討ください。」といったエラーが出る場合

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へと出力することが出来ました。こうすることでログの保管コストを低減しつつログを保管することが出来るかと思います。

今回の構成では全てのログが同一の.gzファイルへと出力されてしまいます。ご利用いただく際はログの保管要件等に合わせて修正の上ご利用ください。

AUTHOR
akira
akira
記事URLをコピーしました