Lambda実行ログ用のCloudWatch LogsをCloudFormation / CDKで管理する


# Lambda用のCloudWatch LogsをIaC管理下に置きたい

CloudFormation（以下CFn）やCDKでログ書き込み権限のあるLambdaを作成すると、
Lambda実行時にログが自動でCloudWatch Logsに書き込まれます。
しかしこの時作成されたロググループはIaC管理外となるため、
- ロググループの保持期間やDeletionPolicy等の設定ができない
- スタック削除してもロググループが残ってしまう

といったもどかしさがあります。
当記事ではCFn, CDKそれぞれでLambda実行ログ用のCloudWatch Logsをコード管理する方法をご紹介します。


# CFnでのCloudWatch Logs管理方法

CFnでLambda用のCloudWatch Logsを管理する場合は、以下のテンプレートのように
ロググループ名にLambda関数名を含めるようにして作成します。

```yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:
  LambdaRole:
    # Lambdaの実行に必要なIAMロール
    Type: AWS::IAM::Role
    Properties:
      RoleName: lambda-role
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        # CloudWatch Logsへのログ書き込み権限があるIAMポリシー
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /

  # Lambda関数
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: lambda-function
      Description: LambdaFunction using python3.12.
      Runtime: python3.12
      Code:
        ZipFile: |
          import json

          def handler(event, context):
              return {
                  'statusCode': 200,
                  'body': json.dumps({
                      'message': 'hello world!'
                  })
              }
      Handler: index.handler
      MemorySize: 128
      Timeout: 10
      Role: !GetAtt LambdaRole.Arn

  ### ⭐️Lambda関数のCloudWatchロググループ ###
  LambdaFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      # ${LambdaFunction}では、上記で定義したLambda関数の"FunctionName"を参照しています
      LogGroupName: !Sub /aws/lambda/${LambdaFunction} 
```

一番下にCloudWatch Logsのロググループを定義しました。
Lambda関数を実行した時に作成されるロググループ名は、デフォルトだと
`/aws/lambda/<Lambda関数名>`となるため、
同じ名前になるように以下のようにロググループ名を定義しています。

```yaml
LogGroupName: !Sub /aws/lambda/${LambdaFunction} 
```

これにより、Lambda用のロググループをCFnで管理できるようになります。
また、DeletionPolicyやログの保持期間を設定することも可能になります。

```yaml
  # イメージ
  LambdaFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Delete      # DeletionPolicyを設定
    UpdateReplacePolicy: Delete # UpdateReplacePolicyを設定
    Properties:
      LogGroupName: !Sub /aws/lambda/${LambdaFunction}
      RetentionInDays: 14       # ログの保持期間を指定
```

## デモ
1.テンプレートをデプロイしてスタック作成（ロググループが含まれている）

![スクリーンショット 2024-11-04 21.31.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/de8b398c-d46a-8806-c69a-98e12e404932.png)

2.Lambda関数を手動でテスト実行
↓
3.CFn管理下のロググループにログが保存されていたことが確認できました。

![スクリーンショット 2024-11-04 21.00.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/58be9309-3615-5fe3-ba14-1f26f5a9a564.png)


# CDKでのCloudWatch Logs管理方法(2パターン)

### CDK実行環境
- CDKのバージョン：2.158.0
- 言語：TypeScript

CDKでLambda用のCloudWatch Logsを管理する方法は以下の２パターンあります。

## ①ロググループを作成し、FunctionのlogGroupプロパティで指定する
１つ目の方法：

以下のようにまずLogGroupコンストラクトを定義し、続いてFunctionコンストラクトのlogGroupプロパティに指定することで
Lambda関数とロググループを関連づけられます。

```typescript
import { RemovalPolicy, Stack, type StackProps } from "aws-cdk-lib";
import {
	Code,
	Function,
	Runtime,
} from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import type { Construct } from "constructs";

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

		// ⭐️ まずLambda用のロググループを作成
		const helloLogGroup = new logs.LogGroup(this, "HelloLogGroup", {
			retention: logs.RetentionDays.ONE_WEEK, // ログの保持期間を指定（必要に応じて）
			removalPolicy: RemovalPolicy.DESTROY,   // RemovalPolicyを設定（必要に応じて）
		});

		// Lambda関数を作成
		new Function(this, "HelloHandler", {
			logGroup: helloLogGroup, // ⭐️ ここで、上記で作成したロググループを指定
			runtime: Runtime.NODEJS_20_X,
			code: Code.fromAsset("lambda"),
			handler: "hello.handler",
		});
	}
}
```

参考コード：[AWS公式CDKワークショップ](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/ja-JP/2000-typescript-workshop)

なお、この方法で作成したロググループ名はデフォルトの
`/aws/lambda/<Lambda関数名>`とはならず、
`<スタック名>-<ロググループ名>-<ランダムな英数字>`となります。

![スクリーンショット 2024-11-17 20.55.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/c714b401-5845-b8f7-5f15-74317cdeacee.png)

## ②ロググループを作成し、ロググループ名にLambda関数名を含める
２つ目の方法：

ロググループを作成し、以下のように
ロググループ名に作成したLambda関数名を含めることで
Lambda関数とロググループを関連づけられます。

```typescript
import { RemovalPolicy, Stack, type StackProps } from "aws-cdk-lib";
import {
	Code,
	Function,
	Runtime,
} from "aws-cdk-lib/aws-lambda";
import * as logs from "aws-cdk-lib/aws-logs";
import type { Construct } from "constructs";

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

		// まずLambda関数を作成
		const helloHandler = new Function(this, "HelloHandler", {
			runtime: Runtime.NODEJS_20_X,
			code: Code.fromAsset("lambda"),
			handler: "hello.handler",
		});

		// ⭐️ Lambda用のロググループを作成
		new logs.LogGroup(this, "HelloLogGroup", {
			// ⭐️ ロググループ名を`/aws/lambda/<Lambda関数名>`にする
			logGroupName: `/aws/lambda/${helloHandler.functionName}`,
			retention: logs.RetentionDays.ONE_WEEK, // ログの保持期間を指定（必要に応じて）
			removalPolicy: RemovalPolicy.DESTROY,   // RemovalPolicyを設定（必要に応じて）
		});
	}
}
```

作成されたロググループは、
デフォルトの`/aws/lambda/<Lambda関数名>`になります。

![スクリーンショット 2024-11-17 21.19.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/613419/b14f0434-b150-5f98-dcd2-8619df583c8d.png)