Merricat
Merricat

Reputation: 2851

Is it possible to extract "instanceId" from EventBridge event data, and use it as Target Value?

I was able to setup AutoScaling events as rules in EventBridge to trigger SSM Commands, but I've noticed that with my chosen Target Value the event is passed to all my active EC2 Instances. My Target key is a tag shared by those instances, so my mistake makes sense now.

I'm pretty new to EventBridge, so I was wondering if there's a way to actually target the instance that triggered the AutoScaling event (as in extracting the "InstanceId" that's present in the event data and use that as my new Target Value). I saw the Input Transformer, but I think that just transforms the event data to pass to the target.

Thanks!

EDIT - help with js code for Lambda + SSM RunCommand

I realize I can achieve this by setting EventBridge to invoke a Lambda function instead of the SSM RunCommand directly. Can anyone help with the javaScript code to call a shell command on the ec2 instance specified in the event data (event.detail.EC2InstanceId)? I can't seem to find a relevant and up-to-date base template online, and I'm not familiar enough with js or Lambda. Any help is greatly appreciated! Thanks

Sample of Event data, as per aws docs

{
  "version": "0",
  "id": "12345678-1234-1234-1234-123456789012",
  "detail-type": "EC2 Instance Launch Successful",
  "source": "aws.autoscaling",
  "account": "123456789012",
  "time": "yyyy-mm-ddThh:mm:ssZ",
  "region": "us-west-2",
  "resources": [
      "auto-scaling-group-arn",
      "instance-arn"
  ],
  "detail": {
      "StatusCode": "InProgress",
      "Description": "Launching a new EC2 instance: i-12345678",
      "AutoScalingGroupName": "my-auto-scaling-group",
      "ActivityId": "87654321-4321-4321-4321-210987654321",
      "Details": {
          "Availability Zone": "us-west-2b",
          "Subnet ID": "subnet-12345678"
      },
      "RequestId": "12345678-1234-1234-1234-123456789012",
      "StatusMessage": "",
      "EndTime": "yyyy-mm-ddThh:mm:ssZ",
      "EC2InstanceId": "i-1234567890abcdef0",
      "StartTime": "yyyy-mm-ddThh:mm:ssZ",
      "Cause": "description-text"
  }
}

Edit 2 - my Lambda code so far

'use strict'

const ssm = new (require('aws-sdk/clients/ssm'))()

exports.handler = async (event) => {
    const instanceId = event.detail.EC2InstanceId
    var params = {
        DocumentName: "AWS-RunShellScript",
        InstanceIds: [ instanceId ],
        TimeoutSeconds: 30,
        Parameters: {
          commands: ["/path/to/my/ec2/script.sh"],
          workingDirectory: [],
          executionTimeout: ["15"]
        }
    };

    const data = await ssm.sendCommand(params).promise()
    const response = {
        statusCode: 200,
        body: "Run Command success",
    };
    return response;
}

Upvotes: 1

Views: 2942

Answers (2)

Aaron Brockmeyer
Aaron Brockmeyer

Reputation: 21

You can do this without using lambda, as I just did, by using eventbridge's input transformers.

I specified a new automation document that called the document I was trying to use (AWS-ApplyAnsiblePlaybooks).

My document called out the InstanceId as a parameter and is passed this by the input transformer from EventBridge. I had to pass the event into lambda just to see how to parse the JSON event object to get the desired instance ID - this ended up being

$.detail.EC2InstanceID 

(it was coming from an autoscaling group).

I then passed it into a template that was used for the runbook

{"InstanceId":[<instance>]}

This template was read in my runbook as a parameter.

This was the SSM playbook inputs I used to run the AWS-ApplyAnsiblePlaybook Document, I just mapped each parameter to the specified parameters in the nested playbook:

 "inputs": {
      "InstanceIds": ["{{ InstanceId }}"],
      "DocumentName": "AWS-ApplyAnsiblePlaybooks",
      "Parameters": {
        "SourceType": "S3",
        "SourceInfo": {"path": "https://testansiblebucketab.s3.amazonaws.com/"},
        "InstallDependencies": "True",
        "PlaybookFile": "ansible-test.yml",
        "ExtraVariables": "SSM=True",
        "Check": "False",
        "Verbose": "-v",
        "TimeoutSeconds": "3600"
      }

See the document below for reference. They used a document that was already set up to receive the variable

https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-tutorial-eventbridge-input-transformers.html

This is the full automation playbook I used, most of the parameters are defaults from the nested playbook:

 {
"description": "Runs Ansible Playbook on Launch Success Instances",
"schemaVersion": "0.3",
"assumeRole": "<Place your automation role ARN here>",
"parameters": {
  "InstanceId": {
    "type": "String",
    "description": "(Required) The ID of the Amazon EC2 instance."
  }
},
"mainSteps": [
  {
    "name": "RunAnsiblePlaybook",
    "action": "aws:runCommand",
    "inputs": {
      "InstanceIds": ["{{ InstanceId }}"],
      "DocumentName": "AWS-ApplyAnsiblePlaybooks",
      "Parameters": {
        "SourceType": "S3",
        "SourceInfo": {"path": "https://testansiblebucketab.s3.amazonaws.com/"},
        "InstallDependencies": "True",
        "PlaybookFile": "ansible-test.yml",
        "ExtraVariables": "SSM=True",
        "Check": "False",
        "Verbose": "-v",
        "TimeoutSeconds": "3600"
      }
    }
  }
]

}

Upvotes: 1

Merricat
Merricat

Reputation: 2851

Yes, but through Lambda

EventBridge -> Lambda (using SSM api) -> EC2

Thank you @Sándor Bakos for helping me out!! My JavaScript ended up not working for some reason, so I ended up just using part of the python code linked in the comments.

1. add ssm:SendCommand permission:

After I let Lambda create a basic role during function creation, I added an inline policy to allow Systems Manager's SendCommand. This needs access to your documents/*, instances/* and managed-instances/*

2. code - python 3.9

import boto3
import botocore
import time

def lambda_handler(event=None, context=None):
    try:
        client = boto3.client('ssm')
    
        instance_id = event['detail']['EC2InstanceId']
        command = '/path/to/my/script.sh'
        
        client.send_command(
            InstanceIds = [ instance_id ],
            DocumentName = 'AWS-RunShellScript',
            Parameters = {
                'commands': [ command ],
                'executionTimeout': [ '60' ]
            }
        )

Upvotes: 2

Related Questions