LoginSignup
6
1

More than 1 year has passed since last update.

【TypeScript】AWS CDKv2でCloudWatchのログアラームを設定する

Last updated at Posted at 2021-12-20

2021/12/2にAWSCDKv2の安定版がリリースされました :tada:

これを期に、弊社でもちょうどログのアラーム定義をIaCで管理しようとしていたタイミングだったので、CDKv2で管理していくことにしました。
日本語の記事がまだ少なかったので、備忘も含めて記事にします。

tl;dr

  • AWS CDKv2がstableになったので、CloudWatchの設定をやってみる(TypeScript)
  • yamlファイルで定義した変数をCDKに組み込んで例えばAnsibleのリポジトリと共用出来ると便利
  • デプロイもコマンド一発で、差分表示なんかもできて便利

環境

  • OS
    • macOS BigSur (Intel Core i9)
  • node: 16.10.0
  • npm: 7.24.0
  • npx: 7.24.0

CDKの始め方

基本的には以下の公式サイトに沿って進めていきます。

  • aws-cdkをグローバルにインストールします
npm install -g aws-cdk
  • その後、空のディレクトリ上で以下のコマンドを実行します。
cdk init app --language typescript

初期ディレクトリが構築されます。
以下のようなディレクトリ構成で作成されます。

cdk
├── README.md
├── bin
│   └── cdk.ts # cdk実行のエントリーポイント
├── cdk.json
├── cdk.out # CloudFormationテンプレートが出力されるディレクトリ
├── jest.config.js
├── lib # このフォルダの下に定義を設定していく
│   ...
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── cdk.test.ts
└── tsconfig.json

加えて今回はyamlファイルを扱いたいので、以下のコマンドでインストールしておきます。

npm install --save js-yaml
npm install --save-dev @types/js-yaml

またAWSCLIが実行できるようにクレデンシャルやプロファイルの設定が済んでいる必要があります。

上記が済んだら、一度ビルドしてbootstrapまで試してみることをおすすめします。

  • ビルド
npm run build
  • bootstrap実行
npm run cdk bootstrap

ブートストラップとはデプロイに必要なIAMロールのプロビジョニングなどを自動で行ってくれるコマンドです。プロジェクトを作成したら一度行っておくと良いです。

ブートストラップ-AWSクラウド開発キット(CDK)v2

アラームを定義する

TypeScriptで開発する際は、ホットリロードを有効にしておくとスムーズに開発出来ると思います。
ターミナルを別で開いて以下のコマンドを実行して置いておきます。

npm run watch

ここから本題ですが、そのままの状態だとlibの配下のtsファイルに書き込んでいくようになってしまうので、私は以下のようにフォルダ分けして管理しやすくしました。もっと良い分け方が有りましたらコメントなどで教えていただければ幸いです :bow:

cdk
...
├── lib
│   ├── conf
│   │   └── conf.ts # 共通の定数と実行するターゲットを指定
│   ├── index.ts # メイン処理
│   ├── stacks # 各々の処理を追加していくところ
│   │   ├── coudwatch-alarm-logmetrics.ts
│   │   └── index.ts
│   └── util
│       └── load-config.ts # yamlファイルから変数を読み込む関数
...

conf.ts

グローバルに定義しておきたい定数をこちらに示しておくようにします。

lib/conf/conf.ts
export const constVariable = {
    topicArn: '<設定したいARN>',
    account: '<アカウントID>',
    region: '<リージョン>'
}
export const targets = [
    <適応対象のサーバーのホスト名を並べておく>
];

accountとregionは後述するStackを継承したクラスのpropsとして渡すので、こちらで外だししておきます。
弊社ではSNSトピックは共通のものを使って単一のアクションを通知先へアラームを発報させていますので、topicArnとしてここで定義しておきます

load-config.ts

今回Ansibleで利用しているログの定義やEC2インスタンスのinstance_idなどをyamlファイルと共通化して管理したかったので、こちらでyamlファイルから変数を読む処理を書いています。

lib/util/load-config.ts
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';

// yamlに書いてある変数のうち、実際に使いたいものをtypeで定義しておく
type YamlProps = {
    local_hostname: string,
    cwagent_env_prefix: string,
    cwagent_logs: string[],
    cwagent_alarms: string[],
    instance_id: string
};

/**
 * 指定したパスにあるyamlファイルから変数を取り出すメソッド
 * 
 * @param filename - 相対パスでの指定が可能
 * @returns YamlProps
 */
export const loadConfig = (filename: string) => {
    const yamlPath = path.resolve(filename);
    const yamlProps = yaml.load(fs.readFileSync(yamlPath, 'utf-8')) as YamlProps;
    return yamlProps;
}

で、yamlファイルは以下のように定義しておきます。
load-config.tsYamlPropsで定義している変数名と型を一致させておく必要があります。

#
# cdk variables
#
cwagent_env_prefix: Development
cwagent_logs:
  - /var/log/app/access.log
  - /var/log/app/error.log
cwagent_alarms:
  - /var/log/app/error.log
instance_id: YOUR_INSTANCE_ID

index.ts

実際のCDKでの定義を後から追加しやすいように、bin/cdk.tsから更にlib/index.tsを実行するようにして、こちらでネストされたスタックを構築するような処理を記載するようにしました。要するにメインの処理です。

lib/index.ts
#!/usr/bin/env node
import 'source-map-support/register';
import { CloudWatchAlarmCWAgentProcstat, CloudWatchAlarmCWAgentProcstatProps } from '../lib/stacks';
import { CloudWatchAlarmLogMetrics, CloudWatchAlarmLogMetricsProps } from '../lib/stacks';
import { constVariable, targets } from '../lib/conf/conf';
import { loadConfig } from '../lib/util/load-config';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

        // logのERROR検知用アラーム定義
        // config.tsで定義したtargetsでループを回す
        targets.forEach(target => {
            const hostname = target;
            // load-config.tsのloadConfigでansibleフォルダのhost_varsフォルダからyamlファイルを読み込んで、`conf`に格納。
            const conf = loadConfig(`../host_vars/${hostname}`);

            conf.cwagent_alarms.forEach(cwagent_alarm => {
                const logMetricsProps: CloudWatchAlarmLogMetricsProps = {
                    hostname: hostname,
                    local_hostname: conf.local_hostname,
                    cwagent_env_prefix: conf.cwagent_env_prefix,
                    log_group_name: cwagent_alarm,
                    topicArn: constVariable.topicArn,
                }
                new CloudWatchAlarmLogMetrics(this, `CloudWatchAlarmLogMetrics-${logMetricsProps.hostname}-${cwagent_alarm}`, logMetricsProps);
            })
        });
    }
}

この親スタックからnew CloudWatchAlarmLogMetrics()でネステッドスタックを必要な分だけインスタンス化していきます。
親となるスタックはStackを継承させ、その下にネストさせるスタックはNestedStackを継承して作成します。
どちらもインスタンス化するときの第二引数がスタック名になるので、わかりやすい名前をつけておきます。
ループさせるなら複数のスタックが作成されても名前が一意になるように変数を仕込んでおくと良いと思います。

stacksの中

実際にCDKで作成したいリソースを定義していく部分で、こちらは今後目的ごとにファイルを増やしていこうと思っていますので、index.tsでまとめてexportしておきます。

lib/staks/index.ts
export { CloudWatchAlarmLogMetrics, CloudWatchAlarmLogMetricsProps } from "./coudwatch-alarm-logmetrics";

具体的な定義はそれぞれファイルを分けています。
今回の例ではCWAgentで飛ばして生成されたロググループにメトリクスフィルターを設定して、ERRORという文字列をカウントするようにしています。
そのメトリクスをアラームで検出し、SNSで発報しているという流れです。
AWSCDKv2の大きな変更点としては、v1までは例えばCloudWatchとLogsを使いたい場合は、別々のパッケージを都度インストールし、importする必要がありましたが、v2からはaws-cdk-libに統合された形になっています。

lib/stacks/coudwatch-alarm-logmetrics.ts
import { NestedStack, NestedStackProps, aws_cloudwatch, Duration, aws_cloudwatch_actions, aws_sns, aws_logs } from 'aws-cdk-lib';
import { Statistic, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch';
import { MetricFilterOptions } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';

export type CloudWatchAlarmLogMetricsProps = {
  hostname: string,
  local_hostname: string,
  cwagent_env_prefix: string,
  log_group_name: string,
  topicArn: string,
}

/**
 * 指定されたロググループにERRORの文字列を検知するメトリックフィルターを追加
 * それをCloudWatch Alarmに登録し、検知したら指定したSNS Topicに基づいてアクションを行う
 * 
 * @param scope  Construct
 * @param id スタックの名前になる
 * @param customProps CloudWatchAlarmLogMetricsProps
 * @param props - NestedStackProps?
 * 
 */
export class CloudWatchAlarmLogMetrics extends NestedStack {
  constructor(scope: Construct, id: string, customProps: CloudWatchAlarmLogMetricsProps, props?: NestedStackProps) {
    super(scope, id, props);

    const metricNameSpace = 'LogMetrics';
    const logGroupName = `${customProps.local_hostname}${customProps.log_group_name}`;
    const metricName = `[ERROR]${logGroupName}`;
    const alarmName = `[${customProps.cwagent_env_prefix}] CloudWatchLog:${customProps.hostname}:${customProps.log_group_name}:error_detect (>=1count)`;

    // メトリックフィルターの定義
    // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.MetricFilter.html
    const metricFilterProps: MetricFilterOptions = {
      metricNamespace: metricNameSpace,
      metricName: metricName,
      filterPattern: aws_logs.FilterPattern.literal('ERROR'),
      metricValue: '1'
    }

    // LogGroupの取得
    const logGroup = aws_logs.LogGroup.fromLogGroupName(this, 'LogGroup', logGroupName);

    // LogGroupにメトリクスフィルターを追加
    logGroup.addMetricFilter('MetricFilter', metricFilterProps);

    // メトリックの定義
    // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Metric.html
    const metric = new aws_cloudwatch.Metric({
      namespace: metricNameSpace,
      metricName: metricName,
      period: Duration.seconds(60),
      statistic: Statistic.SUM,
    });

    // アラームの定義
    // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html
    const alarm = new aws_cloudwatch.Alarm(this, 'Alarm', {
      alarmName: alarmName,
      alarmDescription: alarmName,
      comparisonOperator: aws_cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
      metric: metric,
      threshold: 1,
      evaluationPeriods: 1,
      datapointsToAlarm: 1,
      actionsEnabled: true,
      treatMissingData: TreatMissingData.NOT_BREACHING
    });

    // SNS Topicの取得
    const topic = aws_sns.Topic.fromTopicArn(this, 'Topic', customProps.topicArn);

    // AlarmにSNS Topicを追加
    alarm.addAlarmAction(new aws_cloudwatch_actions.SnsAction(topic));
  }
}

CDKv2がstableになってから少し書き方が変わっていて日本語の記事がないので、基本的には公式のAPIリファレンスを読みながら詳細を見ていったほうが良いです。
とはいえVSCodeで書いていて、補完も効くのである程度なんとなく書けます。

デプロイ

デプロイはすごく簡単で、以下のコマンドを実行するだけです。

npm run cdk deploy

デプロイが走り、うまく行けばCloudFormationのスタックに流れてきます。
cloudformation_stack.png

エラーになった際も、CloudFormationからたどると原因を特定しやすいです。

その他にもnpm run cdk synthCloudFormationのテンプレートが出力できたり、npm run cdk diffでデプロイ済みのスタックと差分表示ができたりします。

まとめ

プレビュー版の解説記事はあるものの、そこから破壊的変更がなされている箇所も多く、結局英語のAPIリファレンスとにらめっこしながら実装せざるを得ませんでした:sweat_smile:
SAMやServerless Frameworkのような宣言的な記載ではなく、アプリを作るときのように書けるのでナチュラルにインフラ部分も構築しやすいなと思いました。
サーバーレスで組む場合もかなり使い勝手が良いなと思うので、他のリソースに関しても今後試していけたらなと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1