通过 CloudWatch Events 检测 EC2 实例是否是 CloudFormation 的一部分

Detecting if EC2 instances are part of a CloudFormation via CloudWatch Events

当 CloudWatch Events 收到有关创建新 EC2 实例的通知时,检测该实例是否属于 CloudFormation 堆栈的最简单方法是什么?

我的第一个方法是在实例上调用 DescribeInstances 来查找 aws:cloudformation:stack-id 标签,但显然在添加该标签之前存在延迟,所以这变得很混乱。

我应该直接查询 CloudFormation API 吗?当我看到有关新 EC2 实例的 CloudWatch 事件时,如果我调用 CloudFormation API,是否有此 EC2 实例将显示为成员的排序保证?

要确定 CloudWatch Event 中返回的 EC2 实例 ID 是否对应于 运行 CloudFormation 堆栈中的实例,您可以调用 DescribeStackResources API with the current stack ID as the StackName request parameter。如果返回任何 StackResource 以及与事件返回的 EC2 实例 ID 匹配的 PhysicalResourceId,那么您可以确定该事件对应于当前堆栈中的实例。

这是一个完整的工作示例:

Description: Run a Lambda function when the EC2 instance is created using a CloudWatch Event.
Mappings:
  # amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2
  RegionMap:
    us-east-1:
      "64": "ami-9be6f38c"
Parameters:
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: m3.medium
    AllowedValues: [m3.medium, m3.large, m3.xlarge, m3.2xlarge, c3.large,
      c3.xlarge, c3.2xlarge, c3.4xlarge, c3.8xlarge, r3.large, r3.xlarge, r3.2xlarge, r3.4xlarge,
      r3.8xlarge, i2.xlarge, i2.2xlarge, i2.4xlarge, i2.8xlarge]
    ConstraintDescription: Please choose a valid instance type.
Resources:
  WebServer:
    Type: AWS::EC2::Instance
    DependsOn: EventLambdaPermission
    Properties:
      ImageId: !FindInMap [ RegionMap, !Ref "AWS::Region", 64]
      InstanceType: !Ref InstanceType
  EventRule:
    Type: AWS::Events::Rule
    Properties:
      Description: EventRule
      EventPattern:
        source: ["aws.ec2"]
        detail-type: ["EC2 Instance State-change Notification"]
        detail:
          state: [pending]
      State: ENABLED
      Targets:
      - Arn: !GetAtt EC2StateChange.Arn
        Id: TargetFunction
  EC2StateChange:
    Type: AWS::Lambda::Function
    Properties:
      Description: Sends a Wait Condition signal to Handle when an EC2 Instance State-change from this stack is received.
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          var AWS = require('aws-sdk');
          exports.handler = function(event, context) {
            console.log("Request received:\n", JSON.stringify(event));
            var instanceId = event.detail['instance-id'];
            var cloudformation = new AWS.CloudFormation();
            cloudformation.describeStackResources({StackName: '${AWS::StackId}'}).promise().then((stackData)=>{
              if (stackData.StackResources.find((stack)=> stack.PhysicalResourceId == instanceId)) {
                finish(context, {
                  "Status" : "SUCCESS",
                  "UniqueId" : "InstanceId",
                  "Data" : instanceId,
                  "Reason" : ""
                });
              } else {
                console.log("Instance ID not found in this stack");
                context.done();
              }
            }).catch((e)=>{
              console.log("Error:\n",JSON.stringify(e));
              finish(context, {
                "Status" : "FAILED",
                "UniqueId" : "InstanceId",
                "Data" : instanceId,
                "Reason" : e.message
              });
            });
          };
          function finish(context, response) {
            responseBody = JSON.stringify(response);
            var https = require("https");
            var url = require("url");
            var parsedUrl = url.parse('${Handle}');
            var options = {
                hostname: parsedUrl.hostname,
                port: 443,
                path: parsedUrl.path,
                method: "PUT",
                headers: {
                    "content-type": "",
                    "content-length": responseBody.length
                }
            };
            console.log("Options:\n", JSON.stringify(options));
            var request = https.request(options, function(response) {
                console.log("Status code: " + response.statusCode);
                console.log("Status message: " + response.statusMessage);
                console.log("Done!");
                context.done();
            });
            request.on("error", function(error) {
                console.log("send(..) failed executing https.request(..): " + error);
                context.done();
            });
            request.write(responseBody);
            request.end();
          }
      Timeout: 30
      Runtime: nodejs4.3
  EventLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref EC2StateChange
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt EventRule.Arn
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    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"
      Policies:
      - PolicyName: EC2Policy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
              - 'cloudformation:DescribeStackResources'
              Resource: ['*']
  Handle:
    Type: AWS::CloudFormation::WaitConditionHandle
  Wait:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !Ref Handle
      Timeout: 300
Outputs:
  Result:
    Value: !GetAtt Wait.Data

wjordan's 中的答案对我有用并且似乎是可靠的并且没有任何竞争条件。

解决方案:一旦在 Cloudwatch 事件流上检测到实例,就针对 cloudformation API 调用 DescribeStackResources(..) 并传递 {PhysicalResourceId: instanceId}。它将 return 实例所属的堆栈 ID,如果它不属于任何 cloudformation 堆栈,则为空结果。

我在 Go 中实现了它,并将其放在 tleyden/awsutil 存储库中的 github 上。