Skip to content

Commit 2adbc14

Browse files
authored
feat(events): API Destinations (#13729)
Adds `Connection` and `ApiDestination` constructs in Events, and an `ApiDestination` target in Events Targets. Can be used to do arbitrary HTTP calls. Fixes #13729 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent bb96621 commit 2adbc14

File tree

14 files changed

+879
-10
lines changed

14 files changed

+879
-10
lines changed

packages/@aws-cdk/aws-events-targets/README.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Currently supported are:
1919
* [Start a CodePipeline pipeline](#start-a-codepipeline-pipeline)
2020
* Run an ECS task
2121
* [Invoke a Lambda function](#invoke-a-lambda-function)
22-
* [Invoke a API Gateway REST API](#invoke-a-api-gateway-rest-api)
22+
* [Invoke a API Gateway REST API](#invoke-an-api-gateway-rest-api)
2323
* Publish a message to an SNS topic
2424
* Send a message to an SQS queue
2525
* [Start a StepFunctions state machine](#start-a-stepfunctions-state-machine)
@@ -29,6 +29,7 @@ Currently supported are:
2929
* [Log an event into a LogGroup](#log-an-event-into-a-loggroup)
3030
* Put a record to a Kinesis Data Firehose stream
3131
* [Put an event on an EventBridge bus](#put-an-event-on-an-eventbridge-bus)
32+
* [Send an event to EventBridge API Destination](#invoke-an-api-destination)
3233

3334
See the README of the `@aws-cdk/aws-events` library for more information on
3435
EventBridge.
@@ -226,7 +227,7 @@ rule.addTarget(new targets.BatchJob(
226227
));
227228
```
228229

229-
## Invoke a API Gateway REST API
230+
## Invoke an API Gateway REST API
230231

231232
Use the `ApiGateway` target to trigger a REST API.
232233

@@ -267,6 +268,31 @@ rule.addTarget(
267268
)
268269
```
269270

271+
## Invoke an API Destination
272+
273+
Use the `targets.ApiDestination` target to trigger an external API. You need to
274+
create an `events.Connection` and `events.ApiDestination` as well.
275+
276+
The code snippet below creates an external destination that is invoked every hour.
277+
278+
```ts
279+
const connection = new events.Connection(this, 'Connection', {
280+
authorization: events.Authorization.apiKey('x-api-key', SecretValue.secretsManager('ApiSecretName')),
281+
description: 'Connection with API Key x-api-key',
282+
});
283+
284+
const destination = new events.ApiDestination(this, 'Destination', {
285+
connection,
286+
endpoint: 'https://example.com',
287+
description: 'Calling example.com with API key x-api-key',
288+
});
289+
290+
const rule = new events.Rule(this, 'Rule', {
291+
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
292+
targets: [new targets.ApiDestination(destination)],
293+
});
294+
```
295+
270296
## Put an event on an EventBridge bus
271297

272298
Use the `EventBus` target to route event to a different EventBus.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as events from '@aws-cdk/aws-events';
2+
import * as iam from '@aws-cdk/aws-iam';
3+
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';
4+
5+
/**
6+
* Customize the EventBridge Api Destinations Target
7+
*/
8+
export interface ApiDestinationProps extends TargetBaseProps {
9+
/**
10+
* The event to send
11+
*
12+
* @default - the entire EventBridge event
13+
*/
14+
readonly event?: events.RuleTargetInput;
15+
16+
/**
17+
* The role to assume before invoking the target
18+
*
19+
* @default - a new role will be created
20+
*/
21+
readonly eventRole?: iam.IRole;
22+
23+
/**
24+
* Additional headers sent to the API Destination
25+
*
26+
* These are merged with headers specified on the Connection, with
27+
* the headers on the Connection taking precedence.
28+
*
29+
* You can only specify secret values on the Connection.
30+
*
31+
* @default - none
32+
*/
33+
readonly headerParameters?: Record<string, string>;
34+
35+
/**
36+
* Path parameters to insert in place of path wildcards (`*`).
37+
*
38+
* If the API destination has a wilcard in the path, these path parts
39+
* will be inserted in that place.
40+
*
41+
* @default - none
42+
*/
43+
readonly pathParameterValues?: string[]
44+
45+
/**
46+
* Additional query string parameters sent to the API Destination
47+
*
48+
* These are merged with headers specified on the Connection, with
49+
* the headers on the Connection taking precedence.
50+
*
51+
* You can only specify secret values on the Connection.
52+
*
53+
* @default - none
54+
*/
55+
readonly queryStringParameters?: Record<string, string>;
56+
}
57+
58+
/**
59+
* Use an API Destination rule target.
60+
*/
61+
export class ApiDestination implements events.IRuleTarget {
62+
constructor(
63+
private readonly apiDestination: events.IApiDestination,
64+
private readonly props: ApiDestinationProps = {},
65+
) { }
66+
67+
/**
68+
* Returns a RuleTarget that can be used to trigger API destinations
69+
* from an EventBridge event.
70+
*/
71+
public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
72+
const httpParameters: events.CfnRule.HttpParametersProperty | undefined =
73+
!!this.props.headerParameters ??
74+
!!this.props.pathParameterValues ??
75+
!!this.props.queryStringParameters
76+
? {
77+
headerParameters: this.props.headerParameters,
78+
pathParameterValues: this.props.pathParameterValues,
79+
queryStringParameters: this.props.queryStringParameters,
80+
} : undefined;
81+
82+
if (this.props?.deadLetterQueue) {
83+
addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue);
84+
}
85+
86+
return {
87+
...(this.props ? bindBaseTargetConfig(this.props) : {}),
88+
arn: this.apiDestination.apiDestinationArn,
89+
role: this.props?.eventRole ?? singletonEventRole(this.apiDestination, [new iam.PolicyStatement({
90+
resources: [this.apiDestination.apiDestinationArn],
91+
actions: ['events:InvokeApiDestination'],
92+
})]),
93+
input: this.props.event,
94+
targetResource: this.apiDestination,
95+
httpParameters,
96+
};
97+
}
98+
}

packages/@aws-cdk/aws-events-targets/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export * from './kinesis-stream';
1313
export * from './log-group';
1414
export * from './kinesis-firehose-stream';
1515
export * from './api-gateway';
16+
export * from './api-destination';
1617
export * from './util';

packages/@aws-cdk/aws-events-targets/rosetta/default.ts-fixture

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Fixture with packages imported, but nothing else
2-
import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core';
2+
import { Duration, RemovalPolicy, SecretValue, Stack } from '@aws-cdk/core';
33
import { Construct } from 'constructs';
44

55
import * as targets from '@aws-cdk/aws-events-targets';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import * as events from '@aws-cdk/aws-events';
3+
import * as iam from '@aws-cdk/aws-iam';
4+
import { Duration, SecretValue, Stack } from '@aws-cdk/core';
5+
import * as targets from '../../lib';
6+
7+
8+
describe('with basic auth connection', () => {
9+
let stack: Stack;
10+
let connection: events.Connection;
11+
let destination: events.ApiDestination;
12+
let rule: events.Rule;
13+
14+
beforeEach(() => {
15+
stack = new Stack();
16+
connection = new events.Connection(stack, 'Connection', {
17+
authorization: events.Authorization.basic('username', SecretValue.plainText('password')),
18+
description: 'ConnectionDescription',
19+
connectionName: 'testConnection',
20+
});
21+
22+
destination = new events.ApiDestination(stack, 'Destination', {
23+
connection,
24+
endpoint: 'https://endpoint.com',
25+
});
26+
27+
rule = new events.Rule(stack, 'Rule', {
28+
schedule: events.Schedule.rate(Duration.minutes(1)),
29+
});
30+
});
31+
32+
test('use api destination as an eventrule target', () => {
33+
// WHEN
34+
rule.addTarget(new targets.ApiDestination(destination));
35+
36+
// THEN
37+
const template = Template.fromStack(stack);
38+
template.hasResourceProperties('AWS::Events::Rule', {
39+
ScheduleExpression: 'rate(1 minute)',
40+
State: 'ENABLED',
41+
Targets: [
42+
{
43+
Arn: { 'Fn::GetAtt': ['DestinationApiDestinationA879FAE5', 'Arn'] },
44+
Id: 'Target0',
45+
RoleArn: { 'Fn::GetAtt': ['DestinationEventsRole7DA63556', 'Arn'] },
46+
},
47+
],
48+
});
49+
});
50+
51+
test('with an explicit event role', () => {
52+
// WHEN
53+
const eventRole = new iam.Role(stack, 'Role', {
54+
assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
55+
});
56+
rule.addTarget(new targets.ApiDestination(destination, { eventRole }));
57+
58+
// THEN
59+
const template = Template.fromStack(stack);
60+
template.hasResourceProperties('AWS::Events::Rule', {
61+
Targets: [
62+
{
63+
RoleArn: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] },
64+
Id: 'Target0',
65+
},
66+
],
67+
});
68+
});
69+
});

packages/@aws-cdk/aws-events/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ The following targets are supported:
153153
* `targets.SfnStateMachine`: Trigger an AWS Step Functions state machine
154154
* `targets.BatchJob`: Queue an AWS Batch Job
155155
* `targets.AwsApi`: Make an AWS API call
156+
* `targets.ApiGateway`: Invoke an AWS API Gateway
157+
* `targets.ApiDestination`: Make an call to an external destination
156158

157159
### Cross-account and cross-region targets
158160

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { IResource, Resource } from '@aws-cdk/core';
2+
import { Construct } from 'constructs';
3+
import { HttpMethod, IConnection } from './connection';
4+
import { CfnApiDestination } from './events.generated';
5+
6+
/**
7+
* The event API Destination properties
8+
*/
9+
export interface ApiDestinationProps {
10+
/**
11+
* The name for the API destination.
12+
* @default - A unique name will be generated
13+
*/
14+
readonly apiDestinationName?: string;
15+
16+
/**
17+
* A description for the API destination.
18+
*
19+
* @default - none
20+
*/
21+
readonly description?: string;
22+
23+
/**
24+
* The ARN of the connection to use for the API destination
25+
*/
26+
readonly connection: IConnection;
27+
28+
/**
29+
* The URL to the HTTP invocation endpoint for the API destination..
30+
*/
31+
readonly endpoint: string;
32+
33+
/**
34+
* The method to use for the request to the HTTP invocation endpoint.
35+
*
36+
* @default HttpMethod.POST
37+
*/
38+
readonly httpMethod?: HttpMethod;
39+
40+
/**
41+
* The maximum number of requests per second to send to the HTTP invocation endpoint.
42+
*
43+
* @default - Not rate limited
44+
*/
45+
readonly rateLimitPerSecond?: number;
46+
}
47+
48+
/**
49+
* Interface for API Destinations
50+
*/
51+
export interface IApiDestination extends IResource {
52+
/**
53+
* The Name of the Api Destination created.
54+
* @attribute
55+
*/
56+
readonly apiDestinationName: string;
57+
58+
/**
59+
* The ARN of the Api Destination created.
60+
* @attribute
61+
*/
62+
readonly apiDestinationArn: string;
63+
}
64+
65+
/**
66+
* Define an EventBridge Api Destination
67+
*
68+
* @resource AWS::Events::ApiDestination
69+
*/
70+
export class ApiDestination extends Resource implements IApiDestination {
71+
/**
72+
* The Connection to associate with Api Destination
73+
*/
74+
public readonly connection: IConnection;
75+
76+
/**
77+
* The Name of the Api Destination created.
78+
* @attribute
79+
*/
80+
public readonly apiDestinationName: string;
81+
82+
/**
83+
* The ARN of the Api Destination created.
84+
* @attribute
85+
*/
86+
public readonly apiDestinationArn: string;
87+
88+
constructor(scope: Construct, id: string, props: ApiDestinationProps) {
89+
super(scope, id, {
90+
physicalName: props.apiDestinationName,
91+
});
92+
93+
this.connection = props.connection;
94+
95+
let apiDestination = new CfnApiDestination(this, 'ApiDestination', {
96+
connectionArn: this.connection.connectionArn,
97+
description: props.description,
98+
httpMethod: props.httpMethod ?? HttpMethod.POST,
99+
invocationEndpoint: props.endpoint,
100+
invocationRateLimitPerSecond: props.rateLimitPerSecond,
101+
name: this.physicalName,
102+
});
103+
104+
this.apiDestinationName = this.getResourceNameAttribute(apiDestination.ref);
105+
this.apiDestinationArn = apiDestination.attrArn;
106+
}
107+
}

0 commit comments

Comments
 (0)