AWS Cloud Operations & Migrations Blog

Build AWS Systems Manager Automation runbooks using AWS CDK

AWS Systems Manager Automation runbooks let you deploy, configure, and manage AWS resources safely and at scale. You can use AWS-published runbooks or build your own to enable AWS resource management across multiple accounts and regions. The AWS Cloud Development Kit (AWS CDK v2) is an open-source framework that can build applications with the expressive power of a programming language.

Today, customers using CDK to author runbooks use lower constructs and custom tools to test these authored runbooks. Now, we’ve added high-level constructs to author runbooks and a capability to simulate runbook processing. This post will show how to use the AWS CDK to speed up runbook authoring and test it by locally simulating the processing.

Documents AWS CDK library overview

The Document CDK Library provides constructs for authoring Automation runbooks, Command documents, and simulation for locally testing Automation runbooks.

  • The library is available in Maven, NuGet, NPM, and PyPI.
  • Documents can export to either YAML or JSON.
  • Build document constructs for repeat patterns or standardization.
  • Locally simulate execution for existing YAML or JSON documents.
  • Provides code completion for some development tools

Prerequisites

The examples in this post use Typescript.  Before you get started, make sure you have the following prerequisites:

Creating the CDK application

Create the directory and navigate into that directory.

mkdir automation-runbook-demo && cd automation-runbook-demo

Next, initialize a new typescript application.

cdk init app --language=typescript

Install the document library that provides the L2 constructs and save the package.json file.

npm install @cdklabs/cdk-ssm-documents --save-prod

Updating the stack

In the lib sub-directory of the project, edit the automation-runbook-demo-stack.ts file and add the following import statements at the beginning of the file.

import {Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";
import {
  HardCodedString,
  AutomationDocument,
  AwsApiStep,
  BranchStep, Choice,
  DataTypeEnum,
  DocumentFormat,
  Input, InvokeLambdaFunctionStep, Operation,
  StringVariable, AwsService
} from "@cdklabs/cdk-ssm-documents";

Next, you need to expose the runbook for testing. On a new line after the export class AutomationRunbookDemoStack extends cdk.Stack { statement add the following line.

readonly myDoc: AutomationDocument;

Directly underneath the call to super(scope, id, props); paste in the following to create a function the runbook will invoke.

new Function(this, 'myFunctionName', {
    functionName: 'myFunctionName',
    runtime: Runtime.PYTHON_3_8,
    handler: 'index.lambda_handler',
    code: Code.fromInline("def lambda_handler(event, context):\n   return 'Hello from Lambda!'")
});

Create the runbook

The runbook will have one input and use an aws:branch step to evaluate if an aws:executeAwsApi or aws:invokeLambdaFunction step will execute.  Paste the following code after the function.

this.myDoc = new AutomationDocument(this, 'myAutomationRunbook', {
    documentFormat: DocumentFormat.YAML,
    tags: [{key: 'myTag',value: 'myValue'}],
    documentName: 'myAutomationRunbook',
    description: 'This is a sample runbook created by the CDK using @cdklabs/cdk-ssm-documents"!',
    updateMethod: 'NewVersion',
    docInputs: [
       Input.ofTypeString('Action', {
       allowedValues: ['API', 'Lambda'],
       description: '(Required) What step to execute.'})
      ],
});

Next, create three steps by pasting the following code after the new document.

const lambdaStep = new InvokeLambdaFunctionStep(this, 'lambda', {
    functionName: new HardCodedString('myFunctionName'),
    isEnd: true
});

const apiStep = new AwsApiStep(this, 'api', {
    service: AwsService.STS,
    pascalCaseApi: 'getCallerIdentity',
    apiParams: {},
    isEnd: true,
    outputs: [{
    outputType: DataTypeEnum.STRING,
        name: 'User',
        selector: '$.UserId'
      }]
});

const branchStep = new BranchStep(this, 'branch', {
   choices: [
       new Choice({
          operation: Operation.STRING_EQUALS,
          variable: StringVariable.of('Action'),
           constant: 'API',
           jumpToStepName: apiStep.name}),
       new Choice({
          operation: Operation.STRING_EQUALS,
          variable: StringVariable.of('Action'),
          constant: 'Lambda',
          jumpToStepName: lambdaStep.name})
    ],
});

Finally, paste the following to call the steps created in the previous step.

this.myDoc.addStep(branchStep);
this.myDoc.addStep(apiStep);
this.myDoc.addStep(lambdaStep);

Save and exit the automation-runbook-demo-stack.ts file

Test the runbook

In the test sub-directory of the project, edit the automation-runbook-demo.test.ts file and replace the commented import statements with the following.

import * as cdk from 'aws-cdk-lib';
import {AwsService, MockAwsInvoker, Simulation} from "@cdklabs/cdk-ssm-documents";
import {AutomationRunbookDemoStack} from "../lib/automation-runbook-demo-stack";

When declaring a Simulation, the library facilitates mocking API calls by assigning an instance of MockAwsInvoker to the awsInvoker property. This pattern allows testing of the runbook before deployment.  Suppose the awsInvoker property isn’t specified when you declare a Simulation. In that case, operations route to the AWS API. Remove the existing test statement and comments, then paste the following to test ‘API’ as the value of the Step input.

test('API Selected', () => {
    const app = new cdk.App();
    const stack = new AutomationRunbookDemoStack(app, 'MyTestStack');

    const mockInvoker = new MockAwsInvoker();
    mockInvoker.whenThen({
        // WHEN api is called ...
        service: AwsService.STS,
        awsApi: 'getCallerIdentity',
        awsParams: {}
        }, {
        // THEN respond with ...
        "UserId": "myUserId",
        "Account": "0123456789012",
        "Arn": "arn:aws:sts::0123456789012:assumed-role/myAssumedRole/myUserId"
    });

    const response = Simulation.ofAutomation(stack.myDoc, {
        awsInvoker: mockInvoker
    }).simulate({
        Action: "API"
    });
    expect(response.outputs?.['api.User']).toEqual("myUserId");
}); 

Next, paste the following to test Script as the value of the Step input.

test('Script Selected', () => {
    const app = new cdk.App();
    const stack = new AutomationRunbookDemoStack(app, 'MyTestStack');

    const mockInvoker = new MockAwsInvoker();
    mockInvoker.whenThen({
        // WHEN api is called ...
        service: AwsService.LAMBDA,
        awsApi: 'invoke',
        awsParams: {"FunctionName":"myFunctionName","InvocationType":"RequestResponse","LogType":"Tail"}
    }, {
        // THEN respond with ...
        StatusCode: 200,
        Payload: "Hello from the simulator."
    });
    const response = Simulation.ofAutomation(stack.myDoc, {
        awsInvoker: mockInvoker
    }).simulate({Action: 'Lambda'});
    expect(response.outputs?.['lambda.Payload']).toEqual("Hello from the simulator.");
});

Save the automation-runbook-demo.test.ts file. Run the following command from the root of the project directory to perform the tests.

npm test

After successful tests, deploy the stack by running the following command.

cdk deploy

Execute the Automation runbook

  1. In the AWS Systems Manager console, select Automation under Change Management.
  2. Click the Execute Automation button and select Owned by me tab.
  3. Select the myAutomationRunbook Automation runbook and select Next.
  4. Choose Simple execution.
  5. Navigate to the Action input parameter and select an option from the drop-down.
  6. Choose Execute.
  7. Cleanup

To remove the resources created with this application, run the following command.

cdk destroy

Conclusion

This post demonstrated creating an Automation runbook using a custom CDK library. You also used the library to simulate and test the runbook before deployment. In addition to Automation runbooks, the library supports the creation of Command documents.  The L2 constructs allow customers to build repeatable L3 constructs or patterns. The library is available in MavenNuGet, NPM, and PyPI.  Download today and start building!

About the author:

Author, Brad Malmgren

Brad Malmgren

Brad Malmgren is a TPM for AWS Systems Manager. He is passionate about developing solutions that empower customers operate in the cloud safely and at scale.