Amazon CloudWatch Alarmから直接 AWS Lambda にてカスタムアクションを実行する [Amazon CloudWatch + AWS Lambda + AWS CloudFormation]

こんにちは。SCSKのふくちーぬです。

Amazon CloudWatch AlarmのターゲットにAmazon Simple Notification Service (Amazon SNS) だけではなく、AWS Lambdaを指定できるようになったことを知っていますでしょうか。

今回は、Amazon CloudWatch Alarm のターゲットに AWS Lambda をターゲットとしたメトリクス監視をご紹介します。またリソースの構築には、AWS CloudFormation を利用します。

CloudWatch AlarmのターゲットにLambdaがサポートされた件

以前までは、CloudWatch Alarmはアラームアクションとして、SNSやAutoacalingの増減やEC2インスタンスの起動/停止、Systems Manager Incident Manager でインシデントの起票が可能でした。

カスタムアクションを実施する際には、一度SNSトピックに送信した後にLambdaでサブスクライブする必要がありました。

2023/12/22に発表されたアップデートにより、直接Lambdaをターゲットとすることが可能になりました。

使いどころ

今回のアップデートによりSNSを経由することなく、カスタムアクションを容易に実施することができます。SNSを挟む必要がなくなったため、その分のリソースを構築する必要がなくなったことは大きなメリットとなります。CloudWatch Alarmのアラート内容をLambdaにて加工し、その後にSNSへ送信する処理をLambda内で完結して実行できます。

SNSを挟むパターンとしては、他の複数のサービス(Lambda・SQS・E-Mail等)へ送信する要件があったり、リトライ処理を入れ込むこと等が対象となってくると思います。

検証

実際に、CloudWatch AlarmのターゲットにLambdaを指定してアラート内容の出力結果を確認します。

アーキテクチャー

  • LambdaのConcurrenExecutions(同時実行数)メトリクスの監視をする
  • アラーム状態の場合、Lambdaをターゲットとして起動する
  • Lambdaの処理では、アラート内容を専用のロググループに出力する

CloudFormationテンプレート

下記のリソースを作成するCloudFormationテンプレートです。

  • CloudWatch アラーム
  • Lambda
  • Lambda リソースポリシー
  • ロググループ
  • IAMロール

以下のテンプレートをデプロイしてください。

AWSTemplateFormatVersion: 2010-09-09
Description: To trigger Lambda with a CloudWatch Alarm
Parameters:
  ResourceName:
    Type: String    
Resources:  
  # ------------------------------------------------------------#
  #  CloudWatch Alarm
  # ------------------------------------------------------------#
  CloudWatchAlarm:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      Namespace : AWS/Lambda
      AlarmName : !Sub ${ResourceName}-Lambda-ConcurrentExecutions-Alarm
      AlarmActions :
        - !GetAtt Function.Arn
      MetricName : ConcurrentExecutions
      ComparisonOperator : GreaterThanThreshold # 以上を表す
      EvaluationPeriods : 5
      DatapointsToAlarm: 1
      Period : 60
      Statistic : Maximum
      Threshold : 100
  # ------------------------------------------------------------#
  #  Lambda
  # ------------------------------------------------------------#
  Function:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${ResourceName}-lambda-function
      Role: !GetAtt LambdaRole.Arn
      Runtime: python3.12
      Handler: index.lambda_handler
      Environment:
        Variables:
          LOGGROUPNAME: Alarm-Lambda-ConcurrentExecutions
      Code:
        ZipFile: !Sub |
          import boto3
          import os
          import time
          import datetime
          import json

          s3_client = boto3.client('s3')
          cwlogs_client = boto3.client('logs')

          def lambda_handler(event, context):
              message = event['alarmData']

              LOG_GROUP_NAME = os.environ['LOGGROUPNAME']
              localtime = datetime.datetime.now() + datetime.timedelta(hours=9)
              LOG_STREAM_NAME = localtime.strftime("%Y%m%d%H%M")
              try:
                  cwlogs_client.create_log_stream(
                      logGroupName = LOG_GROUP_NAME,
                      logStreamName = LOG_STREAM_NAME,
                  )
                  response = cwlogs_client.put_log_events(
                      logGroupName = LOG_GROUP_NAME,
                      logStreamName = LOG_STREAM_NAME,
                      logEvents = [
                          {
                          'timestamp': int(round(time.time() * 1000 )),
                          'message' : json.dumps(message)
                          }
                      ]  
                  )
              except cwlogs_client.exceptions.ResourceAlreadyExistsException:
                  pass
              except Exception as e:
                  print("error",e)
                  raise
              return response                      
  LambdaInvokePermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      FunctionName: !GetAtt Function.Arn
      Action: 'lambda:InvokeFunction'
      Principal: lambda.alarms.cloudwatch.amazonaws.com
      SourceAccount: !Ref 'AWS::AccountId'
      SourceArn: !GetAtt CloudWatchAlarm.Arn
  # ------------------------------------------------------------#
  #  Log Group
  # ------------------------------------------------------------#      
  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${Function}"  
  CloudWatchAlarmLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: Alarm-Lambda-ConcurrentExecutions
  # ------------------------------------------------------------#
  #  IAM Role
  # ------------------------------------------------------------#
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ResourceName}-lambda-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 

アラームの発砲テスト

任意の端末(Cloud9やCloudShell等)から、以下のコマンドを実行してアラートを発砲させます。<ResourceName>には、スタックデプロイ時に入力したパラメータを挿入します。

 aws cloudwatch set-alarm-state --alarm-name <ResourceName>-Lambda-ConcurrentExecutions-Alarm --state-value ALARM --state-reason "alarm test" 

アラート内容が正常に出力されていることが確認できました。

容易にCloudWatch AlarmとLambdaを連携することができましたね。

最後に

いかがだったでしょうか。CloudWatch AlarmのターゲットにLambdaを指定して、カスタムアクションを実行しました。

既存の構成でSNSを利用している場合は、SNSを省くことができるかどうか検討をすることをお勧めします。

本記事が皆様のお役にたてば幸いです。

ではサウナラ~🔥

タイトルとURLをコピーしました