CloudWatchLogGroupsのログをKinesis Firehoseを通じてS3に出力するようにし、ロググループの保持期間をcdkv2から変更したメモ

## 概要
cdkを使ってlambdaを作成するとcloudwatchlogが自動で作成されるが、有効期限が無期限となる。
今回、firehoseを通じてs3にデータを保存する設定を行う。
合わせて、cloudwatchの有効期限を6か月とする設定を適用できるようにする。

[ソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/9)

**09.07 エラーが発生するようになっていたため修正。**
[修正後のソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/10)

## 環境

+ Windows10
+ gitbash
+ aws-cli/2.7.22 Python/3.9.11 Windows/10 exe/AMD64 prompt/off
* cdk v2.37.1
* node v16.16.0

## フォルダ構造

```
- cdk
  + bin ... 実行ファイル置き場
  - tsconfig.json
  - package.json
  - lib
    - log-retension-stack.ts
    - log-s3-firehose.ts
  - constants
    - log-groups.json  ... aws cliで作成されるcloudwatch logs のロググループ名の記載されたファイル
```

## ソース
### 事前準備

* log-groupをcdk内で動的に取得することはできないため、事前に取得しておく。

```json:cdk/pacakge.json
{
  "scripts": {
    "prelog-retention-deploy": "aws logs describe-log-groups --query \"logGroups[?contains(logGroupName,`/aws/lambda/hoge`)].logGroupName\" > constants/log-groups.json",
    "log-retention-deploy": "cdk -a \"npx ts-node --prefer-ts-exts bin/log-retention.ts\" destroy LogS3FireHoseStack  --profile produser"
  }
}
```

`npm run prelog-retention-deploy`を実行すると、以下のファイルができる。

```json:cdk/constants/log-groups.json
[
  "/aws/lambda/hoge"
]
```

### firehoseを通じてlog groupsがログをs3に吐くようにする
* S3の保管期間は180日とする
* すべてのログを吐き出すものとする


```ts:cdk/lib/log-s3-firehose.ts
import * as core from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { Aspects, Tag } from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as firehose from "@aws-cdk/aws-kinesisfirehose-alpha";
import * as kinesisDestinations from '@aws-cdk/aws-kinesisfirehose-destinations-alpha';
import * as logsDestinations from 'aws-cdk-lib/aws-logs-destinations';
import * as kinesis from 'aws-cdk-lib/aws-kinesis'
import { aws_logs as logs } from 'aws-cdk-lib';
import logGroups from '../constants/log-groups.json';

interface Props extends core.StackProps {
  bucketName: string
}

export class LogS3FireHoseStack extends core.Stack {
  constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props)
    const bucket = new s3.Bucket(this, props.bucketName, {
      bucketName: props.bucketName,
      accessControl: s3.BucketAccessControl.PRIVATE,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: core.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      lifecycleRules: [{
        expiration: core.Duration.days(180)
      }]
    })
    const sourceStream = new kinesis.Stream(this, 'Source Stream');
    const deliveryStream = new firehose.DeliveryStream(this, 'Delivery Stream', {
      sourceStream,
      destinations: [new kinesisDestinations.S3Bucket(bucket, {
        dataOutputPrefix: 'log-archive/!{timestamp:yyyy}/!{timestamp:MM}/!{timestamp:dd}/!{timestamp:HHmmss}/!{firehose:random-string}',
        errorOutputPrefix: 'log-archive-failures/!{firehose:error-output-type}/!{timestamp:yyyy}/anyMonth/!{timestamp:dd}',
      })],
    });

    logGroups.forEach(name => {
      const logGroup = logs.LogGroup.fromLogGroupName(this, `log-group-${name}`, name);
      logGroup.addSubscriptionFilter(`subscription-filter-${name}`, {
        destination: new logsDestinations.KinesisDestination(sourceStream),
        filterPattern: logs.FilterPattern.allEvents()
      })
    })
  }
}
```

#### 確認

S3を開き、デプロイされたことを確認する。
firehoseが最初に次のようなエラーのJSONを吐くが、次以降からはcloud watch logsの情報を吐くようになるので気にしなくてよい。 `"message":"CWL CONTROL MESSAGE: Checking health of destination Firehose."}`
lambdaを稼働して確認後、ログが出力されるまで5分から 10分ほどのタイムラグがあるので、確認にはコーヒーでも淹れて一服する時間が必要になる。
この設定ではログは`.gz`形式で吐き出される。
S3からダウンロードした後、解凍する。
gitbashの下記コマンドで解凍可能。(windowsでは名前の変更で`.gz`のsuffixをDL後につけておく必要がある）

```bash
gzip -d Taj2ctoZYelLogS3FireHoseStack-DeliveryStreamF6D5572D-ummy9TmuHerw-2-2022-08-14-16-16-53-6421b6dc-7bd2-4f5b-a60f-2bb8e7b51094.gz
```

## CloudWatch Log Groupsの保持期間の変更
### CDKで行う
S3にバックアップが保存されるようになったため、CloudWatchからは2か月でログが消えるようにする。

```cdk/lib/log-retention-stack.ts
import { Aspects, Stack, StackProps, Tag } from 'aws-cdk-lib';
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { aws_logs as logs } from 'aws-cdk-lib';
import logGroups from '../constants/log-groups.json';

export class CdkV2LogRetentionStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    logGroups.forEach((name => {
      const logRetention = new logs.LogRetention(this, `log-retention-${name}`, {
        logGroupName: name,
        retention: logs.RetentionDays.TWO_MONTHS,
      });
    }))
  }
}
```

### cliで保持期間の変更を行う
cdkで管理のメリットがなさそうなので、手軽にcliからできないか試してみた。
```package.json
  "scripts":{
    "set-log-retention": "aws logs describe-log-groups --query \"logGroups[?contains(logGroupName,'/aws/lambda/hello-jwt')].logGroupName\" | jq -r \".[]\" | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --log-group-name VAR_LOG_GROUP_NAME --retention-in-days 30"
```

実行したところ、以下のエラーが発生。

```
 at 'logGroupName' failed to satisfy constraint: Member must satisfy regular expression pattern: [\.\-_/#A-Za-z0-9]+ted: Value '/aws/lambda/hello
```

[stackoverflow](https://stackoverflow.com/questions/53654186/aws-log-get-log-events-log-group-name-problem)の言うことには、`gitbash`を使用していることが原因。
グループ名が`/`で始まっているのを、パスとして認識しているようだ。(参考:[Azure CLI を Git Bash で使う時の必須環境変数 MSYS_NO_PATHCONV](https://qiita.com/mnrst/items/bb25eae3fcdf5fc53429))

対処法を適用してもう一度試してみる。

```
MSYS_NO_PATHCONV=1 npm run set-log-retention
```

改善されなかった。
パイプでコマンドを実行すると、一時的に付与した`MSYS_NO_PATHCONV`の環境変数が引き継がれない模様。

bashの起動コマンドのほうの対応を行う。`.bash_profile`に`export MSYS_NO_PATHCONV=1`を記載

```
$cat ~/.bashrc
export MSYS_NO_PATHCONV=1
```

まだ改善されない。
ログの出方が少しおかしいと思ったら、最後に改行が混じっていた。

```
 | jq -r ".[]" | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --retention-in-days 30 --log-group-name VAR_LOG_GROUP_NAME
```

sedで改行を消したところ、うまくいった。

```
    "set-log-retention": "aws logs describe-log-groups --query \"logGroups[?contains(logGroupName,'/aws/lambda/hello-jwt')]\" | jq -r \".[].logGroupName\" | sed \"s/^ *\\| *$//\"  | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --log-group-name VAR_LOG_GROUP_NAME --retention-in-days 30",
```

### 絞り込んで設定する

* `--log-group-name-prefix /aws/lambda/hello ` のように、前方一致でのサーバでの絞り込みが可能
  * そのほかのサーバ側での絞り込みは分からなかった

```
    "set-log-retention": "aws logs describe-log-groups --log-group-name-prefix /aws/lambda/hello --query \"logGroups[?contains(logGroupName,'/aws/lambda/hello-jwt')]\" | jq -r \".[].logGroupName\" | sed \"s/^ *\\| *$//\"  | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --log-group-name VAR_LOG_GROUP_NAME --retention-in-days 30"
```

### 保持期間が設定されていないもののみ設定する
* `--query \"logGroups[?retentionInDays!=null]\"`で保持期間が設定されていないものに対して設定可能


```
    "set-log-retention": "aws logs describe-log-groups --log-group-name-prefix /aws/lambda/hello --query \"logGroups[?retentionInDays==null]\" | jq -r \".[].logGroupName\" | sed \"s/^ *\\| *$//\"  | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --log-group-name VAR_LOG_GROUP_NAME --retention-in-days 30"
```

### 保持期間が一定より短いもののみ設定する
* `--query "logGroups[?retentionInDays<`30`]"`で可能。
* なお、`<30`と記載すると、文字列は指定できないエラーとなる。` Bad value for --query logGroups[?retentionInDays<30]: invalid token: Parse error at column 27, token "30" (NUMBER), for expression:
"logGroups[?retentionInDays<30]"`


```
    "set-log-retention": "aws logs describe-log-groups --log-group-name-prefix /aws/lambda/hello --query \"logGroups[?retentionInDays<`30`]\" | jq -r \".[].logGroupName\" | sed \"s/^ *\\| *$//\"  | xargs -IVAR_LOG_GROUP_NAME aws logs put-retention-policy --log-group-name VAR_LOG_GROUP_NAME --retention-in-days 60"

```


なお、保持期間の数値は限られている。指定できない数値を指定すると以下のエラー
```
An error occurred (InvalidParameterException) when calling the PutRetentionPolicy operation: Invalid retention value. Valid values are: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922, 3288, 3653]
```

## 2022.09.07 エラーが発生するようになった。

以下のエラーが発生。
```
Could not deliver test message to specified Kinesis stream. Check if the given kinesis stream is in ACTIVE state.
```
[aws-cdk issues #21827](https://github.com/aws/aws-cdk/issues/21827)でも同様の現象が発生との報告あり。
IAMポリシーのデプロイより前に SubscriptionFilter がデプロイされており、テスト メッセージの送信でkinesisにアクセスできずに失敗している模様。

[AlexStaskoさんのカスタムcdk修正](https://github.com/AlexStasko/aws-subscription-filter-issue/blob/main/lib/app-stack.ts)を参考に、SubscriptionFilterを修正したカスタムリソースを作成する。
[addSubscriptionFilter](https://github.com/aws/aws-cdk/blob/4525be85ff89a9606b5891d5a5a33438e57460e2/packages/%40aws-cdk/aws-logs/lib/log-group.ts#L119)は、内部的には new [SubscriptionFilter](https://github.com/aws/aws-cdk/blob/4525be85ff89a9606b5891d5a5a33438e57460e2/packages/%40aws-cdk/aws-logs/lib/subscription-filter.ts)をしているのみなので、[元のソース](https://github.com/aws/aws-cdk/blob/4525be85ff89a9606b5891d5a5a33438e57460e2/packages/%40aws-cdk/aws-logs/lib/subscription-filter.ts)からポリシーの適用以外に行っていることの差異はない。

```diff

// 省略


    logGroups.forEach(name => {
      const logGroup = logs.LogGroup.fromLogGroupName(this, `log-group-${name}`, name);
-       logGroup.addSubscriptionFilter(`subscription-filter-${name}`, {
-         destination: new logsDestinations.KinesisDestination(sourceStream),
-         filterPattern: logs.FilterPattern.allEvents()
-       })
+      new FixedSubscriptionFilter(this, `subscription-filter-${name}`, {
+        logGroup,
+        destination: new logsDestinations.KinesisDestination(sourceStream),
+        filterPattern: logs.FilterPattern.allEvents(),
+      }
    })
    core.Tags.of(this).add('Project', 'log-s3')

    // 作成したリソース全てにタグをつける
    Aspects.of(this).add(new Tag('Stack', id));
  }

}


+ class FixedSubscriptionFilter extends core.Resource {
+  constructor(scope: Construct, id: string, props: logs.SubscriptionFilterProps) {
+    super(scope, id);
+
+    const destProps = props.destination.bind(this, props.logGroup);
+
+    const filter = new logs.CfnSubscriptionFilter(this, 'Resource', {
+      logGroupName: props.logGroup.logGroupName,
+      destinationArn: destProps.arn,
+      roleArn: destProps.role && destProps.role.roleArn,
+      filterPattern: props.filterPattern.logPatternString,
+    });
+
+    // Add dependency on policy
+    if (destProps.role) {
+      const policy = destProps.role.node.tryFindChild('DefaultPolicy');
+      if (policy) {
+        filter.node.addDependency(policy);
+      } else {
+        throw new Error('there is no policy');
+      }
+    }
+  }
+ }
```

[修正後のソースコード](https://github.com/hibohiboo/aws-cdk-v2/tree/id/10)

 
## 参考
[CloudWatch Logs を定期的にS3 へ転送する](https://oji-cloud.net/2021/08/04/post-6462/)
[cdk LogRetention](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogRetention.html)
[cdk firehose](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-kinesisfirehose-alpha-readme.html#sources)
[cdk logs destination](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs_destinations-readme.html)
[cdk filter pettern](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.FilterPattern.html)

## 調査
[list-all-loggroups-using-cdk](https://stackoverflow.com/questions/67212678/list-all-loggroups-using-cdk)
[CDKv2を使ってLambdaのログ内容からエラー通知をする構成を作る](https://zenn.dev/enish/articles/ca6a1808507b41)
[【CLI無し】CloudWatch Logs上のログデータをKinesis Data Firehoseを用いてリアルタイムでS3に2次保存する](https://www.isoroot.jp/blog/4426/)
[フィルターパターン](https://bftnagoya.hateblo.jp/entry/2022/04/04/131651)
[cli filter](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-usage-filter.html)
[jme path](https://dev.classmethod.jp/articles/aws-cli-query-jmspath-tostring-grep/)
[Azure CLI を Git Bash で使う時の必須環境変数 MSYS_NO_PATHCONV](https://qiita.com/mnrst/items/bb25eae3fcdf5fc53429)
[AWS CLI 出力をフィルタリングする](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-usage-filter.html)
[JMESPath Specification](https://jmespath.org/specification.html#functions)