👌

ASGのインスタンス起動・停止時にSlackに通知するCloudFormationテンプレート

2023/01/20に公開

AWS Auto Scalingを利用することで、Amazon EC2 インスタンスを自動的に起動および停止させることでオートスケーリングやオートヒーリングを実現できます。

EC2インスタンスの起動および終了をSlackに通知することで、インスタンスの入れ替えを知ったり、入れ替えが頻発するなど障害発生の検知に役立ちます。

https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/ec2-auto-scaling-sns-notifications.html

この記事では、SNSとLambdaの連携、および Lambda から Slack Incoming Webhook を叩く操作を、CloudFormationテンプレートでどのように表現するのかを紹介します。

通知の仕組み

  1. ASGのアクティビティ通知先として、Amazon SNS トピックを指定
  2. Amazon SNS トピックをLambda関数で購読
  3. Lambda関数でSlack Webhook URLにPOST

SNSトピックとLambda関数を作成するCloudFormationテンプレート

次のコードは、ASGのアクティビティ通知先として指定できるSNSトピックと、トピックを購読し、イベントの発生をトリガーに起動するLambdaを作成するテンプレートです。
テンプレートパラメータとして Slack Incoming Webhook のURLを与えます。

Parameters:
  WebhookUrl:
    Type: String

Resources:
  TopicEC2ASGNotification:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: asg-notification
  SNSSubscriptionEC2ASGNotification:
    Type: AWS::SNS::Subscription    
    Properties:
      Endpoint: !GetAtt LambdaEC2ASGNotification.Arn
      Protocol: lambda
      TopicArn: !Ref TopicEC2ASGNotification
  LambdaEC2ASGNotification:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |+
          const url = require('url');
          const https = require('https');
          const util = require('util');
        
          const { WebhookUrl, Region } = process.env;

          const debug = (key, object) => { console.log(`DEBUG: ${key}\n`, JSON.stringify(object)); }

          const notifySlack = async (subject, message) => {
            const detail = `AccountId: ${message.AccountId}\nDescription: ${message.Description}\nCause: ${message.Cause}`;
            const link = `https://${Region}.console.aws.amazon.com/ec2/home?region=${Region}#AutoScalingGroupDetails:id=${message.AutoScalingGroupName};view=activity`;
            const blocks = {
              blocks: [
                {
                  type: "section",
                  text: {
                    type: "mrkdwn",
                    text: subject + "\n```" + detail + "```\n<" + link + "|Link>"
                  }
                }
              ]
            };

            const postData = JSON.stringify(blocks);
            const url = new URL(WebhookUrl);
            const requestOptions = {
              hostname: url.host,
              port: 443,
              path: url.pathname,
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData)
              }
            };
            return new Promise((resolve, reject) => {
              const req = https.request(requestOptions, (res) => {
                const data = [];
                res.on("data", (d) => data.push(d));
                res.on("end", () => resolve(data.join("")));
              });
              req.on("error", reject);
              req.write(postData);
              req.end();
            });
          };

          exports.handler = async (event, context, callback) => {
            console.log("INFO: request Recieved.\nEvent:\n", JSON.stringify(event));
            context.callbackWaitsForEmptyEventLoop = false;
            const snsData = event.Records[0].Sns;
            const message = JSON.parse(snsData.Message);
            const results = await notifySlack(snsData.Subject, message);
            debug("results", results);
            return results;
          };
      Environment:
        Variables:
          WebhookUrl: !Ref WebhookUrl
          Region: !Ref AWS::Region
      Handler: index.handler
      Role: !GetAtt LambdaEC2ASGNotificationRole.Arn
      Runtime: "nodejs16.x"
      MemorySize: 128
      Timeout: 30
  LambdaEC2ASGNotificationRole:
    Type: "AWS::IAM::Role"
    DeletionPolicy: Retain
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  LambdaPermissionEC2ASGNotification:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaEC2ASGNotification.Arn
      Principal: sns.amazonaws.com
      SourceArn: !Ref TopicEC2ASGNotification

Lambda実行テスト

実行テスト結果

作成されたLambda関数のテストに次のテストイベントJSONを渡すと、通知を確認できます。

{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:012345678901:TEST-ASG-TopicEC2ASGNotification-Gx4PHPpQDYxR:800000000-0000-0000-0000-000000000000",
      "Sns": {
        "Type": "Notification",
        "MessageId": "ece92d9b-504b-56fc-8d23-71dbd88bf54a",
        "TopicArn": "arn:aws:sns:ap-northeast-1:012345678901:TEST-ASG-TopicEC2ASGNotification-Gx4PHPpQDYxR",
        "Subject": "Auto Scaling: termination for group \"EC2AutoScalingGroup\"",
        "Message": "{\"Origin\":\"AutoScalingGroup\",\"Destination\":\"EC2\",\"Progress\":60,\"AccountId\":\"012345678901\",\"Description\":\"Terminating EC2 instance: i-0000000000000000\",\"RequestId\":\"d413bc11-0548-4ec3-810f-1863ff8bbe0f\",\"EndTime\":\"2023-01-17T11:29:04.571Z\",\"AutoScalingGroupARN\":\"arn:aws:autoscaling:ap-northeast-1:012345678901:autoScalingGroup:a563dcba-2eb7-4d10-8f8d-c2e79a6ff460:autoScalingGroupName/EC2AutoScalingGroup\",\"ActivityId\":\"d413bc11-0548-4ec3-810f-1863ff8bbe0f\",\"StartTime\":\"2023-01-17T11:22:37.321Z\",\"Service\":\"AWS Auto Scaling\",\"Time\":\"2023-01-17T11:29:04.571Z\",\"EC2InstanceId\":\"i-0000000000000000\",\"StatusCode\":\"MidTerminatingLifecycleAction\",\"StatusMessage\":\"\",\"Details\":{\"Subnet ID\":\"subnet-0a7508392f1c3bbba\",\"Availability Zone\":\"ap-northeast-1a\"},\"AutoScalingGroupName\":\"EC2AutoScalingGroup\",\"Cause\":\"At 2023-01-17T11:22:37Z an instance was taken out of service in response to an instance refresh.  At 2023-01-17T11:22:37Z instance i-0000000000000000 was selected for termination.\",\"Event\":\"autoscaling:EC2_INSTANCE_TERMINATE\"}",
        "Timestamp": "2023-01-17T11:29:04.630Z",
        "SignatureVersion": "1",
        "Signature": "XiTi7Zs6lRwOuN05fGROkZK8Wy9StvI20U79tzCgHk0LCiPPSh0izW+8qLucnCp2HMO44Qq+ju6AyCExer+HpVpjDZIqh3DX28/Cv0KgkVCzR5ZGKWW5W7P1CIgGya7rl2CGnL8RqEVpcmOyjKUsQdDfdSYNsq/pTd2U+uQt+0DO27n1A/nU4hCF+nZV8g7+2KseDOD8KZldErh/lSx6Y5u5d4OUyuDsUaPwugqvSn8r7f1242Iwky561W1xh07w28Y/+RQPfcDlFSXHTFgQE/WXcpINJGMG7trZj+cGYXHy5JIHGE5SGFp/SoMJt2mIYqkCuaIObbBwFgHGUvBiqQ==",
        "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-000000000000000000000000000000.pem",
        "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:012345678901:TEST-ASG-TopicEC2ASGNotification-Gx4PHPpQDYxR:800000000-0000-0000-0000-000000000000",
        "MessageAttributes": {}
      }
    }
  ]
}

EC2 ASGを作成して通知先としてトピックを設定するCloudFormationテンプレート

ASGのアクティビティ通知先は NotificationConfigurations で次を指定します。

  • 通知するイベントの種類
  • 通知先のトピックARN

ASGを作成するテンプレートは案件により様々ですし長くなるので、通知の設定箇所のみ紹介します。

  EC2AutoScalingGroup:
    Type: 'AWS::AutoScaling::AutoScalingGroup'
    Properties:
      # (省略)
      NotificationConfigurations:
        - NotificationTypes:
            - autoscaling:EC2_INSTANCE_LAUNCH
            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
            - autoscaling:EC2_INSTANCE_TERMINATE
            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
          TopicARN: !Ref TopicEC2ASGNotification

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-notificationconfigurations.html

マネジメントコンソール上で設定する方法

CloudFormationを使用せずASGを構成している場合、公式ドキュメントの通知を送信するように Auto Scaling グループを設定するの手順に従って設定できます。

タケユー・ウェブ株式会社

Discussion