Skip to content

Commit 933399b

Browse files
author
Saqib Dhuka
committed
StepFunctionsRestApi implemented along with unit and integration testing.
Fixed Integration test and generated expected json for stepFunctionsRestApi Stack deployment. Added code snippet to the README. Removing restApiprops option as the composition in StepFunctionsRestApiProps. closes #15081.
1 parent 00a8063 commit 933399b

File tree

10 files changed

+1070
-2
lines changed

10 files changed

+1070
-2
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ running on AWS Lambda, or any web application.
2222
- [Defining APIs](#defining-apis)
2323
- [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks)
2424
- [AWS Lambda-backed APIs](#aws-lambda-backed-apis)
25+
- [AWS Synchronous State Machine backed APIs](#aws-synchronous-state-machine-backed-APIs)
2526
- [Integration Targets](#integration-targets)
2627
- [Usage Plan & API Keys](#usage-plan--api-keys)
2728
- [Working with models](#working-with-models)
@@ -106,6 +107,19 @@ item.addMethod('GET'); // GET /items/{item}
106107
item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com'));
107108
```
108109

110+
## AWS Synchronous State Machine backed APIs
111+
112+
You can use Amazon API Gateway with AWS Synchronous Express State Machine as the backend integration. The `StepFunctionsRestApi` construct makes this easy and also sets up input, output and error mapping.
113+
114+
The following code defines a REST API that routes all requests to the specified AWS Synchronous Express State Machine:
115+
116+
```ts
117+
declare const stateMachine: stepFunctions.StateMachine;
118+
new apigateway.StepFunctionsRestApi(this, 'StepFunctions-rest-api', {
119+
handler: stateMachine,
120+
});
121+
```
122+
109123
### Breaking up Methods and Resources across Stacks
110124

111125
It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation

packages/@aws-cdk/aws-apigateway/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './authorizers';
2121
export * from './access-log';
2222
export * from './api-definition';
2323
export * from './gateway-response';
24+
export * from './stepFunctions-api';
2425

2526
// AWS::ApiGateway CloudFormation Resources:
2627
export * from './apigateway.generated';

packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './aws';
22
export * from './lambda';
33
export * from './http';
44
export * from './mock';
5+
export * from './stepFunctions';
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Token } from '@aws-cdk/core';
4+
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration';
5+
import { Method } from '../method';
6+
import { AwsIntegration } from './aws';
7+
8+
/**
9+
* StepFunctionsIntegrationOptions
10+
*/
11+
export interface StepFunctionsIntegrationOptions extends IntegrationOptions {
12+
/**
13+
* Use proxy integration or normal (request/response mapping) integration.
14+
*
15+
* @default false
16+
*/
17+
readonly proxy?: boolean;
18+
19+
/**
20+
* Check if cors is enabled
21+
* @default false
22+
*/
23+
readonly corsEnabled?: boolean;
24+
25+
}
26+
/**
27+
* Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method.
28+
*
29+
* @example
30+
*
31+
* const handler = new sfn.StateMachine(this, 'MyStateMachine', ...);
32+
* api.addMethod('GET', new StepFunctionsIntegration(handler));
33+
*
34+
*/
35+
36+
export class StepFunctionsIntegration extends AwsIntegration {
37+
private readonly handler: sfn.IStateMachine;
38+
39+
constructor(handler: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { }) {
40+
41+
const integResponse = getIntegrationResponse();
42+
const requestTemplate = getRequestTemplates(handler);
43+
44+
if (options.corsEnabled) {
45+
super({
46+
proxy: options.proxy,
47+
service: 'states',
48+
action: 'StartSyncExecution',
49+
options,
50+
});
51+
} else {
52+
super({
53+
proxy: options.proxy,
54+
service: 'states',
55+
action: 'StartSyncExecution',
56+
options: {
57+
credentialsRole: options.credentialsRole,
58+
integrationResponses: integResponse,
59+
passthroughBehavior: PassthroughBehavior.NEVER,
60+
requestTemplates: requestTemplate,
61+
},
62+
});
63+
}
64+
65+
this.handler = handler;
66+
}
67+
68+
public bind(method: Method): IntegrationConfig {
69+
const bindResult = super.bind(method);
70+
const principal = new iam.ServicePrincipal('apigateway.amazonaws.com');
71+
72+
this.handler.grantExecution(principal, 'states:StartSyncExecution');
73+
74+
let stateMachineName;
75+
76+
if (this.handler instanceof sfn.StateMachine) {
77+
//if not imported, extract the name from the CFN layer to reach the
78+
//literal value if it is given (rather than a token)
79+
stateMachineName = (this.handler.node.defaultChild as sfn.CfnStateMachine).stateMachineName;
80+
} else {
81+
stateMachineName = 'StateMachine-' + (String(this.handler.stack.node.addr).substring(0, 8));
82+
}
83+
84+
let deploymentToken;
85+
86+
if (!Token.isUnresolved(stateMachineName)) {
87+
deploymentToken = JSON.stringify({ stateMachineName });
88+
}
89+
return {
90+
...bindResult,
91+
deploymentToken,
92+
};
93+
94+
}
95+
}
96+
97+
function getIntegrationResponse() {
98+
const errorResponse = [
99+
{
100+
selectionPattern: '4\\d{2}',
101+
statusCode: '400',
102+
responseTemplates: {
103+
'application/json': `{
104+
"error": "Bad input!"
105+
}`,
106+
},
107+
},
108+
{
109+
selectionPattern: '5\\d{2}',
110+
statusCode: '500',
111+
responseTemplates: {
112+
'application/json': '"error": $input.path(\'$.error\')',
113+
},
114+
},
115+
];
116+
117+
const integResponse = [
118+
{
119+
statusCode: '200',
120+
responseTemplates: {
121+
'application/json': `#set($inputRoot = $input.path('$'))
122+
#if($input.path('$.status').toString().equals("FAILED"))
123+
#set($context.responseOverride.status = 500)
124+
{
125+
"error": "$input.path('$.error')",
126+
"cause": "$input.path('$.cause')"
127+
}
128+
#else
129+
$input.path('$.output')
130+
#end`,
131+
},
132+
},
133+
...errorResponse,
134+
];
135+
136+
return integResponse;
137+
}
138+
139+
function getRequestTemplates(handler: sfn.IStateMachine) {
140+
const templateString = getTemplateString(handler);
141+
142+
const requestTemplate: { [contentType:string] : string } =
143+
{
144+
'application/json': templateString,
145+
};
146+
147+
return requestTemplate;
148+
}
149+
150+
function getTemplateString(handler: sfn.IStateMachine): string {
151+
const templateString: string = `
152+
#set($inputRoot = $input.path('$')) {
153+
"input": "$util.escapeJavaScript($input.json(\'$\'))",
154+
"stateMachineArn": "${handler.stateMachineArn}"
155+
}`;
156+
157+
return templateString;
158+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Construct } from 'constructs';
4+
import { RestApi, RestApiProps } from '.';
5+
import { StepFunctionsIntegration } from './integrations/stepFunctions';
6+
import { Model } from './model';
7+
8+
/**
9+
* StepFunctionsRestApiProps
10+
*/
11+
export interface StepFunctionsRestApiProps extends RestApiProps {
12+
/**
13+
* The default State Machine that handles all requests from this API.
14+
*
15+
* This handler will be used as a the default integration for all methods in
16+
* this API, unless specified otherwise in `addMethod`.
17+
*/
18+
readonly handler: sfn.IStateMachine;
19+
20+
/**
21+
* If true, route all requests to the State Machine
22+
*
23+
* If set to false, you will need to explicitly define the API model using
24+
* `addResource` and `addMethod` (or `addProxy`).
25+
*
26+
* @default true
27+
*/
28+
readonly proxy?: boolean;
29+
}
30+
31+
/**
32+
* Defines an API Gateway REST API with a Synchrounous Express State Machine as a proxy integration.
33+
*
34+
*/
35+
36+
export class StepFunctionsRestApi extends RestApi {
37+
constructor(scope: Construct, id: string, props: StepFunctionsRestApiProps) {
38+
if (props.defaultIntegration) {
39+
throw new Error('Cannot specify "defaultIntegration" since Step Functions integration is automatically defined');
40+
}
41+
42+
const apiRole = getRole(scope, props);
43+
const methodResp = getMethodResponse();
44+
45+
let corsEnabled;
46+
47+
if (props.defaultCorsPreflightOptions !== undefined) {
48+
corsEnabled = true;
49+
} else {
50+
corsEnabled = false;
51+
}
52+
53+
super(scope, id, {
54+
defaultIntegration: new StepFunctionsIntegration(props.handler, {
55+
credentialsRole: apiRole,
56+
proxy: false, //proxy not avaialble for Step Functions yet
57+
corsEnabled: corsEnabled,
58+
}),
59+
...props,
60+
});
61+
62+
this.root.addMethod('ANY', new StepFunctionsIntegration(props.handler, {
63+
credentialsRole: apiRole,
64+
}), {
65+
methodResponses: [
66+
...methodResp,
67+
],
68+
});
69+
}
70+
}
71+
72+
function getRole(scope: Construct, props: StepFunctionsRestApiProps): iam.Role {
73+
const apiName: string = props.handler + '-apiRole';
74+
const apiRole = new iam.Role(scope, apiName, {
75+
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
76+
});
77+
78+
apiRole.attachInlinePolicy(
79+
new iam.Policy(scope, 'AllowStartSyncExecution', {
80+
statements: [
81+
new iam.PolicyStatement({
82+
actions: ['states:StartSyncExecution'],
83+
effect: iam.Effect.ALLOW,
84+
resources: [props.handler.stateMachineArn],
85+
}),
86+
],
87+
}),
88+
);
89+
90+
return apiRole;
91+
}
92+
93+
function getMethodResponse() {
94+
const methodResp = [
95+
{
96+
statusCode: '200',
97+
responseModels: {
98+
'application/json': Model.EMPTY_MODEL,
99+
},
100+
},
101+
{
102+
statusCode: '400',
103+
responseModels: {
104+
'application/json': Model.ERROR_MODEL,
105+
},
106+
},
107+
{
108+
statusCode: '500',
109+
responseModels: {
110+
'application/json': Model.ERROR_MODEL,
111+
},
112+
},
113+
];
114+
115+
return methodResp;
116+
}

packages/@aws-cdk/aws-apigateway/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"@aws-cdk/aws-s3-assets": "0.0.0",
9393
"@aws-cdk/core": "0.0.0",
9494
"@aws-cdk/cx-api": "0.0.0",
95-
"constructs": "^3.3.69"
95+
"constructs": "^3.3.69",
96+
"@aws-cdk/aws-stepfunctions": "0.0.0"
9697
},
9798
"homepage": "https://github.com/aws/aws-cdk",
9899
"peerDependencies": {
@@ -108,7 +109,8 @@
108109
"@aws-cdk/aws-s3-assets": "0.0.0",
109110
"@aws-cdk/core": "0.0.0",
110111
"@aws-cdk/cx-api": "0.0.0",
111-
"constructs": "^3.3.69"
112+
"constructs": "^3.3.69",
113+
"@aws-cdk/aws-stepfunctions": "0.0.0"
112114
},
113115
"engines": {
114116
"node": ">= 10.13.0 <13 || >=13.7.0"
@@ -318,6 +320,7 @@
318320
"attribute-tag:@aws-cdk/aws-apigateway.RequestAuthorizer.authorizerArn",
319321
"attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn",
320322
"attribute-tag:@aws-cdk/aws-apigateway.RestApi.restApiName",
323+
"attribute-tag:@aws-cdk/aws-apigateway.StepFunctionsRestApi.restApiName",
321324
"attribute-tag:@aws-cdk/aws-apigateway.SpecRestApi.restApiName",
322325
"attribute-tag:@aws-cdk/aws-apigateway.LambdaRestApi.restApiName",
323326
"from-method:@aws-cdk/aws-apigateway.Stage",

0 commit comments

Comments
 (0)