Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Currently supported are:
* Put a record to a Kinesis Data Firehose stream
* [Put an event on an EventBridge bus](#put-an-event-on-an-eventbridge-bus)
* [Send an event to EventBridge API Destination](#invoke-an-api-destination)
* [Start a Systems Manager Run Command](#start-a-systems-manager-run-command)
* [Start a Systems Manager Automation](#start-a-systems-manager-automation)

See the README of the `@aws-cdk/aws-events` library for more information on
EventBridge.
Expand Down Expand Up @@ -345,3 +347,61 @@ rule.addTarget(new targets.EventBus(
),
));
```

## Start a Systems Manager Run Command

Use the `SsmRunCommand` target to trigger a Systems Manager Run Command.

The code snippet below creates the scheduled event rule that triggers a Systems Manager Run Command every day.

```ts
const rule = new events.Rule(this, 'Rule', {
schedule: events.Schedule.expression('rate(1 day)'),
});

rule.addTarget(new targets.SsmRunCommand(new ssm.CfnDocument(stack, 'MyDocument', {
content: {...},
documentType: 'Command',
name: 'my-document',
}), {
targetKey: 'InstanceIds',
targetValues: ['i-asdfiuh2304f'],
}));
```

## Start a Systems Manager Automation

Use the `SsmRunCommand` target to trigger a Systems Manager Automation.

The code snippet below creates the scheduled event rule that triggers a Systems Manager Automation every day. It also
creates a new role to be used by the Automation. This role should have the required permissions to execute the Automation.

```ts
const rule = new events.Rule(this, 'Rule', {
schedule: events.Schedule.expression('rate(1 day)'),
});
const ssmAssumeRole = new iam.Role(this, 'SSMAssumeRole', {
assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
});

rule.addTarget(new targets.SsmAutomation(new ssm.CfnDocument(stack, 'MyDocument', {
content: {...},
documentType: 'Automation',
name: 'my-document',
}), {
ssmAssumeRole,
}));
```

You can also target a shared document by passing the document ARN to the `SsmAutomation` target.

```ts
const automationArn = 'arn:aws:ssm:us-east-1::automation-definition/AWS-StopRdsInstance:$DEFAULT';

rule.addTarget(automationArn, {
input: {
InstanceIds: ['my-rds-instance'],
},
ssmAssumeRole,
});
```
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export * from './log-group';
export * from './kinesis-firehose-stream';
export * from './api-gateway';
export * from './api-destination';
export * from './ssm-automation';
export * from './ssm-runcommand';
export * from './util';
98 changes: 98 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/ssm-automation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as ssm from '@aws-cdk/aws-ssm';
import * as cdk from '@aws-cdk/core';
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* Create an SSM Automation Event Target
*/
export interface SsmAutomationProps extends TargetBaseProps {
/**
* Role to be used for invoking the Automation from the Rule. This should be a
* role that allows the the events.amazonaws.com service principal to assume
* and execute the Automation. This role is not used by the Automation itself,
* to execute the actions in the document, see `automationAssumeRole` for that.
*
* @default - a new role is created.
*/
readonly role?: iam.IRole;

/**
* The input parameters for the Automation document.
*
* @default - no input parameters passed to the document
*/
readonly input?: { [key: string]: string[] };

/**
* Role to be used to run the Automation on your behalf. This should be a role
* that allows the Automation service principal (ssm.amazonaws.com) to assume
* and run the actions in your Automation document.
*
* @default - no role assumed
*/
readonly automationAssumeRole?: iam.IRole;
}

/**
* Create an SSM Automation Event Target
*/
export class SsmAutomation implements events.IRuleTarget {
private documentArn: string;

constructor(
/**
* Can be an instance of `ssm.CfnDocument` or a share/managed document ARN.
*/
public readonly document: ssm.CfnDocument | string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union types don't work with jsii.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, I don't think we want to use the Cfn type of anything here. When Document gets implemented, this will then create a breaking change. This should use an IDocument as the type. I realize that this isn't implemented yet but I think that it needs to be before we can accept this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the exact same thought and started down that road.. but figured I would get your feedback first. Is there any existing or in-progress work already on implementing L2 constructs for Document? Is it possible to just wrap CfnDocument to avoid the breaking change down the road?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be willing to start the implementation of Document. Is there any documentation, standards, or requirements I can follow or reference for that implementation? Other than looking at other similar L2 implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thought.. once Document is implemented, there would be no need for the union type as theoretically (and hopefully) there would be some sort of .fromLookup() method on Document for shared documents.

Copy link
Contributor

@TheRealAmazonKendra TheRealAmazonKendra Oct 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this package: https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/example-construct-library

Specifically, the example resource, as that would be what the Document would be. Please also open that in a separate PR.

From the documentation it looks like this should be a pretty simple implementation.

private readonly props: SsmAutomationProps,
) {
this.documentArn = this.getDocumentArn();
}

/**
* Returns a RuleTarget that can be used to trigger this SSM Automation as a
* result from an EventBridge event.
*
* @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html
*/
public bind(rule: events.IRule, _id?: string): events.RuleTargetConfig {
const role = this.props.role ?? singletonEventRole(rule);
role.addToPrincipalPolicy(this.executeStatement());

if (this.props.deadLetterQueue) {
addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue);
}

return {
...bindBaseTargetConfig(this.props),
arn: this.documentArn,
input: events.RuleTargetInput.fromObject({ ...this.props.input, AutomationAssumeRole: [this.props.automationAssumeRole?.roleArn] }),
role,
targetResource: (typeof this.document === 'string') ? undefined : this.document,
};
}

private getDocumentArn(): string {
if (typeof this.document === 'string') {
return this.document;
} else {
return cdk.Arn.format({
service: 'ssm',
resource: 'automation-definition',
resourceName: this.document.name,
region: cdk.Aws.REGION,
account: cdk.Aws.ACCOUNT_ID,
partition: cdk.Aws.PARTITION,
});
}
}

private executeStatement(): iam.PolicyStatement {
return new iam.PolicyStatement({
actions: ['ssm:StartAutomationExecution'],
resources: [this.documentArn],
});
}
}
133 changes: 133 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/ssm-runcommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as ssm from '@aws-cdk/aws-ssm';
import * as cdk from '@aws-cdk/core';
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* SsmRunCommandProps
*/
export interface SsmRunCommandProps extends TargetBaseProps {
/**
* Role to be used to run the Document
*
* @default - a new role is created.
*/
readonly role?: iam.IRole;

/**
* Can be either `tag:` *tag-key* or `InstanceIds` .
*
* @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-rule-runcommandtarget.html#cfn-events-rule-runcommandtarget-key
*/
readonly targetKey: events.CfnRule.RunCommandTargetProperty['key'];

/**
* If Target Key is 'InstanceIds', Values are the list of EC2 Instance Ids. If Target Key is 'tag:<Amazon EC2 tag>', Values are list of tag values.
*/
readonly targetValues: string[];

/**
* Specify input for the SSM Document if appropriate.
*
* @default - no input parameters passed to document
*/
readonly input?: { [key: string]: string[] };
}

/**
* Create an SSM Run Command Event Target
*/
export class SsmRunCommand implements events.IRuleTarget {
private documentArn: string;

constructor(
/**
* Provide an instance of a `ssm.CfnDocument`
*/
public readonly document: ssm.CfnDocument,
private readonly props: SsmRunCommandProps,
) {
this.documentArn = this.getDocumentArn();
}

/**
* Returns a RuleTarget that can be used to trigger this SSM Run Command as a
* result from an EventBridge event.
*
* @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public bind(rule: events.IRule, _id?: string): events.RuleTargetConfig {
const role = this.props.role ?? singletonEventRole(rule);
role.addToPrincipalPolicy(this.executeStatement());

if (this.props.deadLetterQueue) {
addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue);
}

return {
...bindBaseTargetConfig(this.props),
arn: this.documentArn,
role,
input: events.RuleTargetInput.fromObject(this.props.input),
runCommandParameters: {
runCommandTargets: [
{
key: this.props.targetKey,
values: this.props.targetValues,
},
],
},
targetResource: this.document,
};
}

private getDocumentArn(): string {
return cdk.Arn.format({
service: 'ssm',
resource: 'document',
resourceName: this.document.name,
region: cdk.Aws.REGION,
account: cdk.Aws.ACCOUNT_ID,
partition: cdk.Aws.PARTITION,
});
}

private executeStatement(): iam.PolicyStatement {
if (this.props.targetKey === 'InstanceIds') {
return new iam.PolicyStatement({
actions: ['ssm:SendCommand'],
resources: this.props.targetValues.map(instanceId =>
cdk.Arn.format({
service: 'ec2',
resource: 'instance',
resourceName: instanceId,
region: cdk.Aws.REGION,
account: cdk.Aws.ACCOUNT_ID,
partition: cdk.Aws.PARTITION,
}),
),
});
} else {
return new iam.PolicyStatement({
actions: ['ssm:SendCommand'],
resources: [
cdk.Arn.format({
service: 'ec2',
resource: 'instance',
resourceName: '*',
region: cdk.Aws.REGION,
account: cdk.Aws.ACCOUNT_ID,
partition: cdk.Aws.PARTITION,
}),
],
conditions: {
StringEquals: {
'ec2:ResourceTag/*': this.props.targetValues,
},
},
});
}
}
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-events-targets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"@aws-cdk/aws-sns": "0.0.0",
"@aws-cdk/aws-sns-subscriptions": "0.0.0",
"@aws-cdk/aws-sqs": "0.0.0",
"@aws-cdk/aws-ssm": "0.0.0",
"@aws-cdk/aws-stepfunctions": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
Expand All @@ -133,6 +134,7 @@
"@aws-cdk/aws-sns": "0.0.0",
"@aws-cdk/aws-sns-subscriptions": "0.0.0",
"@aws-cdk/aws-sqs": "0.0.0",
"@aws-cdk/aws-ssm": "0.0.0",
"@aws-cdk/aws-stepfunctions": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
import * as integ from '@aws-cdk/integ-tests';
import { Construct } from 'constructs';
import * as targets from '../../lib';

class TestStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);

const event = new events.Rule(this, 'MyRule', {
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
});

const automationArn = `arn:aws:ssm:${cdk.Stack.of(this).region}::automation-definition/AWS-StopRdsInstance:$DEFAULT`;

const deadLetterQueue = new sqs.Queue(this, 'MyDeadLetterQueue');

const automationAssumeRole = new iam.Role(this, 'AutomationAssumeRole', {
assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
});

event.addTarget(new targets.SsmAutomation(automationArn, {
input: {
InstanceId: ['my-rds-instance'],
},
automationAssumeRole,
deadLetterQueue,
}));
}
}

const app = new cdk.App();

new integ.IntegTest(app, 'Testing', {
testCases: [
new TestStack(app, 'aws-cdk-ssm-automation-event-target'),
],
});
Loading