連日の雲の写真である。芸がない。
既存のCloudWatch Logsロググループに対して、メトリクスフィルターとCloudWatchアラームを作成する、という簡単なタスクがあった。
aws-cdkでCloudFormationテンプレートを出力し、スタックの作成を行った際に問題が起きたので、解決方法をメモしておく。
- [aws-cdkでメトリクスフィルターを実装する]
- [Resource already exists]
- [aws-cdkでCloudFormationテンプレート生成時に独自処理を追加する]
- [まとめ]
[aws-cdkでメトリクスフィルターを実装する]
CloudWatch Logsの画面をみると、すでにMyLogGroupというロググループが存在している。
このロググループに対してメトリクスフィルターとアラームを設定したいので、aws-cdkを用いてCloudFormationスタックを実装していく。aws-cdkは昨日に続き0.23.0を利用している。
まずはプロジェクトの作成から。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
このようなディレクトリ構成になっているので、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」とエラーが表示される。
すでに同名のロググループが存在するため、新たなロググループを作成することができないようだ。
[aws-cdkでCloudFormationテンプレート生成時に独自処理を追加する]
どこかでこのロググループへの依存を断ち切りたい……ということで、ググっていると例によってGithubにたどり着いた。
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に修正して) 新しいスタックを作成すると、今度は先ほどのエラーなしに無事処理が完了した。
今回は試していないが、cdk deployコマンドを実行すると、直接AWSにスタックを作成することもできるらしい。いちいちブラウザで管理コンソールを開かなくても良いというのは便利かもしれない。
[まとめ]
- aws-cdkを利用すると、TypeScript + VS Codeで補完が効くのでCloudFormationテンプレート手書きと比べてかなり実装・レビューが速いし楽(当社比)
- aws-cdkで出力されるCloudFormationテンプレートに手を入れる場合はcdk.Stack#toCloudFormation()を実装すると良い
- CloudWatch Logsのロググループは同名のものは作成できない
- cdk deployコマンド便利そう