Appendix C: Action Customization - Operations Conductor

Appendix C: Action Customization

Operations Conductor enables you to extend the solution by adding your own actions in the web consoles action catalog.

Create an Automation Document

Operations Conductor leverages AWS Systems Manager to perform actions on specific resources. Before adding a new action to the Systems Manager console, you must first create a new automation document. For more information, see Working with Automation Documents.

Note

We recommend first creating a new automation document in a text editor and then uploading the entire document to the Systems Manager console. Note that the document must be written in YAML format.

The following procedures show how to create an automation document that stops a running Amazon Elastic Compute Cloud (Amazon EC2) instance.

Document Description

This description will appear when viewing your document in the Systems Manager console and when viewing the action in the solution’s web console.

description: <(Operations Conductor) Stops an EC2 Instance>

Schema Version

Currently, AWS Systems Manager automation documents only support Schema Version 0.3.

schemaVersion: 0.3

Operations Conductor Role

When Systems Manager executes your automation document, it assumes the role you provided. Replace the value of assumeRole with the ARN of the role that was created when the solution was launched. You can find your created ARN by navigating to the stack Outputs section of the template.

assumeRole: “<%%OperationsConductorSharedRoleArn%%>

Parameters

The automation document requires the SQSMsgBody, and SQSMsgReceiptHandle parameters to run the document.

The TargetResourceType parameter determines what type of resources will be selected when the resources selector AWS Lambda function searches for the resources with the supplied Target Tag. The parameter requires a value and must be formatted service:resourceType. In the below example, the parameter is set to ec2:instance.

The listed parameters above won’t be shown in the web console. However, any additional parameters added will be shown in the UI, in order to be set during task creation. The following example shows an additional ArbitraryParameter which you can see in the web console and will be referenced later in the automation document.

parameters: SQSMsgBody: type: "String" description: "JSON Stringified version of the message body that was read off the Resource Queue" SQSMsgReceiptHandle: type: "String" description: "Receipt handle of the SQS message that was read off the queue" TargetResourceType: type: "String" description: "The AWS resource type for which this automation applies. The format of this value should be: service:resourceType" default: "ec2:instance" ArbitraryParameter: type: "String" description: "(Optional) An arbitrary value" default: ""

Automation Steps

When the solution executes the automation document, a series of steps are performed to validate that the resources are still tagged with the correct Target Tag, execute the action on the resource, and update the solution’s Amazon DynamoDB tables.

The VALIDATE_MSG_CONTENTS step inspects the body of the message that was read from the resource queue and parses out the parameters needed to perform the action.

If a Python exception is raised in this script, the automation document stops processing and no further steps are executed. The script raises an exception if any required parameters are missing from the SQSMsgBody. An output object is populated as the step is executed and is returned when the it finishes. Properties of this object are mapped as output parameters for this step and will be referenced in subsequent steps.

The following code samples, show the individual steps of the automation.

mainSteps: - name: "VALIDATE_MSG_CONTENTS" action: aws:executeScript timeoutSeconds: 30 description: "Validates contents of the message from the Resource Queue and parses out parameters" inputs: Runtime: python3.6 Handler: script_handler InputPayload: SQSMsgBody: "{{ SQSMsgBody }}" ArbitraryParameter: "{{ ArbitraryParameter }}" Script: |- import boto3 import json def script_handler(events, context): output = { "statusCode": 200 } sqs_msg_body = json.loads(events["SQSMsgBody"]) if "ResourceId" not in sqs_msg_body: raise Exception("ResourceId was not found in the SQS Message Body.") output["resource_id"] = sqs_msg_body["ResourceId"] if "ResourceRegion" not in sqs_msg_body: raise Exception("ResourceRegion was not found in the SQS Message Body.") output["source_region"] = sqs_msg_body["ResourceRegion"] if "ResourceAccount" not in sqs_msg_body: raise Exception("ResourceAccount was not found in the SQS Message Body.") output["source_account_id"] = sqs_msg_body["ResourceAccount"] if "TargetTag" not in sqs_msg_body: raise Exception("TargetTag was not found in the SQS Message Body.") output["target_tag_name"] = sqs_msg_body["TargetTag"] if "TaskId" not in sqs_msg_body: raise Exception("TaskId was not found in the SQS Message Body.") output["task_id"] = sqs_msg_body["TaskId"] if "ParentExecutionId" not in sqs_msg_body: raise Exception("ParentExecutionId was not found in the SQS Message Body.") output["parent_execution_id"] = sqs_msg_body["ParentExecutionId"] output["arbitrary_parameter"] = events["ArbitraryParameter"] return output outputs: - Name: "resource_id" Selector: "$.Payload.resource_id" Type: "String" - Name: "source_region" Selector: "$.Payload.source_region" Type: "String" - Name: "source_account_id" Selector: "$.Payload.source_account_id" Type: "String" - Name: "target_tag_name" Selector: "$.Payload.target_tag_name" Type: "String" - Name: "task_id" Selector: "$.Payload.task_id" Type: "String" - Name: "parent_execution_id" Selector: "$.Payload.parent_execution_id" Type: "String" - Name: "arbitrary_parameter" Selector: "$.Payload.arbitrary_parameter" Type: "String"

The CREATE_PERFORM_ACTION_AUTOMATION_EXECUTION_RECORD step will log a record of the current document execution in the automation executions Amazon DynamoDB table.

The value for parentExecutionId is the output from the previous step and the value for automationExecutionId is provisioned by Systems Manager.

- name: "CREATE_PERFORM_ACTION_AUTOMATION_EXECUTION_RECORD" action: aws:executeAwsApi timeoutSeconds: 30 description: "Creates a record of this automation execution in the Operations Conductor Automation Executions Table" inputs: { "Service": "dynamodb", "Api": "PutItem", "TableName": "<%%AutomationExecutionsTableName%%>”, "Item": { "parentExecutionId": { "S": "{{ SSM_BRANCH_EXECUTE_ACTION.parent_execution_id }}" }, "automationExecutionId": { "S": "{{ automation:EXECUTION_ID }}" } } }

The PERFORM_ACTION_ON_RESOURCE step assumes the AWS Identity and Access Management (IAM) role that was generated for the task. Assuming the role enables the script to continue and perform the action on the resource.

In the first line of code, the script handler demonstrates how you would access the value of a parameter that was defined earlier in the automation document. For steps of type aws:executeScript, parameters must be defined in the InputPayload and accessed within an argument passed to the script handler.

- name: "PERFORM_ACTION_ON_RESOURCE" action: aws:executeScript timeoutSeconds: 30 description: "Stops an EC2 instance" inputs: Runtime: python3.6 Handler: script_handler InputPayload: arbitrary_parameter: "{{ VALIDATE_MSG_CONTENTS.arbitrary_parameter }}" source_account_id: "{{ VALIDATE_MSG_CONTENTS.source_account_id }}" source_region: "{{ VALIDATE_MSG_CONTENTS.source_region }}" resource_id: "{{ VALIDATE_MSG_CONTENTS.resource_id }}" target_tag_name: "{{ VALIDATE_MSG_CONTENTS.target_tag_name }}" task_id: "{{ VALIDATE_MSG_CONTENTS.task_id}}" Script: |- import boto3 import json def script_handler(events, context): print(f"Arbitrary Parameter Value: { events['arbitrary_parameter'] }") source_account_id = events["source_account_id"] source_region = events["source_region"] task_id = events["task_id"] # Assume role in source account sts_connection = boto3.client('sts') assumed_role = sts_connection.assume_role( RoleArn=f"arn:aws:iam::{source_account_id}:role/{source_account_id}-{source_region}-{task_id}", RoleSessionName="ops_conductor_stop_instance" ) # Look up the Instance by ID and make sure it is still tagged correctly ec2_client = boto3.client( 'ec2', region_name=source_region, aws_access_key_id=assumed_role['Credentials']['AccessKeyId'], aws_secret_access_key=assumed_role['Credentials']['SecretAccessKey'], aws_session_token=assumed_role['Credentials']['SessionToken'] ) desc_instance_response = ec2_client.describe_instances(InstanceIds=[events["resource_id"]]) instance = desc_instance_response["Reservations"][0]["Instances"][0] tag_found = False instance_tags = instance["Tags"] for tag in instance_tags: if tag["Key"] == events["target_tag_name"]: tag_found = True break if not tag_found: raise Exception(f"Instance ({ events['resource_id'] }) was found but it was not tagged with { events['target_tag_name'] }.") print(f"Instance ({ events['resource_id'] }) is still tagged with { events['target_tag_name'] }. Stopping the instance.") stop_params = { "InstanceIds": [events["resource_id"]] } stop_response = ec2_client.stop_instances(**stop_params) print("Success") print(f"{str(stop_response)}") return { 'statusCode': 200 }

The REMOVE_MSG_FROM_RESOURCE_QUEUE step provides information of the action that was performed on the resource.

Verify that you replace %%ResourceQueueUrl%% with the URL of the Resource Queue that was created when the AWS CloudFormation template was deployed.

- name: "REMOVE_MSG_FROM_RESOURCE_QUEUE" action: "aws:executeAwsApi" inputs: { "Service": "sqs", "Api": "DeleteMessage", "QueueUrl": "%%ResourceQueueUrl%%", "ReceiptHandle": "{{ SQSMsgReceiptHandle }}" }

The UPDATE_AUTOMATION_EXECUTION_RECORD step updates the record for the execution of this action on a resource. Progress is shown in the solution’s web console.

Verify that you replace %% AutomationExecutionsTableName%% with the name of the automation executions Amazon DynamoDB table that was created when the AWS CloudFormation template was deployed.

- name: "UPDATE_AUTOMATION_EXECUTION_RECORD" action: aws:executeAwsApi timeoutSeconds: 30 description: "Updates the record of this automation execution in the Operations Conductor Automation Executions Table to mark it as successfully completed" inputs: { "Service": "dynamodb", "Api": "UpdateItem", "TableName": "<%%AutomationExecutionsTableName%%>", "Key": { "parentExecutionId": { "S": "{{ VALIDATE_MSG_CONTENTS.parent_execution_id }}" }, "automationExecutionId": { "S": "{{ automation:EXECUTION_ID }}" } }, "UpdateExpression": "SET #stat = :val1", "ExpressionAttributeNames": { "#stat": "status" }, "ExpressionAttributeValues": { ":val1": { "S": "Success" } } }

The UPDATE_TASK_EXECUTIONS_RECORD step updates the record for the current execution of the task. Progress can be tracked in the solution’s web console.

Verify that you replace %%TaskExecutionsTableName%% with the name of the task executions Amazon DynamoDB table that was created when the AWS CloudFormation template was deployed.

- name: "UPDATE_TASK_EXECUTIONS_RECORD" action: aws:executeAwsApi timeoutSeconds: 30 description: "Updates the record for the overall execution of the Operations Conductor Task that spawned this automation" inputs: { "Service": "dynamodb", "Api": "UpdateItem", "TableName": "<%%TaskExecutionsTableName%%>", "Key": { "taskId": { "S": "{{ VALIDATE_MSG_CONTENTS.task_id }}" }, "parentExecutionId": { "S": "{{ VALIDATE_MSG_CONTENTS.parent_execution_id }}" } }, "UpdateExpression": "SET completedResourceCount = completedResourceCount + :incr, lastUpdateTime = :uptime", "ExpressionAttributeValues": { ":incr": { "N": "1" }, ":uptime": { "S": "{{ global:DATE_TIME }}" } } }

The CHECK_FOR_TASK_EXECUTION_COMPLETION step verifies that the number of resources successfully processed for this task matches the total number of resources found when the task was initially executed. If it matches, the status of the overall task is set to success, and will be shown in the web console.

Verify that you replace %%TaskExecutionsTableName%% with the name of the task executions Amazon DynamoDB table that was created when the AWS CloudFormation template was deployed.

You must include onFailure: Continue. This enables the automation document to continue successfully if the ConditionExpression of the UpdateItem call isn’t met. This condition will only be met upon completion of the final resource for this task.

- name: "CHECK_FOR_TASK_EXECUTION_COMPLETION" action: aws:executeAwsApi timeoutSeconds: 30 description: "Marks the overall task execution as Success if all resources have been successfully acted on" inputs: { "Service": "dynamodb", "Api": "UpdateItem", "TableName": "<%%TaskExecutionsTableName%%>", "Key": { "taskId": { "S": "{{ VALIDATE_MSG_CONTENTS.task_id }}" }, "parentExecutionId": { "S": "{{ VALIDATE_MSG_CONTENTS.parent_execution_id }}" } }, "UpdateExpression": "SET #s = :stat", "ConditionExpression": "completedResourceCount = totalResourceCount", "ExpressionAttributeNames": { "#s": "status" }, "ExpressionAttributeValues": { ":stat": { "S": "Success" } } } onFailure: Continue isEnd: true

Create IAM Role Template

You must define an AWS Identity and Access Management (IAM) role with the permissions required to perform the given action on the resources you specify. The solution’s scripts will assume this role when performing and verifying the action. In the event of stopping an Amazon EC2 instance, the instance must have the ec2:DescribeInstances, ec2:StopInstances, and tag:GetResources permissions. Verify that you have made the following changes:

  • %%ResourceSelectorExecutionRoleArn%%: Replace with the ARN of the Operations Conductor resource selector.

  • %%OperationsConductorSharedRoleArn%%: Replace with the ARN of the Operations Conductor shared role.

  • %%MASTER_ACCOUNT%%: The 12-digit ID of the account where you launched the solution’s AWS CloudFormation template.

The name of the role will be set using the following format: <AccountId>-$<Region>-<TaskId>. AccountId and Region will be replaced by the AWS CloudFormation template, and must be set manually. Do not replace %%TASK_ID%%, the solution will automatically set the value when a task is created for this action.

AWSTemplateFormatVersion: "2010-09-09" Description: "(SO0065) - Operations Conductor Stop Instances for cross accounts/regions.” Mappings: MasterAccount: ResourceSelectorExecutionRole: Name: "<%%ResourceSelectorExecutionRoleArn%%>" DocumentAssumeRole: Name: "<%%OperationsConductorSharedRoleArn%%>" Account: Id: "%%MASTER_ACCOUNT%%" Resources: IAMRole: Type: AWS::IAM::Role Description: "Role to allow master account to perform actions" Properties: RoleName: !Sub "${AWS::AccountId}-${AWS::Region}-%%TASK_ID%%" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: AWS: - !FindInMap ["MasterAccount", "Account", "Id"] Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: "OperationsConductor-StopInstances" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "ec2:DescribeInstances" - "ec2:StopInstances" - "tag:GetResources" Resource: - "*" - Effect: "Allow" Action: - "iam:PassRole" Resource: - !FindInMap ["MasterAccount", "ResourceSelectorExecutionRole", "Name"] - !FindInMap ["MasterAccount", "DocumentAssumeRole", "Name"]

Upload the Automation Document to Systems Manager

Use the following procedure to upload the automation document to AWS Systems Manager.

  1. In the AWS Systems Manager console in your primary account, navigate to Shared Resources.

  2. Select Documents on the left.

  3. Select Owned by me to view the documents owned by this account.

  4. Select Create automation.

    We recommend naming your document using the same format as the documents the solution created when the primary AWS CloudFormation template was launched. For example, <solution-stack-name>-OperationsConductor-<action-name>.

  5. Select the Editor tab, then select Edit, and select OK.

  6. In the Document Editor, paste the contents of the automation document, and select Create automation.

  7. Select Owned by me, and navigate to the uploaded document.

  8. Select Details, navigate to Tags, and select Edit.

  9. Enter the Tag Key and Value you defined in the Document Tag Key and Document Tag Value template parameters when you launched the primary template.

  10. Select Save.

  11. Log in to the solution’s web console, select Get Started, and verify that the new action is listed in the Action Catalog.

Upload the IAM Role Temaplate to Amazon S3

Use the following procedure to upload the IAM role template to the Amazon Simple Storage Service (Amazon S3) bucket.

Note

Verify that you are logged into the console in the primary account where you deployed the solution.

  1. Navigate to the Amazon S3 console in your primary account.

  2. Navigate to the solution-created Amazon S3 bucket, select Create Folder, and name the folder using the same name as the newly created automation document. Note that the solution requires you to use the same name for the folder and document.

  3. Save the template as cloudformation.template, and upload the IAM role template.

    Once completed, you can begin setting up tasks for your actions in the solution’s web console.