mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

AWS監視アラートをCDKで実装する

前回以下の監視アラートに関する記事を書きました。(予想以上に反響がありました。ありがとうございます)

mazyu36.hatenablog.com

監視アラートの基準はシステム特性によるものの、同じメトリクスを使用する場合は閾値や集計期間がシステムによって違うぐらいで仕組みとしては同じため、なるべく使い回しができるようコード化しておきたいところです。

今回は監視アラートをAWS CDKで実装する方法について簡単にご紹介したいと思います。

※CDKユーザーの中にはご存じの方も多いかと思いますが、特にメトリクス監視はAWS公式のBLEAにも実装がされており非常に参考になります。本記事においても数多く引用させていただいております。

github.com

目次

実装したいもの

前回の記事からの抜粋になりますが、今回はCloudWatchを使用した以下2つの仕組みをCDKで実装することを考えます。

  • (1).各サービスから収集したメトリクスを元にアラートを行う。
  • (2).アプリから出力したログを元にアラートを行う。

(1).各サービスから収集したメトリクスを元にアラートを行う

こちらですが、AWS公式のBLEAが非常に参考になります(そのまま流用できるレベルで実装がされています)。

<参考> https://pages.awscloud.com/rs/112-TZM-766/images/AWS-22_Governance_on_AWS_with_BLEA_templates_KMD40.pdf

以下のサンプル構成においてアラート(メール通知)の実装例が示されています。

  • ECSによるコンテナアプリのサンプルアプリ github.com

  • API Gateway-LambdaによるAPIのサンプルアプリ github.com

  • CloudTrailなどセキュリティサービスを中心にしたガバナンスベース(リンクはstandalone版) github.com

アラートを出すための実装の流れとしては以下になります。

  • ①通知先のSNSトピックを定義する
  • ②メトリクスに対してアラームを作成し、SNSトピックを通知先に指定する

BLEAの実装例を元に詳細を順に見ていきます。

①通知先のSNSトピックを定義する

これはBLEAの実装そのままで基本問題ないと思います。以下の3段階になります。

  • SNSトピックを作成
  • サブスクリプションを作成し、トピックと通知先のEメールアドレスと紐付ける
  • CloudWatchからSNSトピックに対してパブリッシュを許可する

これはどのサンプル構成でも同じ流れで実装されています。

// SNSトピックを作成
const topic = new sns.Topic(this, 'MonitorAlarmTopic');

// サブスクリプションを作成し、トピックと通知先のメールアドレス(エンドポイント)を紐付け
new sns.Subscription(this, 'MonitorAlarmEmail', {
  endpoint: props.notifyEmail,
  protocol: sns.SubscriptionProtocol.EMAIL,
  topic: topic,
});
this.alarmTopic = topic;

// CloudWatchからSNSトピックに対してパブリッシュを許可
topic.addToResourcePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    principals: [new iam.ServicePrincipal('cloudwatch.amazonaws.com')],
    actions: ['sns:Publish'],
    resources: [topic.topicArn],
  }),
);

※以下より引用。コメントのみ追記・変更しています。

github.com

②メトリクスに対してアラームを作成し、SNSトピックを通知先に指定する

やたらタイトルが長いように見えますが、L2 Constructを使用するとタイトルのことが一行でできることが多いです。

具体的には以下の3段階を1行で実装できます。

  • L2 Constructのメソッドを使用しMetricを生成。例えば以下はECS FargateのCPUUtilizationのMetricを返却するメソッドはこちら
  • MetriccreateAlarmを使用し、Alarmを設定。引数でアラート条件を指定する。
  • AlarmaddAlarmActionを使用し、アラート発生時に①で生成したSNSトピックに対して通知するよう設定。以下はaddAlarmActionのリンク。

以下はBLEAにおける実装例です。一行で書けますが可読性のため改行が入っています。

ecsService
   // L2 ConstructのmetricCpuUtilization を使用し、Metricを取得
  .metricCpuUtilization({
    period: cdk.Duration.minutes(1),
    statistic: cw.Statistic.AVERAGE,
  }) 
   // MetricのcreateAlarmを使用しAlarmを作成
  .createAlarm(this, 'FargateCpuUtil', {
    evaluationPeriods: 3,
    datapointsToAlarm: 3,
    threshold: 95,
    comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
    actionsEnabled: true,
  }) 
   // AlarmのaddAlarmActionを使用して、アラート発生時にSNSトピックに通知するよう指定
  .addAlarmAction(new cw_actions.SnsAction(alarmTopic)); 

※以下より引用。コメントのみ追記・変更しています。

github.com

L2 Constructが実装されていれば、メジャーどころのメトリクスに対応するMetricを取得するメソッドが大体ある印象です。

一方で以下のようなケースでは上記のやり方は使えません。

  • L2 Constructが存在しない。例えばElastiCacheは2023/2現在、L2 Constructが存在しません。
  • L2 Constructは存在するが、アラート対象としたいメトリクスのMetricを生成するメソッドがない。
  • 手動設定したサービスに対するアラートを設定したい(SNSのSMS料金やSESのバウンス率などのは大体このパターンになることが多いと思います

上記のような場合は名前空間とメトリクス名を元に、直接newしてMetricを生成すれば同様のことができます。

BLEAだと例えばLambdaのConcurrentExecutionsの監視の実装に使われています。Lambda FunctionはL2 Constructが存在しているもの、ConcurrentExecutionsMetricを返すメソッドは実装されていません。

// Metricを直接生成
new cw.Metric({
  namespace: 'AWS/Lambda', // 名前空間を指定
  metricName: 'ConcurrentExecutions',  // メトリクス名を指定
  period: cdk.Duration.minutes(5),
  statistic: cw.Statistic.MAXIMUM,
  dimensionsMap: {
    FunctionName: getItemFunction.functionName,
  },
}) // これ以降は同じ。Alarmを作成して、AlarmActionとしてSNSトピックを指定する
  .createAlarm(this, 'getItemConcurrentExecutionsAlarm', {
    evaluationPeriods: 3,
    threshold: 80,
    datapointsToAlarm: 3,
    comparisonOperator: cw.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
    actionsEnabled: true,
  })
  .addAlarmAction(new cw_actions.SnsAction(props.alarmTopic));

※以下より引用。コメントのみ追記・変更しています。

github.com

なお、名前空間やメトリクス名は実際のものと完全一致している必要があります。マネジメントコンソールやAWS CLIで確認可能です。

docs.aws.amazon.com

メトリクスではなくEventを元にアラートを行いたい場合

この場合は対象のイベントパターンを検知し、ターゲットにSNSトピックを指定することで実現することが可能です。セキュリティ系のサービスではよく用いることになるかと思います。

例えば以下はConfigで非準拠となったルールを検知した場合にアラートを行う、BLEAの実装例です。

new cwe.Rule(this, 'BLEARuleConfigRules', {
  description: 'CloudWatch Event Rule to send notification on Config Rule compliance changes.',
  enabled: true,
  // 検知対象のイベントパターンを定義
  eventPattern: {
    source: ['aws.config'],
    detailType: ['Config Rules Compliance Change'],
    detail: {
      // 検知対象のConfigルールを指定
      configRuleName: ['bb-default-security-group-closed'],
      newEvaluationResult: {
        // 非準拠の場合
        complianceType: ['NON_COMPLIANT'],
      },
    },
  },
  // 通知先のSNSトピックを指定
  targets: [new cwet.SnsTopic(secTopic)],
});

※以下より引用。コメントのみ追記・変更しています。

github.com

なお上記のblea-security-alarm-stack.tsは他にもCloudTrailやSecurity Hub, GuardDutyなどのアラートも実装されており、ほぼそのまま使えるようになっており非常に参考になります。

(2).アプリから出力したログを元にアラートを行う。

以下の図の下側のルートになります。CloudWatch Logsに出力したログを元に、サブスクリプションフィルターを使用し通知を行います。

なおSubscriptionFIlterで宛先をLambdaとしているのは、メッセージの加工を行うためです。以下が参考になります。

dev.classmethod.jp

ここではサブスクリプションフィルターを実装してみます。CloudWatch LogsのロググループのL2 Constructにおける addSubscriptionFilter を使用することで実装可能です。

declare const logGroup: logs.LogGroup // 対象のロググループ

const subscriptionFilter = logGroup.addSubscriptionFilter('SubScriptionFilter', {
  destination: new destinations.LambdaDestination(lambdaFunction),  // Lambdaを指定
  filterPattern: logs.FilterPattern.anyTerm('ERROR', 'Error', 'error')  // パターンとしていずれかに一致(OR条件)で指定。
});

// Escape Hatches を使用して依存関係を定義
(subscriptionFilter.node.defaultChild as logs.CfnSubscriptionFilter).addDependsOn(subscriptionFilter.node.findChild('CanInvokeLambda') as lambda.CfnPermission);

filterPatternについてはメソッドがいくつかあるため、要件に合ったものを使用すれば良いです。ここではERROR, Error, errorのいずれか一つにマッチした場合を指定しています。

またEscape Hatchesで CloudWatch LogsからLambdaをトリガーするパーミッションサブスクリプションフィルターの順でリソース作成が行われるよう、依存関係を定義しています。

これは2023/2現在だと、パーミッション作成とサブスクリプションフィルター作成の間に依存関係がなく並列で作成しようとし、エラーになる可能性があるためです。以下のIssueが起票されています。

github.com

なお、私はEscape Hatchesの存在を知らない頃にこの問題にぶち当たり、その時はL1 Constructを使用しました。 今やるのであればL2 Construct+Escape Hatchesでやるのが良いと思いますが、参考までに以下に記載しておきます。

内容としては上記のL2 Constructで実装したものと全く同じです。

// ロググループにからLambda関数を呼び出すパーミッションを作成
const lambdaPermission = new lambda.CfnPermission(this, 'LambdaPermission', {
  functionName: lambdaFunction.functionName,
  action: "lambda:InvokeFunction",
  principal: "logs.amazonaws.com",
  sourceArn: logGroup.logGroupArn
});

// サブスクリプションフィルターを作成
const subscriptionFilter = new logs.CfnSubscriptionFilter(this, "SubScriptionFilter", {
  logGroupName: logGroup.logGroupName,
  destinationArn: lambdaFunction.functionArn,
  filterPattern: "?ERROR ?Error ?error"
});

// サブスクリプションフィルターとパーミッションの依存関係を定義
subscriptionFilter.addDependsOn(lambdaPermission);

おまけ:ダッシュボードについて

BLEAにはCloudWatch Dashboardの実装例もあります。これもそのまま流用できる部分も多く、また実装方法の確認にも非常に便利です。

github.com

以下はBLEAのものをそのまま使用して、AuroraのWriterとReaderのLatencyをダッシュボードに表示してみた例です。

流用してカスタマイズするだけでいい感じのダッシュボードが作れるのと、書き方も学べるので非常におすすめです。