Skip to content

Commit ae840ff

Browse files
authored
feat(stepfunctions-tasks): AWS SDK service integrations (#16746)
Add support for Step Functions' AWS SDK integrations to call any of the over two hundred AWS services directly from a state machine. See https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html See https://aws.amazon.com/blogs/aws/now-aws-step-functions-supports-200-aws-services-to-enable-easier-workflow-automation/ Closes #16780 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 54472a0 commit ae840ff

File tree

7 files changed

+511
-2
lines changed

7 files changed

+511
-2
lines changed

packages/@aws-cdk/aws-stepfunctions-tasks/README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw
3333
- [API Gateway](#api-gateway)
3434
- [Call REST API Endpoint](#call-rest-api-endpoint)
3535
- [Call HTTP API Endpoint](#call-http-api-endpoint)
36+
- [AWS SDK](#aws-sdk)
3637
- [Athena](#athena)
3738
- [StartQueryExecution](#startqueryexecution)
3839
- [GetQueryExecution](#getqueryexecution)
@@ -205,7 +206,7 @@ const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', {
205206
});
206207
```
207208

208-
You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) with `JsonPath.stringAt()`.
209+
You can also use [intrinsic functions](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html) with `JsonPath.stringAt()`.
209210
Here is an example of starting an Athena query that is dynamically created using the task input:
210211

211212
```ts
@@ -314,6 +315,43 @@ const invokeTask = new tasks.CallApiGatewayHttpApiEndpoint(stack, 'Call HTTP API
314315
});
315316
```
316317

318+
### AWS SDK
319+
320+
Step Functions supports calling [AWS service's API actions](https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html)
321+
through the service integration pattern.
322+
323+
You can use Step Functions' AWS SDK integrations to call any of the over two hundred AWS services
324+
directly from your state machine, giving you access to over nine thousand API actions.
325+
326+
```ts
327+
const getObject = new tasks.CallAwsService(this, 'GetObject', {
328+
service: 's3',
329+
action: 'getObject',
330+
parameters: {
331+
Bucket: myBucket.bucketName,
332+
Key: sfn.JsonPath.stringAt('$.key')
333+
},
334+
iamResources: [myBucket.arnForObjects('*')],
335+
});
336+
```
337+
338+
Use camelCase for actions and PascalCase for parameter names.
339+
340+
The task automatically adds an IAM statement to the state machine role's policy based on the
341+
service and action called. The resources for this statement must be specified in `iamResources`.
342+
343+
Use the `iamAction` prop to manually specify the IAM action name in the case where the IAM
344+
action name does not match with the API service/action name:
345+
346+
```ts
347+
const listBuckets = new tasks.CallAwsService(this, 'ListBuckets', {
348+
service: 's3',
349+
action: 'ListBuckets',
350+
iamResources: ['*'],
351+
iamAction: 's3:ListAllMyBuckets'
352+
});
353+
```
354+
317355
## Athena
318356

319357
Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html) through the service integration pattern.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 { Construct } from 'constructs';
5+
import { integrationResourceArn } from '../private/task-utils';
6+
7+
/**
8+
* Properties for calling an AWS service's API action from your
9+
* state machine.
10+
*
11+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html
12+
*/
13+
export interface CallAwsServiceProps extends sfn.TaskStateBaseProps {
14+
/**
15+
* The AWS service to call.
16+
*
17+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/supported-services-awssdk.html
18+
*/
19+
readonly service: string;
20+
21+
/**
22+
* The API action to call.
23+
*
24+
* Use camelCase.
25+
*/
26+
readonly action: string;
27+
28+
/**
29+
* Parameters for the API action call.
30+
*
31+
* Use PascalCase for the parameter names.
32+
*
33+
* @default - no parameters
34+
*/
35+
readonly parameters?: { [key: string]: any };
36+
37+
/**
38+
* The resources for the IAM statement that will be added to the state
39+
* machine role's policy to allow the state machine to make the API call.
40+
*
41+
* By default the action for this IAM statement will be `service:action`.
42+
*/
43+
readonly iamResources: string[];
44+
45+
/**
46+
* The action for the IAM statement that will be added to the state
47+
* machine role's policy to allow the state machine to make the API call.
48+
*
49+
* Use in the case where the IAM action name does not match with the
50+
* API service/action name, e.g. `s3:ListBuckets` requires `s3:ListAllMyBuckets`.
51+
*
52+
* @default - service:action
53+
*/
54+
readonly iamAction?: string;
55+
}
56+
57+
/**
58+
* A StepFunctions task to call an AWS service API
59+
*/
60+
export class CallAwsService extends sfn.TaskStateBase {
61+
protected readonly taskMetrics?: sfn.TaskMetricsConfig;
62+
protected readonly taskPolicies?: iam.PolicyStatement[];
63+
64+
constructor(scope: Construct, id: string, private readonly props: CallAwsServiceProps) {
65+
super(scope, id, props);
66+
67+
this.taskPolicies = [
68+
new iam.PolicyStatement({
69+
resources: props.iamResources,
70+
// The prefix and the action name are case insensitive
71+
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_action.html
72+
actions: [props.iamAction ?? `${props.service}:${props.action}`],
73+
}),
74+
];
75+
}
76+
77+
/**
78+
* @internal
79+
*/
80+
protected _renderTask(): any {
81+
let service = this.props.service;
82+
83+
if (!Token.isUnresolved(service)) {
84+
service = service.toLowerCase();
85+
}
86+
87+
return {
88+
Resource: integrationResourceArn(
89+
'aws-sdk',
90+
`${service}:${this.props.action}`,
91+
this.props.integrationPattern,
92+
),
93+
Parameters: sfn.FieldUtils.renderObject(this.props.parameters) ?? {}, // Parameters is required for aws-sdk
94+
};
95+
}
96+
}

packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ export * from './databrew/start-job-run';
4747
export * from './eks/call';
4848
export * from './apigateway';
4949
export * from './eventbridge/put-events';
50+
export * from './aws-sdk/call-aws-service';

packages/@aws-cdk/aws-stepfunctions-tasks/lib/private/task-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const resourceArnSuffix: Record<IntegrationPattern, string> = {
2626
[IntegrationPattern.WAIT_FOR_TASK_TOKEN]: '.waitForTaskToken',
2727
};
2828

29-
export function integrationResourceArn(service: string, api: string, integrationPattern: IntegrationPattern): string {
29+
export function integrationResourceArn(service: string, api: string, integrationPattern?: IntegrationPattern): string {
3030
if (!service || !api) {
3131
throw new Error("Both 'service' and 'api' must be provided to build the resource ARN.");
3232
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import '@aws-cdk/assert-internal/jest';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import * as cdk from '@aws-cdk/core';
4+
import * as tasks from '../../lib';
5+
6+
let stack: cdk.Stack;
7+
8+
beforeEach(() => {
9+
// GIVEN
10+
stack = new cdk.Stack();
11+
});
12+
13+
test('CallAwsService task', () => {
14+
// WHEN
15+
const task = new tasks.CallAwsService(stack, 'GetObject', {
16+
service: 's3',
17+
action: 'getObject',
18+
parameters: {
19+
Bucket: 'my-bucket',
20+
Key: sfn.JsonPath.stringAt('$.key'),
21+
},
22+
iamResources: ['*'],
23+
});
24+
25+
new sfn.StateMachine(stack, 'StateMachine', {
26+
definition: task,
27+
});
28+
29+
// THEN
30+
expect(stack.resolve(task.toStateJson())).toEqual({
31+
Type: 'Task',
32+
Resource: {
33+
'Fn::Join': [
34+
'',
35+
[
36+
'arn:',
37+
{
38+
Ref: 'AWS::Partition',
39+
},
40+
':states:::aws-sdk:s3:getObject',
41+
],
42+
],
43+
},
44+
End: true,
45+
Parameters: {
46+
'Bucket': 'my-bucket',
47+
'Key.$': '$.key',
48+
},
49+
});
50+
51+
expect(stack).toHaveResource('AWS::IAM::Policy', {
52+
PolicyDocument: {
53+
Statement: [
54+
{
55+
Action: 's3:getObject',
56+
Effect: 'Allow',
57+
Resource: '*',
58+
},
59+
],
60+
Version: '2012-10-17',
61+
},
62+
});
63+
});
64+
65+
test('with custom IAM action', () => {
66+
// WHEN
67+
const task = new tasks.CallAwsService(stack, 'ListBuckets', {
68+
service: 's3',
69+
action: 'listBuckets',
70+
iamResources: ['*'],
71+
iamAction: 's3:ListAllMyBuckets',
72+
});
73+
74+
new sfn.StateMachine(stack, 'StateMachine', {
75+
definition: task,
76+
});
77+
78+
// THEN
79+
expect(stack.resolve(task.toStateJson())).toEqual({
80+
Type: 'Task',
81+
Resource: {
82+
'Fn::Join': [
83+
'',
84+
[
85+
'arn:',
86+
{
87+
Ref: 'AWS::Partition',
88+
},
89+
':states:::aws-sdk:s3:listBuckets',
90+
],
91+
],
92+
},
93+
End: true,
94+
Parameters: {},
95+
});
96+
97+
expect(stack).toHaveResource('AWS::IAM::Policy', {
98+
PolicyDocument: {
99+
Statement: [
100+
{
101+
Action: 's3:ListAllMyBuckets',
102+
Effect: 'Allow',
103+
Resource: '*',
104+
},
105+
],
106+
Version: '2012-10-17',
107+
},
108+
});
109+
});
110+
111+
test('with unresolved tokens', () => {
112+
// WHEN
113+
const task = new tasks.CallAwsService(stack, 'ListBuckets', {
114+
service: new cdk.CfnParameter(stack, 'Service').valueAsString,
115+
action: new cdk.CfnParameter(stack, 'Action').valueAsString,
116+
iamResources: ['*'],
117+
});
118+
119+
new sfn.StateMachine(stack, 'StateMachine', {
120+
definition: task,
121+
});
122+
123+
// THEN
124+
expect(stack.resolve(task.toStateJson())).toEqual({
125+
Type: 'Task',
126+
Resource: {
127+
'Fn::Join': [
128+
'',
129+
[
130+
'arn:',
131+
{
132+
Ref: 'AWS::Partition',
133+
},
134+
':states:::aws-sdk:',
135+
{
136+
Ref: 'Service',
137+
},
138+
':',
139+
{
140+
Ref: 'Action',
141+
},
142+
],
143+
],
144+
},
145+
End: true,
146+
Parameters: {},
147+
});
148+
});

0 commit comments

Comments
 (0)