Skip to content

Commit f2bf322

Browse files
authoredNov 24, 2021
feat(stepfunctions-tasks): add 'Emr on Eks' tasks (#17103)
This CDK feature adds support for Emr on Eks by implementing API service integrations for the following three APIs. This PR adds three tasks which support Emr on Eks: 1) [Create Virtual Cluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) 2) [ Start a job run](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_StartJobRun.html) 3) [Delete virtual cluster ](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_DeleteVirtualCluster.html) Continuation of #15262 by @matthewsvu and @BenChaimberg: Closes #15234. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b432822 commit f2bf322

14 files changed

+6787
-0
lines changed
 

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

+167
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw
6060
- [Cancel Step](#cancel-step)
6161
- [Modify Instance Fleet](#modify-instance-fleet)
6262
- [Modify Instance Group](#modify-instance-group)
63+
- [EMR on EKS](#emr-on-eks)
64+
- [Create Virtual Cluster](#create-virtual-cluster)
65+
- [Delete Virtual Cluster](#delete-virtual-cluster)
66+
- [Start Job Run](#start-job-run)
6367
- [EKS](#eks)
6468
- [Call](#call)
6569
- [EventBridge](#eventbridge)
@@ -783,6 +787,169 @@ new tasks.EmrModifyInstanceGroupByName(this, 'Task', {
783787
});
784788
```
785789

790+
## EMR on EKS
791+
792+
Step Functions supports Amazon EMR on EKS through the service integration pattern.
793+
The service integration APIs correspond to Amazon EMR on EKS APIs, but differ in the parameters that are used.
794+
795+
[Read more](https://docs.aws.amazon.com/step-functions/latest/dg/connect-emr-eks.html) about the differences when using these service integrations.
796+
797+
[Setting up](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up.html) the EKS cluster is required.
798+
799+
### Create Virtual Cluster
800+
801+
The [CreateVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_CreateVirtualCluster.html) API creates a single virtual cluster that's mapped to a single Kubernetes namespace.
802+
803+
The EKS cluster containing the Kubernetes namespace where the virtual cluster will be mapped can be passed in from the task input.
804+
805+
```ts
806+
new tasks.EmrContainersCreateVirtualCluster(this, 'Create a Virtual Cluster', {
807+
eksCluster: tasks.EksClusterInput.fromTaskInput(sfn.TaskInput.fromText('clusterId')),
808+
});
809+
```
810+
811+
The EKS cluster can also be passed in directly.
812+
813+
```ts
814+
import * as eks from '@aws-cdk/aws-eks';
815+
816+
declare const eksCluster: eks.Cluster;
817+
818+
new tasks.EmrContainersCreateVirtualCluster(this, 'Create a Virtual Cluster', {
819+
eksCluster: tasks.EksClusterInput.fromCluster(eksCluster),
820+
});
821+
```
822+
823+
By default, the Kubernetes namespace that a virtual cluster maps to is "default", but a specific namespace within an EKS cluster can be selected.
824+
825+
```ts
826+
new tasks.EmrContainersCreateVirtualCluster(this, 'Create a Virtual Cluster', {
827+
eksCluster: tasks.EksClusterInput.fromTaskInput(sfn.TaskInput.fromText('clusterId')),
828+
eksNamespace: 'specified-namespace',
829+
});
830+
```
831+
832+
### Delete Virtual Cluster
833+
834+
The [DeleteVirtualCluster](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_DeleteVirtualCluster.html) API deletes a virtual cluster.
835+
836+
```ts
837+
new tasks.EmrContainersDeleteVirtualCluster(this, 'Delete a Virtual Cluster', {
838+
virtualClusterId: sfn.TaskInput.fromJsonPathAt('$.virtualCluster'),
839+
});
840+
```
841+
842+
### Start Job Run
843+
844+
The [StartJobRun](https://docs.aws.amazon.com/emr-on-eks/latest/APIReference/API_StartJobRun.html) API starts a job run. A job is a unit of work that you submit to Amazon EMR on EKS for execution. The work performed by the job can be defined by a Spark jar, PySpark script, or SparkSQL query. A job run is an execution of the job on the virtual cluster.
845+
846+
Required setup:
847+
848+
- If not done already, follow the [steps](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up.html) to setup EMR on EKS and [create an EKS Cluster](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-eks-readme.html#quick-start).
849+
- Enable [Cluster access](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-cluster-access.html)
850+
- Enable [IAM Role access](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html)
851+
852+
The following actions must be performed if the virtual cluster ID is supplied from the task input. Otherwise, if it is supplied statically in the state machine definition, these actions will be done automatically.
853+
854+
- Create an [IAM role](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)
855+
- Update the [Role Trust Policy](https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-trust-policy.html) of the Job Execution Role.
856+
857+
The job can be configured with spark submit parameters:
858+
859+
```ts
860+
new tasks.EmrContainersStartJobRun(this, 'EMR Containers Start Job Run', {
861+
virtualCluster: tasks.VirtualClusterInput.fromVirtualClusterId('de92jdei2910fwedz'),
862+
releaseLabel: tasks.ReleaseLabel.EMR_6_2_0,
863+
jobDriver: {
864+
sparkSubmitJobDriver: {
865+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
866+
sparkSubmitParameters: '--conf spark.executor.instances=2 --conf spark.executor.memory=2G --conf spark.executor.cores=2 --conf spark.driver.cores=1',
867+
},
868+
},
869+
});
870+
```
871+
872+
Configuring the job can also be done via application configuration:
873+
874+
```ts
875+
new tasks.EmrContainersStartJobRun(this, 'EMR Containers Start Job Run', {
876+
virtualCluster: tasks.VirtualClusterInput.fromVirtualClusterId('de92jdei2910fwedz'),
877+
releaseLabel: tasks.ReleaseLabel.EMR_6_2_0,
878+
jobName: 'EMR-Containers-Job',
879+
jobDriver: {
880+
sparkSubmitJobDriver: {
881+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
882+
},
883+
},
884+
applicationConfig: [{
885+
classification: tasks.Classification.SPARK_DEFAULTS,
886+
properties: {
887+
'spark.executor.instances': '1',
888+
'spark.executor.memory': '512M',
889+
},
890+
}],
891+
});
892+
```
893+
894+
Job monitoring can be enabled if `monitoring.logging` is set true. This automatically generates an S3 bucket and CloudWatch logs.
895+
896+
```ts
897+
new tasks.EmrContainersStartJobRun(this, 'EMR Containers Start Job Run', {
898+
virtualCluster: tasks.VirtualClusterInput.fromVirtualClusterId('de92jdei2910fwedz'),
899+
releaseLabel: tasks.ReleaseLabel.EMR_6_2_0,
900+
jobDriver: {
901+
sparkSubmitJobDriver: {
902+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
903+
sparkSubmitParameters: '--conf spark.executor.instances=2 --conf spark.executor.memory=2G --conf spark.executor.cores=2 --conf spark.driver.cores=1',
904+
},
905+
},
906+
monitoring: {
907+
logging: true,
908+
},
909+
});
910+
```
911+
912+
Otherwise, providing monitoring for jobs with existing log groups and log buckets is also available.
913+
914+
```ts
915+
import * as logs from '@aws-cdk/aws-logs';
916+
917+
const logGroup = new logs.LogGroup(this, 'Log Group');
918+
const logBucket = new s3.Bucket(this, 'S3 Bucket')
919+
920+
new tasks.EmrContainersStartJobRun(this, 'EMR Containers Start Job Run', {
921+
virtualCluster: tasks.VirtualClusterInput.fromVirtualClusterId('de92jdei2910fwedz'),
922+
releaseLabel: tasks.ReleaseLabel.EMR_6_2_0,
923+
jobDriver: {
924+
sparkSubmitJobDriver: {
925+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
926+
sparkSubmitParameters: '--conf spark.executor.instances=2 --conf spark.executor.memory=2G --conf spark.executor.cores=2 --conf spark.driver.cores=1',
927+
},
928+
},
929+
monitoring: {
930+
logGroup: logGroup,
931+
logBucket: logBucket,
932+
},
933+
});
934+
```
935+
936+
Users can provide their own existing Job Execution Role.
937+
938+
```ts
939+
new tasks.EmrContainersStartJobRun(this, 'EMR Containers Start Job Run', {
940+
virtualCluster:tasks.VirtualClusterInput.fromTaskInput(sfn.TaskInput.fromJsonPathAt('$.VirtualClusterId')),
941+
releaseLabel: tasks.ReleaseLabel.EMR_6_2_0,
942+
jobName: 'EMR-Containers-Job',
943+
executionRole: iam.Role.fromRoleArn(this, 'Job-Execution-Role', 'arn:aws:iam::xxxxxxxxxxxx:role/JobExecutionRole'),
944+
jobDriver: {
945+
sparkSubmitJobDriver: {
946+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
947+
sparkSubmitParameters: '--conf spark.executor.instances=2 --conf spark.executor.memory=2G --conf spark.executor.cores=2 --conf spark.driver.cores=1',
948+
},
949+
},
950+
});
951+
```
952+
786953
## EKS
787954

788955
Step Functions supports Amazon EKS through the service integration pattern.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as eks from '@aws-cdk/aws-eks';
2+
import * as iam from '@aws-cdk/aws-iam';
3+
import * as sfn from '@aws-cdk/aws-stepfunctions';
4+
import { Stack } from '@aws-cdk/core';
5+
import { Construct } from 'constructs';
6+
import { integrationResourceArn, validatePatternSupported } from '../private/task-utils';
7+
8+
/**
9+
* Class for supported types of EMR Containers' Container Providers
10+
*/
11+
enum ContainerProviderTypes {
12+
13+
/**
14+
* Supported container provider type for a EKS Cluster
15+
*/
16+
EKS = 'EKS'
17+
}
18+
19+
/**
20+
* Class that supports methods which return the EKS cluster name depending on input type.
21+
*/
22+
export class EksClusterInput {
23+
24+
/**
25+
* Specify an existing EKS Cluster as the name for this Cluster
26+
*/
27+
static fromCluster(cluster: eks.ICluster): EksClusterInput {
28+
return new EksClusterInput(cluster.clusterName);
29+
}
30+
31+
/**
32+
* Specify a Task Input as the name for this Cluster
33+
*/
34+
static fromTaskInput(taskInput: sfn.TaskInput): EksClusterInput {
35+
return new EksClusterInput(taskInput.value);
36+
}
37+
38+
/**
39+
* Initializes the clusterName
40+
*
41+
* @param clusterName The name of the EKS Cluster
42+
*/
43+
private constructor(readonly clusterName: string) { }
44+
}
45+
46+
/**
47+
* Properties to define a EMR Containers CreateVirtualCluster Task on an EKS cluster
48+
*/
49+
export interface EmrContainersCreateVirtualClusterProps extends sfn.TaskStateBaseProps {
50+
51+
/**
52+
* EKS Cluster or task input that contains the name of the cluster
53+
*/
54+
readonly eksCluster: EksClusterInput;
55+
56+
/**
57+
* The namespace of an EKS cluster
58+
*
59+
* @default - 'default'
60+
*/
61+
readonly eksNamespace?: string;
62+
63+
/**
64+
* Name of the virtual cluster that will be created.
65+
*
66+
* @default - the name of the state machine execution that runs this task and state name
67+
*/
68+
readonly virtualClusterName?: string;
69+
70+
/**
71+
* The tags assigned to the virtual cluster
72+
*
73+
* @default {}
74+
*/
75+
readonly tags?: { [key: string]: string };
76+
}
77+
78+
/**
79+
* Task that creates an EMR Containers virtual cluster from an EKS cluster
80+
*
81+
* @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-emr-eks.html
82+
*/
83+
export class EmrContainersCreateVirtualCluster extends sfn.TaskStateBase {
84+
85+
private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [
86+
sfn.IntegrationPattern.REQUEST_RESPONSE,
87+
];
88+
89+
protected readonly taskMetrics?: sfn.TaskMetricsConfig;
90+
protected readonly taskPolicies?: iam.PolicyStatement[];
91+
92+
private readonly integrationPattern: sfn.IntegrationPattern;
93+
94+
constructor(scope: Construct, id: string, private readonly props: EmrContainersCreateVirtualClusterProps) {
95+
super(scope, id, props);
96+
this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE;
97+
validatePatternSupported(this.integrationPattern, EmrContainersCreateVirtualCluster.SUPPORTED_INTEGRATION_PATTERNS);
98+
99+
this.taskPolicies = this.createPolicyStatements();
100+
}
101+
102+
/**
103+
* @internal
104+
*/
105+
protected _renderTask(): any {
106+
return {
107+
Resource: integrationResourceArn('emr-containers', 'createVirtualCluster', this.integrationPattern),
108+
Parameters: sfn.FieldUtils.renderObject({
109+
Name: this.props.virtualClusterName ?? sfn.JsonPath.stringAt('States.Format(\'{}/{}\', $$.Execution.Name, $$.State.Name)'),
110+
ContainerProvider: {
111+
Id: this.props.eksCluster.clusterName,
112+
Info: {
113+
EksInfo: {
114+
Namespace: this.props.eksNamespace ?? 'default',
115+
},
116+
},
117+
Type: ContainerProviderTypes.EKS,
118+
},
119+
Tags: this.props.tags,
120+
}),
121+
};
122+
};
123+
124+
private createPolicyStatements(): iam.PolicyStatement[] {
125+
return [
126+
new iam.PolicyStatement({
127+
resources: ['*'], // We need * permissions for creating a virtual cluster https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-iam.html
128+
actions: ['emr-containers:CreateVirtualCluster'],
129+
}),
130+
new iam.PolicyStatement({
131+
resources: [
132+
Stack.of(this).formatArn({
133+
service: 'iam',
134+
region: '',
135+
resource: 'role/aws-service-role/emr-containers.amazonaws.com',
136+
resourceName: 'AWSServiceRoleForAmazonEMRContainers',
137+
}),
138+
],
139+
actions: ['iam:CreateServiceLinkedRole'],
140+
conditions: {
141+
StringLike: { 'iam:AWSServiceName': 'emr-containers.amazonaws.com' },
142+
},
143+
}),
144+
];
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import * as cdk from '@aws-cdk/core';
4+
import { Construct } from 'constructs';
5+
import { integrationResourceArn, validatePatternSupported } from '../private/task-utils';
6+
7+
/**
8+
* Properties to define a EMR Containers DeleteVirtualCluster Task
9+
*/
10+
export interface EmrContainersDeleteVirtualClusterProps extends sfn.TaskStateBaseProps {
11+
12+
/**
13+
* The ID of the virtual cluster that will be deleted.
14+
*/
15+
readonly virtualClusterId: sfn.TaskInput;
16+
}
17+
18+
/**
19+
* Deletes an EMR Containers virtual cluster as a Task.
20+
*
21+
* @see https://docs.amazonaws.cn/en_us/step-functions/latest/dg/connect-emr-eks.html
22+
*/
23+
export class EmrContainersDeleteVirtualCluster extends sfn.TaskStateBase {
24+
25+
private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [
26+
sfn.IntegrationPattern.REQUEST_RESPONSE,
27+
sfn.IntegrationPattern.RUN_JOB,
28+
];
29+
30+
protected readonly taskMetrics?: sfn.TaskMetricsConfig;
31+
protected readonly taskPolicies?: iam.PolicyStatement[];
32+
33+
private readonly integrationPattern: sfn.IntegrationPattern;
34+
35+
constructor(scope: Construct, id: string, private readonly props: EmrContainersDeleteVirtualClusterProps) {
36+
super(scope, id, props);
37+
this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE;
38+
39+
validatePatternSupported(this.integrationPattern, EmrContainersDeleteVirtualCluster.SUPPORTED_INTEGRATION_PATTERNS);
40+
41+
this.taskPolicies = this.createPolicyStatements();
42+
}
43+
44+
/**
45+
* @internal
46+
*/
47+
protected _renderTask(): any {
48+
return {
49+
Resource: integrationResourceArn('emr-containers', 'deleteVirtualCluster', this.integrationPattern),
50+
Parameters: sfn.FieldUtils.renderObject({
51+
Id: this.props.virtualClusterId.value,
52+
}),
53+
};
54+
};
55+
56+
private createPolicyStatements(): iam.PolicyStatement[] {
57+
const actions = ['emr-containers:DeleteVirtualCluster'];
58+
if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) {
59+
actions.push('emr-containers:DescribeVirtualCluster');
60+
}
61+
62+
return [new iam.PolicyStatement({
63+
resources: [
64+
cdk.Stack.of(this).formatArn({
65+
arnFormat: cdk.ArnFormat.SLASH_RESOURCE_SLASH_RESOURCE_NAME,
66+
service: 'emr-containers',
67+
resource: 'virtualclusters',
68+
resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.virtualClusterId.value) ? '*' : this.props.virtualClusterId.value,
69+
}),
70+
],
71+
actions: actions,
72+
})];
73+
}
74+
}

‎packages/@aws-cdk/aws-stepfunctions-tasks/lib/emrcontainers/start-job-run.ts

+681
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import subprocess as sp
2+
import os
3+
import logging
4+
5+
#https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-stepfunctions#custom-state
6+
def handler(event, context):
7+
logger = logging.getLogger()
8+
logger.setLevel(logging.INFO)
9+
command = f"/opt/awscli/aws emr-containers update-role-trust-policy --cluster-name {event['ResourceProperties']['eksClusterId']} --namespace {event['ResourceProperties']['eksNamespace']} --role-name {event['ResourceProperties']['roleName']}"
10+
if event['RequestType'] == 'Create' or event['RequestType'] == 'Update' :
11+
try:
12+
res = sp.check_output(command, shell=True)
13+
logger.info(f"Successfully ran {command}")
14+
except Exception as e:
15+
logger.info(f"ERROR: {str(e)}")
16+

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

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export * from './emr/emr-add-step';
2929
export * from './emr/emr-cancel-step';
3030
export * from './emr/emr-modify-instance-fleet-by-name';
3131
export * from './emr/emr-modify-instance-group-by-name';
32+
export * from './emrcontainers/create-virtual-cluster';
33+
export * from './emrcontainers/delete-virtual-cluster';
34+
export * from './emrcontainers/start-job-run';
3235
export * from './glue/run-glue-job-task';
3336
export * from './glue/start-job-run';
3437
export * from './batch/run-batch-job';

‎packages/@aws-cdk/aws-stepfunctions-tasks/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,14 @@
107107
"@aws-cdk/aws-iam": "0.0.0",
108108
"@aws-cdk/aws-kms": "0.0.0",
109109
"@aws-cdk/aws-lambda": "0.0.0",
110+
"@aws-cdk/aws-logs": "0.0.0",
110111
"@aws-cdk/aws-s3": "0.0.0",
111112
"@aws-cdk/aws-sns": "0.0.0",
112113
"@aws-cdk/aws-sqs": "0.0.0",
113114
"@aws-cdk/aws-stepfunctions": "0.0.0",
114115
"@aws-cdk/core": "0.0.0",
116+
"@aws-cdk/custom-resources": "0.0.0",
117+
"@aws-cdk/lambda-layer-awscli": "0.0.0",
115118
"constructs": "^3.3.69"
116119
},
117120
"homepage": "https://github.com/aws/aws-cdk",
@@ -129,11 +132,14 @@
129132
"@aws-cdk/aws-iam": "0.0.0",
130133
"@aws-cdk/aws-kms": "0.0.0",
131134
"@aws-cdk/aws-lambda": "0.0.0",
135+
"@aws-cdk/aws-logs": "0.0.0",
132136
"@aws-cdk/aws-s3": "0.0.0",
133137
"@aws-cdk/aws-sns": "0.0.0",
134138
"@aws-cdk/aws-sqs": "0.0.0",
135139
"@aws-cdk/aws-stepfunctions": "0.0.0",
136140
"@aws-cdk/core": "0.0.0",
141+
"@aws-cdk/custom-resources": "0.0.0",
142+
"@aws-cdk/lambda-layer-awscli": "0.0.0",
137143
"constructs": "^3.3.69"
138144
},
139145
"engines": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import * as eks from '@aws-cdk/aws-eks';
3+
import * as sfn from '@aws-cdk/aws-stepfunctions';
4+
import { Stack } from '@aws-cdk/core';
5+
import { EmrContainersCreateVirtualCluster, EksClusterInput } from '../../lib/emrcontainers/create-virtual-cluster';
6+
7+
const emrContainersVirtualClusterName = 'EMR Containers Virtual Cluster';
8+
let stack: Stack;
9+
let clusterId: string;
10+
11+
beforeEach(() => {
12+
stack = new Stack();
13+
clusterId = 'test-eks';
14+
});
15+
16+
describe('Invoke emr-containers CreateVirtualCluster with ', () => {
17+
test('only required properties', () => {
18+
// WHEN
19+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
20+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
21+
});
22+
23+
new sfn.StateMachine(stack, 'SM', {
24+
definition: task,
25+
});
26+
27+
// THEN
28+
expect(stack.resolve(task.toStateJson())).toEqual({
29+
Type: 'Task',
30+
Resource: {
31+
'Fn::Join': [
32+
'',
33+
[
34+
'arn:',
35+
{
36+
Ref: 'AWS::Partition',
37+
},
38+
':states:::emr-containers:createVirtualCluster',
39+
],
40+
],
41+
},
42+
End: true,
43+
Parameters: {
44+
'Name.$': "States.Format('{}/{}', $$.Execution.Name, $$.State.Name)",
45+
'ContainerProvider': {
46+
Id: clusterId,
47+
Info: {
48+
EksInfo: {
49+
Namespace: 'default',
50+
},
51+
},
52+
Type: 'EKS',
53+
},
54+
},
55+
});
56+
});
57+
58+
test('all required/non-required properties', () => {
59+
// WHEN
60+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
61+
virtualClusterName: emrContainersVirtualClusterName,
62+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
63+
eksNamespace: 'namespace',
64+
integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE,
65+
});
66+
67+
new sfn.StateMachine(stack, 'SM', {
68+
definition: task,
69+
});
70+
71+
// THEN
72+
expect(stack.resolve(task.toStateJson())).toMatchObject({
73+
Parameters: {
74+
Name: emrContainersVirtualClusterName,
75+
ContainerProvider: {
76+
Id: clusterId,
77+
Info: {
78+
EksInfo: {
79+
Namespace: 'namespace',
80+
},
81+
},
82+
},
83+
},
84+
});
85+
});
86+
87+
test('clusterId from payload', () => {
88+
// WHEN
89+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
90+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromJsonPathAt('$.ClusterId')),
91+
});
92+
93+
// THEN
94+
expect(stack.resolve(task.toStateJson())).toMatchObject({
95+
Parameters: {
96+
ContainerProvider: {
97+
'Id.$': '$.ClusterId',
98+
},
99+
},
100+
});
101+
});
102+
103+
test('with an existing EKS cluster', () => {
104+
// WHEN
105+
const eksCluster = new eks.Cluster(stack, 'EKS Cluster', {
106+
version: eks.KubernetesVersion.V1_20,
107+
});
108+
109+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
110+
eksCluster: EksClusterInput.fromCluster(eksCluster),
111+
});
112+
113+
// THEN
114+
expect(stack.resolve(task.toStateJson())).toMatchObject({
115+
Parameters: {
116+
ContainerProvider: {
117+
Id: {
118+
Ref: 'EKSClusterEDAD5FD1',
119+
},
120+
},
121+
},
122+
});
123+
});
124+
125+
test('Tags', () => {
126+
// WHEN
127+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
128+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
129+
tags: {
130+
key: 'value',
131+
},
132+
});
133+
134+
new sfn.StateMachine(stack, 'SM', {
135+
definition: task,
136+
});
137+
138+
// THEN
139+
expect(stack.resolve(task.toStateJson())).toMatchObject({
140+
Parameters: {
141+
Tags: {
142+
key: 'value',
143+
},
144+
},
145+
});
146+
});
147+
});
148+
149+
test('Permitted role actions included for CreateVirtualCluster if service integration pattern is REQUEST_RESPONSE', () => {
150+
// WHEN
151+
const task = new EmrContainersCreateVirtualCluster(stack, 'Task', {
152+
virtualClusterName: emrContainersVirtualClusterName,
153+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
154+
});
155+
156+
new sfn.StateMachine(stack, 'SM', {
157+
definition: task,
158+
});
159+
160+
// THEN
161+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
162+
PolicyDocument: {
163+
Statement: [{
164+
Action: 'emr-containers:CreateVirtualCluster',
165+
Effect: 'Allow',
166+
Resource: '*',
167+
},
168+
{
169+
Action: 'iam:CreateServiceLinkedRole',
170+
Condition: {
171+
StringLike: {
172+
'iam:AWSServiceName': 'emr-containers.amazonaws.com',
173+
},
174+
},
175+
Effect: 'Allow',
176+
Resource: {
177+
'Fn::Join': [
178+
'',
179+
[
180+
'arn:',
181+
{
182+
Ref: 'AWS::Partition',
183+
},
184+
':iam::',
185+
{
186+
Ref: 'AWS::AccountId',
187+
},
188+
':role/aws-service-role/emr-containers.amazonaws.com/AWSServiceRoleForAmazonEMRContainers',
189+
],
190+
],
191+
},
192+
}],
193+
},
194+
});
195+
});
196+
197+
test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => {
198+
expect(() => {
199+
new EmrContainersCreateVirtualCluster(stack, 'EMR Containers CreateVirtualCluster Job', {
200+
virtualClusterName: emrContainersVirtualClusterName,
201+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
202+
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
203+
});
204+
}).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE. Received: WAIT_FOR_TASK_TOKEN/);
205+
});
206+
207+
test('Task throws if RUN_JOB is supplied as service integration pattern', () => {
208+
expect(() => {
209+
new EmrContainersCreateVirtualCluster(stack, 'EMR Containers CreateVirtualCluster Job', {
210+
virtualClusterName: emrContainersVirtualClusterName,
211+
eksCluster: EksClusterInput.fromTaskInput(sfn.TaskInput.fromText(clusterId)),
212+
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
213+
});
214+
}).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE. Received: RUN_JOB/);
215+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Stack } from '@aws-cdk/core';
4+
import { EmrContainersDeleteVirtualCluster } from '../../lib/emrcontainers/delete-virtual-cluster';
5+
6+
let stack: Stack;
7+
let virtualClusterId: string;
8+
9+
beforeEach(() => {
10+
stack = new Stack();
11+
virtualClusterId = 'x01f27i9a7cv1td52keaktr6j';
12+
});
13+
14+
describe('Invoke EMR Containers Delete Virtual cluster with ', () => {
15+
test('a valid cluster ID', () => {
16+
// WHEN
17+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
18+
virtualClusterId: sfn.TaskInput.fromText(virtualClusterId),
19+
});
20+
21+
// THEN
22+
expect(stack.resolve(task.toStateJson())).toEqual({
23+
Type: 'Task',
24+
Resource: {
25+
'Fn::Join': [
26+
'',
27+
[
28+
'arn:',
29+
{
30+
Ref: 'AWS::Partition',
31+
},
32+
':states:::emr-containers:deleteVirtualCluster',
33+
],
34+
],
35+
},
36+
End: true,
37+
Parameters: {
38+
Id: virtualClusterId,
39+
},
40+
});
41+
});
42+
43+
test('a RUN_JOB call', () => {
44+
// WHEN
45+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
46+
virtualClusterId: sfn.TaskInput.fromText(virtualClusterId),
47+
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
48+
});
49+
50+
// THEN
51+
expect(stack.resolve(task.toStateJson())).toEqual({
52+
Type: 'Task',
53+
Resource: {
54+
'Fn::Join': [
55+
'',
56+
[
57+
'arn:',
58+
{
59+
Ref: 'AWS::Partition',
60+
},
61+
':states:::emr-containers:deleteVirtualCluster.sync',
62+
],
63+
],
64+
},
65+
End: true,
66+
Parameters: {
67+
Id: virtualClusterId,
68+
},
69+
});
70+
});
71+
72+
test('passing in JSON Path', () => {
73+
// WHEN
74+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
75+
virtualClusterId: sfn.TaskInput.fromJsonPathAt('$.VirtualClusterId'),
76+
});
77+
78+
// THEN
79+
expect(stack.resolve(task.toStateJson())).toMatchObject({
80+
Parameters: {
81+
'Id.$': '$.VirtualClusterId',
82+
},
83+
});
84+
});
85+
});
86+
87+
describe('Valid policy statements and resources are passed ', () => {
88+
test('to the state machine with a REQUEST_RESPONSE call', () => {
89+
// WHEN
90+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
91+
virtualClusterId: sfn.TaskInput.fromText(virtualClusterId),
92+
});
93+
94+
new sfn.StateMachine(stack, 'SM', {
95+
definition: task,
96+
});
97+
98+
// THEN
99+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
100+
PolicyDocument: {
101+
Statement: [{
102+
Action: 'emr-containers:DeleteVirtualCluster',
103+
Effect: 'Allow',
104+
Resource: {
105+
'Fn::Join': [
106+
'',
107+
[
108+
'arn:',
109+
{
110+
Ref: 'AWS::Partition',
111+
},
112+
':emr-containers:',
113+
{
114+
Ref: 'AWS::Region',
115+
},
116+
':',
117+
{
118+
Ref: 'AWS::AccountId',
119+
},
120+
`:/virtualclusters/${virtualClusterId}`,
121+
],
122+
],
123+
},
124+
}],
125+
},
126+
});
127+
});
128+
129+
test('to the state machine with a RUN_JOB call', () => {
130+
// WHEN
131+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
132+
virtualClusterId: sfn.TaskInput.fromText(virtualClusterId),
133+
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
134+
});
135+
136+
new sfn.StateMachine(stack, 'SM', {
137+
definition: task,
138+
});
139+
140+
// THEN
141+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
142+
PolicyDocument: {
143+
Statement: [{
144+
Action: [
145+
'emr-containers:DeleteVirtualCluster',
146+
'emr-containers:DescribeVirtualCluster',
147+
],
148+
Effect: 'Allow',
149+
Resource: {
150+
'Fn::Join': [
151+
'',
152+
[
153+
'arn:',
154+
{
155+
Ref: 'AWS::Partition',
156+
},
157+
':emr-containers:',
158+
{
159+
Ref: 'AWS::Region',
160+
},
161+
':',
162+
{
163+
Ref: 'AWS::AccountId',
164+
},
165+
`:/virtualclusters/${virtualClusterId}`,
166+
],
167+
],
168+
},
169+
}],
170+
},
171+
});
172+
});
173+
174+
test('when the virtual cluster ID is from a payload', () => {
175+
// WHEN
176+
const task = new EmrContainersDeleteVirtualCluster(stack, 'Task', {
177+
virtualClusterId: sfn.TaskInput.fromJsonPathAt('$.ClusterId'),
178+
integrationPattern: sfn.IntegrationPattern.RUN_JOB,
179+
});
180+
181+
new sfn.StateMachine(stack, 'SM', {
182+
definition: task,
183+
});
184+
185+
// THEN
186+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
187+
PolicyDocument: {
188+
Statement: [{
189+
Action: [
190+
'emr-containers:DeleteVirtualCluster',
191+
'emr-containers:DescribeVirtualCluster',
192+
],
193+
Effect: 'Allow',
194+
Resource: {
195+
'Fn::Join': [
196+
'',
197+
[
198+
'arn:',
199+
{
200+
Ref: 'AWS::Partition',
201+
},
202+
':emr-containers:',
203+
{
204+
Ref: 'AWS::Region',
205+
},
206+
':',
207+
{
208+
Ref: 'AWS::AccountId',
209+
},
210+
':/virtualclusters/*',
211+
],
212+
],
213+
},
214+
}],
215+
},
216+
});
217+
});
218+
});
219+
220+
221+
test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => {
222+
expect(() => {
223+
new EmrContainersDeleteVirtualCluster(stack, 'EMR Containers DeleteVirtualCluster Job', {
224+
virtualClusterId: sfn.TaskInput.fromText(virtualClusterId),
225+
integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN,
226+
});
227+
}).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/);
228+
});

‎packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.job-submission-workflow.expected.json

+1,809
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as ec2 from '@aws-cdk/aws-ec2';
2+
import * as eks from '@aws-cdk/aws-eks';
3+
import * as iam from '@aws-cdk/aws-iam';
4+
import * as sfn from '@aws-cdk/aws-stepfunctions';
5+
import * as cdk from '@aws-cdk/core';
6+
import {
7+
Classification, VirtualClusterInput, EksClusterInput, EmrContainersDeleteVirtualCluster,
8+
EmrContainersCreateVirtualCluster, EmrContainersStartJobRun, ReleaseLabel,
9+
} from '../../lib';
10+
11+
/**
12+
* Stack verification steps:
13+
* Everything in the links below must be setup for the EKS Cluster and Execution Role before running the state machine.
14+
* @see https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-cluster-access.html
15+
* @see https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html
16+
* @see https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-trust-policy.html
17+
*
18+
* aws stepfunctions start-execution --state-machine-arn <deployed state machine arn> : should return execution arn
19+
* aws stepfunctions describe-execution --execution-arn <exection-arn generated before> : should return status as SUCCEEDED
20+
*/
21+
22+
const app = new cdk.App();
23+
const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-emr-containers-all-services-integ');
24+
25+
const eksCluster = new eks.Cluster(stack, 'integration-test-eks-cluster', {
26+
version: eks.KubernetesVersion.V1_21,
27+
defaultCapacity: 3,
28+
defaultCapacityInstance: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.XLARGE),
29+
});
30+
31+
const jobExecutionRole = new iam.Role(stack, 'JobExecutionRole', {
32+
assumedBy: new iam.CompositePrincipal(
33+
new iam.ServicePrincipal('emr-containers.amazonaws.com'),
34+
new iam.ServicePrincipal('states.amazonaws.com'),
35+
),
36+
});
37+
38+
const createVirtualCluster = new EmrContainersCreateVirtualCluster(stack, 'Create a virtual Cluster', {
39+
virtualClusterName: 'Virtual-Cluster-Name',
40+
eksCluster: EksClusterInput.fromCluster(eksCluster),
41+
resultPath: '$.cluster',
42+
});
43+
44+
const startJobRun = new EmrContainersStartJobRun(stack, 'Start a Job Run', {
45+
virtualCluster: VirtualClusterInput.fromTaskInput(sfn.TaskInput.fromJsonPathAt('$.cluster.Id')),
46+
releaseLabel: ReleaseLabel.EMR_6_2_0,
47+
jobName: 'EMR-Containers-Job',
48+
executionRole: iam.Role.fromRoleArn(stack, 'Job-Execution-Role', jobExecutionRole.roleArn),
49+
jobDriver: {
50+
sparkSubmitJobDriver: {
51+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
52+
entryPointArguments: sfn.TaskInput.fromObject(['2']),
53+
sparkSubmitParameters: '--conf spark.driver.memory=512M --conf spark.kubernetes.driver.request.cores=0.2 --conf spark.kubernetes.executor.request.cores=0.2 --conf spark.sql.shuffle.partitions=60 --conf spark.dynamicAllocation.enabled=false',
54+
},
55+
},
56+
monitoring: {
57+
logging: true,
58+
persistentAppUI: true,
59+
},
60+
applicationConfig: [{
61+
classification: Classification.SPARK_DEFAULTS,
62+
properties: {
63+
'spark.executor.instances': '1',
64+
'spark.executor.memory': '512M',
65+
},
66+
}],
67+
resultPath: '$.job',
68+
});
69+
70+
71+
const deleteVirtualCluster = new EmrContainersDeleteVirtualCluster(stack, 'Delete a Virtual Cluster', {
72+
virtualClusterId: sfn.TaskInput.fromJsonPathAt('$.job.VirtualClusterId'),
73+
});
74+
75+
const chain = sfn.Chain
76+
.start(createVirtualCluster)
77+
.next(startJobRun)
78+
.next(deleteVirtualCluster);
79+
80+
const sm = new sfn.StateMachine(stack, 'StateMachine', {
81+
definition: chain,
82+
timeout: cdk.Duration.minutes(20),
83+
});
84+
85+
new cdk.CfnOutput(stack, 'stateMachineArn', {
86+
value: sm.stateMachineArn,
87+
});
88+
89+
90+
app.synth();

‎packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/integ.start-job-run.expected.json

+2,254
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as ec2 from '@aws-cdk/aws-ec2';
2+
import * as eks from '@aws-cdk/aws-eks';
3+
import { AwsAuthMapping } from '@aws-cdk/aws-eks';
4+
import * as iam from '@aws-cdk/aws-iam';
5+
import * as sfn from '@aws-cdk/aws-stepfunctions';
6+
import * as cdk from '@aws-cdk/core';
7+
import { Aws } from '@aws-cdk/core';
8+
import { EmrContainersStartJobRun } from '../../lib';
9+
import { ReleaseLabel, VirtualClusterInput } from '../../lib/emrcontainers/start-job-run';
10+
11+
/**
12+
* Stack verification steps:
13+
* Everything in the link below must be setup before running the state machine.
14+
* @see https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html
15+
* aws stepfunctions start-execution --state-machine-arn <deployed state machine arn> : should return execution arn
16+
* aws stepfunctions describe-execution --execution-arn <exection-arn generated before> : should return status as SUCCEEDED
17+
*/
18+
19+
const app = new cdk.App();
20+
const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-emr-containers-start-job-run-integ-test');
21+
22+
const eksCluster = new eks.Cluster(stack, 'integration-test-eks-cluster', {
23+
version: eks.KubernetesVersion.V1_21,
24+
defaultCapacity: 3,
25+
defaultCapacityInstance: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.XLARGE),
26+
});
27+
28+
const virtualCluster = new cdk.CfnResource(stack, 'Virtual Cluster', {
29+
type: 'AWS::EMRContainers::VirtualCluster',
30+
properties: {
31+
ContainerProvider: {
32+
Id: eksCluster.clusterName,
33+
Info: {
34+
EksInfo: {
35+
Namespace: 'default',
36+
},
37+
},
38+
Type: 'EKS',
39+
},
40+
Name: 'Virtual-Cluster-Name',
41+
},
42+
});
43+
44+
const emrRole = eksCluster.addManifest('emrRole', {
45+
apiVersion: 'rbac.authorization.k8s.io/v1',
46+
kind: 'Role',
47+
metadata: { name: 'emr-containers', namespace: 'default' },
48+
rules: [
49+
{ apiGroups: [''], resources: ['namespaces'], verbs: ['get'] },
50+
{ apiGroups: [''], resources: ['serviceaccounts', 'services', 'configmaps', 'events', 'pods', 'pods/log'], verbs: ['get', 'list', 'watch', 'describe', 'create', 'edit', 'delete', 'deletecollection', 'annotate', 'patch', 'label'] },
51+
{ apiGroups: [''], resources: ['secrets'], verbs: ['create', 'patch', 'delete', 'watch'] },
52+
{ apiGroups: ['apps'], resources: ['statefulsets', 'deployments'], verbs: ['get', 'list', 'watch', 'describe', 'create', 'edit', 'delete', 'annotate', 'patch', 'label'] },
53+
{ apiGroups: ['batch'], resources: ['jobs'], verbs: ['get', 'list', 'watch', 'describe', 'create', 'edit', 'delete', 'annotate', 'patch', 'label'] },
54+
{ apiGroups: ['extensions'], resources: ['ingresses'], verbs: ['get', 'list', 'watch', 'describe', 'create', 'edit', 'delete', 'annotate', 'patch', 'label'] },
55+
{ apiGroups: ['rbac.authorization.k8s.io'], resources: ['roles', 'rolebindings'], verbs: ['get', 'list', 'watch', 'describe', 'create', 'edit', 'delete', 'deletecollection', 'annotate', 'patch', 'label'] },
56+
],
57+
});
58+
59+
const emrRoleBind = eksCluster.addManifest('emrRoleBind', {
60+
apiVersion: 'rbac.authorization.k8s.io/v1',
61+
kind: 'RoleBinding',
62+
metadata: { name: 'emr-containers', namespace: 'default' },
63+
subjects: [{ kind: 'User', name: 'emr-containers', apiGroup: 'rbac.authorization.k8s.io' }],
64+
roleRef: { kind: 'Role', name: 'emr-containers', apiGroup: 'rbac.authorization.k8s.io' },
65+
});
66+
67+
emrRoleBind.node.addDependency(emrRole);
68+
69+
const emrServiceRole = iam.Role.fromRoleArn(stack, 'emrServiceRole', 'arn:aws:iam::'+Aws.ACCOUNT_ID+':role/AWSServiceRoleForAmazonEMRContainers');
70+
const authMapping: AwsAuthMapping = { groups: [], username: 'emr-containers' };
71+
eksCluster.awsAuth.addRoleMapping(emrServiceRole, authMapping);
72+
73+
virtualCluster.node.addDependency(emrRoleBind);
74+
virtualCluster.node.addDependency(eksCluster.awsAuth);
75+
76+
const startJobRunJob = new EmrContainersStartJobRun(stack, 'Start a Job Run', {
77+
virtualCluster: VirtualClusterInput.fromVirtualClusterId(virtualCluster.getAtt('Id').toString()),
78+
releaseLabel: ReleaseLabel.EMR_6_2_0,
79+
jobName: 'EMR-Containers-Job',
80+
jobDriver: {
81+
sparkSubmitJobDriver: {
82+
entryPoint: sfn.TaskInput.fromText('local:///usr/lib/spark/examples/src/main/python/pi.py'),
83+
entryPointArguments: sfn.TaskInput.fromObject(['2']),
84+
sparkSubmitParameters: '--conf spark.driver.memory=512M --conf spark.kubernetes.driver.request.cores=0.2 --conf spark.kubernetes.executor.request.cores=0.2 --conf spark.sql.shuffle.partitions=60 --conf spark.dynamicAllocation.enabled=false',
85+
},
86+
},
87+
});
88+
89+
const chain = sfn.Chain.start(startJobRunJob);
90+
91+
const sm = new sfn.StateMachine(stack, 'StateMachine', {
92+
definition: chain,
93+
timeout: cdk.Duration.seconds(1000),
94+
});
95+
96+
new cdk.CfnOutput(stack, 'stateMachineArn', {
97+
value: sm.stateMachineArn,
98+
});
99+
100+
app.synth();

‎packages/@aws-cdk/aws-stepfunctions-tasks/test/emrcontainers/start-job-run.test.ts

+998
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.