aws-cdkで既存のCloudWatch Logsロググループと組み合わせてCloudFormationテンプレートを作成した話とか

f:id:kidani_a:20190204072421j:plain

連日の雲の写真である。芸がない。

既存のCloudWatch Logsロググループに対して、メトリクスフィルターとCloudWatchアラームを作成する、という簡単なタスクがあった。

aws-cdkでCloudFormationテンプレートを出力し、スタックの作成を行った際に問題が起きたので、解決方法をメモしておく。

 

 

[aws-cdkでメトリクスフィルターを実装する]

CloudWatch Logsの画面をみると、すでにMyLogGroupというロググループが存在している。

f:id:kidani_a:20190207013302p:plain

 

このロググループに対してメトリクスフィルターとアラームを設定したいので、aws-cdkを用いてCloudFormationスタックを実装していく。aws-cdkは昨日に続き0.23.0を利用している。

kdnakt.hatenablog.com

 

まずはプロジェクトの作成から。TypeScriptの経験はcdk以外にないが、なんとなくそちらを選択。

$ mkdir cdk-log-group
$ cd cdk-log-group/
$ cdk init --language=typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN cdk-log-group@0.1.0 No repository field.
npm WARN cdk-log-group@0.1.0 No license field.

# Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

 

続いて、@aws-cdk/aws-logsパッケージをインストールする。

$ npm i @aws-cdk/aws-logs
npm WARN cdk-log-group@0.1.0 No repository field.
npm WARN cdk-log-group@0.1.0 No license field.

+ @aws-cdk/aws-logs@0.23.0
added 3 packages from 1 contributor and audited 606 packages in 5.319s
found 0 vulnerabilities

 

f:id:kidani_a:20190207014024p:plain

このようなディレクトリ構成になっているので、lib/cdk-log-group-stack.tsファイルを開き、実装していく。

以下がプロジェクト初期化直後のスタックの実装。まだ何もない。

import cdk = require('@aws-cdk/cdk');

export class CdkLogGroupStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
  }
}

 

まずはコメント部分を削除して、ロググループを定義し、メトリクスフィルターとアラームを追加する。

import cdk = require('@aws-cdk/cdk');
import { LogGroup } from '@aws-cdk/aws-logs'
import { Alarm, Metric } from '@aws-cdk/aws-cloudwatch';

export class CdkLogGroupStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const myLogGroup = new LogGroup(this, 'MyLogGroup', {
      logGroupName: 'MyLogGroup',
    });
    myLogGroup.newMetricFilter(this, 'MyMetricFilter', {
      filterPattern: {
        logPatternString: 'Error',
      },
      metricName: 'MyMetric',
      metricNamespace: 'LogMetrics'
    });
    const alarm = new Alarm(this, 'MyAlarm', {
      metric: new Metric({
        namespace: 'LogMetrics',
        metricName: 'MyMetric',
      }),
      threshold: 0,
      evaluationPeriods: 1,
      periodSec: 60,
    });
    alarm.onAlarm({
      alarmActionArn: 'MySNSTopicArn',//ここはちゃんとしたARNを書く
    })
  }
}

 

ここではLogGroupBase.newMetricFilterというメソッドを用いてメトリクスフィルターを作成したが、下記のような方法もある。

const myLogGroup = new LogGroup(this, 'MyLogGroup', {
  logGroupName: 'MyLogGroup',
});
new MetricFilter(this, 'MyMetricFilter', {
  logGroup: myLogGroup,
  filterPattern: {
    logPatternString: 'Error',
  },
  metricName: 'MyMetric',
  metricNamespace: 'LogMetrics'
});

 

いずれの方法でメトリクスフィルターを実装するにせよ、LogGroupをはじめに定義してから出ないと実装することができない、という点が問題となる。

 

[Resource already exists]

上記の通り作成したスタックに対して、ビルドとCloudFormationテンプレートの出力を行う。

 

$ npm run build && cdk synth

> cdk-log-group@0.1.0 build /Users/akito/Develop/sandbox/cdk-log-group
> tsc

Resources:
  MyLogGroup5C0DAD85:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: MyLogGroup
      RetentionInDays: 731
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: CdkLogGroupStack/MyLogGroup/Resource
  MyMetricFilter6B4C0CF6:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: Error
      LogGroupName:
        Ref: MyLogGroup5C0DAD85
      MetricTransformations:
        - MetricName: MyMetric
          MetricNamespace: LogMetrics
          MetricValue: "1"
    Metadata:
      aws:cdk:path: CdkLogGroupStack/MyMetricFilter/Resource
  MyAlarm696658B6:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      Threshold: 0
      AlarmActions:
        - MySNSTopicArn
      MetricName: MyMetric
      Namespace: LogMetrics
      Period: 300
      Statistic: Average
    Metadata:
      aws:cdk:path: CdkLogGroupStack/MyAlarm/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=0.23.0,@aws-cdk/aws-cloudwatch=0.23.0,@aws-cdk/aws-iam=0.23.0,@aws-cdk/aws-logs=0.23.0,@aws-cdk/cdk=0.23.0,@aws-cdk/cx-api=0.23.0,jsii-runtime=node.js/v10.15.0

 

見事なCloudFormationテンプレートが出力された。

 

しかし、これを用いてCloudFormationスタックの作成を行うと「MyLogGroup already exists」とエラーが表示される。

f:id:kidani_a:20190207021055p:plain


すでに同名のロググループが存在するため、新たなロググループを作成することができないようだ。

 

[aws-cdkでCloudFormationテンプレート生成時に独自処理を追加する]

どこかでこのロググループへの依存を断ち切りたい……ということで、ググっていると例によってGithubにたどり着いた。

github.com

 

cdk.Stackクラスの「toCloudFormation()」という関数を実装すれば、独自関数を差し込むことができそうだ。

 

という訳で、次のような実装に修正。

import cdk = require('@aws-cdk/cdk');
import {LogGroup} from '@aws-cdk/aws-logs'
import { Alarm, Metric } from '@aws-cdk/aws-cloudwatch';

export class CdkLogGroupStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const logGroup = new LogGroup(this, 'MyLogGroup', {
      logGroupName: 'MyLogGroup',
    });
    logGroup.newMetricFilter(this, 'MyMetricFilter', {
      filterPattern: {
        logPatternString: 'Error',
      },
      metricName: 'MyMetric',
      metricNamespace: 'LogMetrics'
    });
    const alarm = new Alarm(this, 'MyAlarm', {
      metric: new Metric({
        namespace: 'LogMetrics',
        metricName: 'MyMetric',
      }),
      threshold: 0,
      evaluationPeriods: 1,
      periodSec: 60,
    });
    alarm.onAlarm({
      alarmActionArn: 'MySNSTopicArn',
    })
  }

  public toCloudFormation() {
    const cfn = super.toCloudFormation();
    for (let resource in cfn.Resources) {
      // ロググループは新規Resourceとしてテンプレートに出力しない
      if (cfn.Resources[resource].Type === 'AWS::Logs::LogGroup') {
        delete cfn.Resources[resource];
        continue;
      }
      // メトリクスフィルターが新規Resourceとして定義したロググループに依存しているので
      // ロググループ名を直接参照する
      if (cfn.Resources[resource].Type === 'AWS::Logs::MetricFilter') {
        const ref: string = cfn.Resources[resource].Properties.LogGroupName.Ref;
        if (ref && ref.startsWith('MyLogGroup')) {
          cfn.Resources[resource].Properties.LogGroupName = 'MyLogGroup';
        }
      }
    }
    return cfn;
  }
}

 

この実装でテンプレートを出力すると次のようになる。

$ npm run build && cdk synth

> cdk-log-group@0.1.0 build /Users/akito/Develop/sandbox/cdk-log-group
> tsc

Resources:
  MyMetricFilter6B4C0CF6:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: Error
      LogGroupName: MyLogGroup
      MetricTransformations:
        - MetricName: MyMetric
          MetricNamespace: LogMetrics
          MetricValue: "1"
    Metadata:
      aws:cdk:path: CdkLogGroupStack/MyMetricFilter/Resource
  MyAlarm696658B6:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      Threshold: 0
      AlarmActions:
        - MySNSTopicArn
      MetricName: MyMetric
      Namespace: LogMetrics
      Period: 300
      Statistic: Average
    Metadata:
      aws:cdk:path: CdkLogGroupStack/MyAlarm/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=0.23.0,@aws-cdk/aws-cloudwatch=0.23.0,@aws-cdk/aws-iam=0.23.0,@aws-cdk/aws-logs=0.23.0,@aws-cdk/cdk=0.23.0,@aws-cdk/cx-api=0.23.0,jsii-runtime=node.js/v10.15.0

 

toCloudFormation()を実装したことによる変更点としては、次の2点がある。

  • Resourcesセクションからロググループが削除された
  • メトリクスフィルターのLogGroupNameに組み込み関数Ref経由での別リソースへの依存がなくなっている

 

このテンプレートを利用して(実際には正しいSNS TopicのARNに修正して) 新しいスタックを作成すると、今度は先ほどのエラーなしに無事処理が完了した。

f:id:kidani_a:20190207023440p:plain

 

今回は試していないが、cdk deployコマンドを実行すると、直接AWSにスタックを作成することもできるらしい。いちいちブラウザで管理コンソールを開かなくても良いというのは便利かもしれない。

 

[まとめ]

  • aws-cdkを利用すると、TypeScript + VS Codeで補完が効くのでCloudFormationテンプレート手書きと比べてかなり実装・レビューが速いし楽(当社比)
  • aws-cdkで出力されるCloudFormationテンプレートに手を入れる場合はcdk.Stack#toCloudFormation()を実装すると良い 
  • CloudWatch Logsのロググループは同名のものは作成できない
  • cdk deployコマンド便利そう