
連日の雲の写真である。芸がない。
既存の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コマンド便利そう