Skip to content

Commit

Permalink
feat(events): add target to make AWS API calls (#3720)
Browse files Browse the repository at this point in the history
Add a AwsApi rule target to make AWS API calls.

Comparable to the AwsCustomResource in terms of API and IAM permissions.

Closes #2538
  • Loading branch information
jogold authored and Elad Ben-Israel committed Aug 27, 2019
1 parent 042fb53 commit b6f055a
Show file tree
Hide file tree
Showing 14 changed files with 861 additions and 24 deletions.
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-events-targets/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ coverage
.LAST_PACKAGE
*.snk
.cdk.staging

lib/sdk-api-metadata.json
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Currently supported are:
* Publish a message to an SNS topic
* Send a message to an SQS queue
* Start a StepFunctions state machine
* Make an AWS API call

See the README of the `@aws-cdk/aws-events` library for more information on
CloudWatch Events.
Expand Down
20 changes: 20 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/aws-api-handler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// tslint:disable:no-console
import AWS = require('aws-sdk');
import { AwsApiProps } from '../aws-api';

export async function handler(event: AwsApiProps) {
console.log('Event: %j', event);
console.log('AWS SDK VERSION: ' + (AWS as any).VERSION);

const awsService = new (AWS as any)[event.service](event.apiVersion && { apiVersion: event.apiVersion });

try {
const response = await awsService[event.action](event.parameters).promise();
console.log('Response: %j', response);
} catch (e) {
console.log(e);
if (!event.catchErrorPattern || !new RegExp(event.catchErrorPattern).test(e.code)) {
throw e;
}
}
}
113 changes: 113 additions & 0 deletions packages/@aws-cdk/aws-events-targets/lib/aws-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import path = require('path');
import metadata = require('./sdk-api-metadata.json');
import { addLambdaPermission } from './util';

/**
* AWS SDK service metadata.
*/
export type AwsSdkMetadata = {[key: string]: any};

const awsSdkMetadata: AwsSdkMetadata = metadata;

export interface AwsApiProps {
/**
* The service to call
*
* @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html
*/
readonly service: string;

/**
* The service action to call
*
* @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html
*/
readonly action: string;

/**
* The parameters for the service action
*
* @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html
*
* @default - no parameters
*/
readonly parameters?: any;

/**
* The regex pattern to use to catch API errors. The `code` property of the
* `Error` object will be tested against this pattern. If there is a match an
* error will not be thrown.
*
* @default - do not catch errors
*/
readonly catchErrorPattern?: string;

/**
* API version to use for the service
*
* @see https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/locking-api-versions.html
* @default - use latest available API version
*/
readonly apiVersion?: string;

/**
* The IAM policy statement to allow the API call. Use only if
* resource restriction is needed.
*
* @default - extract the permission from the API call
*/
readonly policyStatement?: iam.PolicyStatement;
}

/**
* Use an AWS Lambda function that makes API calls as an event rule target.
*/
export class AwsApi implements events.IRuleTarget {
constructor(private readonly props: AwsApiProps) {}

/**
* Returns a RuleTarget that can be used to trigger this AwsApi as a
* result from a CloudWatch event.
*/
public bind(rule: events.IRule, id?: string): events.RuleTargetConfig {
const handler = new lambda.SingletonFunction(rule as events.Rule, `${rule.node.id}${id}Handler`, {
code: lambda.Code.fromAsset(path.join(__dirname, 'aws-api-handler')),
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'index.handler',
uuid: 'b4cf1abd-4e4f-4bc6-9944-1af7ccd9ec37',
lambdaPurpose: 'AWS',
});

if (this.props.policyStatement) {
handler.addToRolePolicy(this.props.policyStatement);
} else {
handler.addToRolePolicy(new iam.PolicyStatement({
actions: [awsSdkToIamAction(this.props.service, this.props.action)],
resources: ['*']
}));
}

// Allow handler to be called from rule
addLambdaPermission(rule, handler);

return {
id: '',
arn: handler.functionArn,
input: events.RuleTargetInput.fromObject(this.props),
targetResource: handler,
};
}
}

/**
* Transform SDK service/action to IAM action using metadata from aws-sdk module.
*/
function awsSdkToIamAction(service: string, action: string): string {
const srv = service.toLowerCase();
const iamService = awsSdkMetadata[srv].prefix || srv;
const iamAction = action.charAt(0).toUpperCase() + action.slice(1);
return `${iamService}:${iamAction}`;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './codepipeline';
export * from './sns';
export * from './sqs';
export * from './codebuild';
export * from './aws-api';
export * from './lambda';
export * from './ecs-task-properties';
export * from './ecs-task';
Expand Down
14 changes: 4 additions & 10 deletions packages/@aws-cdk/aws-events-targets/lib/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import { addLambdaPermission } from './util';

/**
* Customize the SNS Topic Event Target
* Customize the Lambda Event Target
*/
export interface LambdaFunctionProps {
/**
Expand All @@ -29,14 +29,8 @@ export class LambdaFunction implements events.IRuleTarget {
* result from a CloudWatch event.
*/
public bind(rule: events.IRule, _id?: string): events.RuleTargetConfig {
const permissionId = `AllowEventRule${rule.node.uniqueId}`;
if (!this.handler.permissionsNode.tryFindChild(permissionId)) {
this.handler.addPermission(permissionId, {
action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal('events.amazonaws.com'),
sourceArn: rule.ruleArn
});
}
// Allow handler to be called from rule
addLambdaPermission(rule, this.handler);

return {
id: '',
Expand Down
18 changes: 17 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import { Construct, IConstruct } from "@aws-cdk/core";

/**
Expand All @@ -19,4 +21,18 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli
policyStatements.forEach(role.addToPolicy.bind(role));

return role;
}
}

/**
* Allows a Lambda function to be called from a rule
*/
export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunction): void {
const permissionId = `AllowEventRule${rule.node.uniqueId}`;
if (!handler.permissionsNode.tryFindChild(permissionId)) {
handler.addPermission(permissionId, {
action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal('events.amazonaws.com'),
sourceArn: rule.ruleArn
});
}
}
Loading

0 comments on commit b6f055a

Please sign in to comment.