Skip to content

Commit 56a7cc8

Browse files
committed
Added support for user defined environments in CI/CD pipeline
1 parent 1a92217 commit 56a7cc8

File tree

9 files changed

+237
-52
lines changed

9 files changed

+237
-52
lines changed

.projen/tasks.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework/API.md

Lines changed: 84 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework/LICENSE

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework/src/processing/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,36 @@ You need to also provide the accounts information in the cdk.json in the form of
206206
}
207207
```
208208

209+
## User Defined Stages
210+
211+
To define multiple stages (which can also be deployed in different AWS accounts by following the bootstrap command in the previous section), configure the `cdk.json` file with the following:
212+
213+
```json
214+
{
215+
"environments": [
216+
{
217+
"stageName": "<STAGE_NAME_1>",
218+
"account": "<STAGE_ACCOUNT_ID>",
219+
"region": "<REGION>",
220+
"triggerIntegTest": "<OPTIONAL_BOOLEAN_CAN_BE_OMMITTED>"
221+
},
222+
{
223+
"stageName": "<STAGE_NAME_2>",
224+
"account": "<STAGE_ACCOUNT_ID>",
225+
"region": "<REGION>",
226+
"triggerIntegTest": "<OPTIONAL_BOOLEAN_CAN_BE_OMMITTED>"
227+
},
228+
{
229+
"stageName": "<STAGE_NAME_3>",
230+
"account": "<STAGE_ACCOUNT_ID>",
231+
"region": "<REGION>",
232+
"triggerIntegTest": "<OPTIONAL_BOOLEAN_CAN_BE_OMMITTED>"
233+
}
234+
235+
]
236+
}
237+
```
238+
209239
## Defining a CDK Stack for the Spark application
210240

211241
The `SparkCICDPipeline` construct deploys an application stack, which contains your business logic, into staging and production environments.

framework/src/processing/lib/cicd-pipeline/spark-emr-cicd-pipeline.ts

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RemovalPolicy, ResourceEnvironment } from 'aws-cdk-lib';
55
import { Key } from 'aws-cdk-lib/aws-kms';
66
import { ILogGroup, LogGroup } from 'aws-cdk-lib/aws-logs';
77
import { Bucket, BucketEncryption, IBucket } from 'aws-cdk-lib/aws-s3';
8-
import { CodeBuildStep, CodePipeline } from 'aws-cdk-lib/pipelines';
8+
import { CodeBuildStep, CodePipeline, StageDeployment } from 'aws-cdk-lib/pipelines';
99
import { Construct } from 'constructs';
1010
import { SparkEmrCICDPipelineProps } from './spark-emr-cicd-pipeline-props';
1111
import { AccessLogsBucket } from '../../../storage';
@@ -18,6 +18,15 @@ import {
1818
} from '../../../utils';
1919
import { DEFAULT_SPARK_IMAGE, SparkImage } from '../emr-releases';
2020

21+
/**
22+
* User defined CI/CD environment stages
23+
*/
24+
interface CICDEnvironment {
25+
stageName: string;
26+
account: string;
27+
region: string;
28+
triggerIntegTest?: boolean;
29+
}
2130

2231
/**
2332
* A CICD Pipeline to test and deploy a Spark application on Amazon EMR in cross-account environments using CDK Pipelines.
@@ -211,36 +220,89 @@ export class SparkEmrCICDPipeline extends TrackedConstruct {
211220
},
212221
});
213222

214-
// Create the Staging stage of the CICD
215-
const staging = new ApplicationStage(this, 'Staging', {
216-
env: this.getAccountFromContext('staging'),
223+
try {
224+
const environments = this.getUserDefinedEnvironmentsFromContext();
225+
226+
for (const e of environments) {
227+
this.attachStageToPipeline(e.stageName.toUpperCase(), {
228+
account: e.account,
229+
region: e.region,
230+
}, e.triggerIntegTest || false, buildStage, props);
231+
}
232+
} catch (e) {
233+
[,, this.integrationTestStage] = this.attachStageToPipeline('Staging', this.getAccountFromContext('staging'), true, buildStage, props);
234+
this.attachStageToPipeline('Prod', this.getAccountFromContext('prod'), false, buildStage, props);
235+
}
236+
237+
// // Create the Staging stage of the CICD
238+
// const staging = new ApplicationStage(this, 'Staging', {
239+
// env: this.getAccountFromContext('staging'),
240+
// applicationStackFactory: props.applicationStackFactory,
241+
// outputsEnv: props.integTestEnv,
242+
// stage: CICDStage.STAGING,
243+
// });
244+
// const stagingDeployment = this.pipeline.addStage(staging);
245+
246+
// if (props.integTestScript) {
247+
// // Extract the path and script name from the integration tests script path
248+
// const [integPath, integScript] = SparkEmrCICDPipeline.extractPath(props.integTestScript);
249+
250+
// this.integrationTestStage = new CodeBuildStep('IntegrationTests', {
251+
// input: buildStage.addOutputDirectory(integPath),
252+
// commands: [`chmod +x ${integScript} && ./${integScript}`],
253+
// envFromCfnOutputs: staging.stackOutputsEnv,
254+
// rolePolicyStatements: props.integTestPermissions,
255+
// });
256+
// // Add a post step to run the integration tests
257+
// stagingDeployment.addPost(this.integrationTestStage);
258+
// }
259+
260+
// // Create the Production stage of the CICD
261+
// this.pipeline.addStage(new ApplicationStage(this, 'Production', {
262+
// env: this.getAccountFromContext('prod'),
263+
// applicationStackFactory: props.applicationStackFactory,
264+
// stage: CICDStage.PROD,
265+
// }));
266+
267+
}
268+
269+
/**
270+
* Attaches the given stage to the pipeline
271+
* @param stageName
272+
* @param resourceEnvironment
273+
* @param attachIntegTest
274+
* @param buildStage
275+
* @param props
276+
* @returns {Array} contains array of ApplicationStage, StageDeployment, and optional integration test step
277+
*/
278+
private attachStageToPipeline(stageName: string, resourceEnvironment: ResourceEnvironment
279+
, attachIntegTest: boolean, buildStage: CodeBuildStep
280+
, props: SparkEmrCICDPipelineProps): [ApplicationStage, StageDeployment, CodeBuildStep|undefined] {
281+
const applicationStage = new ApplicationStage(this, stageName, {
282+
env: resourceEnvironment,
217283
applicationStackFactory: props.applicationStackFactory,
218-
outputsEnv: props.integTestEnv,
219-
stage: CICDStage.STAGING,
284+
outputsEnv: (attachIntegTest && props.integTestScript) ? props.integTestEnv : undefined,
285+
stage: CICDStage.of(stageName.toUpperCase()),
220286
});
221-
const stagingDeployment = this.pipeline.addStage(staging);
287+
const stageDeployment = this.pipeline.addStage(applicationStage);
222288

223-
if (props.integTestScript) {
289+
let integrationTestStage:CodeBuildStep|undefined = undefined;
290+
291+
if (attachIntegTest && props.integTestScript) {
224292
// Extract the path and script name from the integration tests script path
225293
const [integPath, integScript] = SparkEmrCICDPipeline.extractPath(props.integTestScript);
226294

227-
this.integrationTestStage = new CodeBuildStep('IntegrationTests', {
295+
integrationTestStage = new CodeBuildStep(`${stageName}-IntegrationTests`, {
228296
input: buildStage.addOutputDirectory(integPath),
229297
commands: [`chmod +x ${integScript} && ./${integScript}`],
230-
envFromCfnOutputs: staging.stackOutputsEnv,
298+
envFromCfnOutputs: applicationStage.stackOutputsEnv,
231299
rolePolicyStatements: props.integTestPermissions,
232300
});
233301
// Add a post step to run the integration tests
234-
stagingDeployment.addPost(this.integrationTestStage);
302+
stageDeployment.addPost(integrationTestStage);
235303
}
236304

237-
// Create the Production stage of the CICD
238-
this.pipeline.addStage(new ApplicationStage(this, 'Production', {
239-
env: this.getAccountFromContext('prod'),
240-
applicationStackFactory: props.applicationStackFactory,
241-
stage: CICDStage.PROD,
242-
}));
243-
305+
return [applicationStage, stageDeployment, integrationTestStage];
244306
}
245307

246308
/**
@@ -251,4 +313,14 @@ export class SparkEmrCICDPipeline extends TrackedConstruct {
251313
if (!account) throw new Error(`Missing context variable ${name}`);
252314
return account;
253315
}
316+
317+
private getUserDefinedEnvironmentsFromContext(): CICDEnvironment[] {
318+
const environments = this.node.tryGetContext('environments') as CICDEnvironment[];
319+
320+
if (!environments) {
321+
throw new Error('Missing context variable environments');
322+
}
323+
324+
return environments;
325+
}
254326
}

framework/src/utils/lib/application-stage.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,31 @@ import { ApplicationStackFactory } from './application-stack-factory';
1010
/**
1111
* The list of CICD Stages used in CICD Pipelines.
1212
*/
13-
export enum CICDStage {
14-
STAGING = 'staging',
15-
PROD = 'prod',
13+
export class CICDStage {
14+
15+
/**
16+
* Prod stage
17+
*/
18+
public static readonly PROD = CICDStage.of('PROD');
19+
20+
/**
21+
* Staging stage
22+
*/
23+
public static readonly STAGING = CICDStage.of('STAGING');
24+
25+
/**
26+
* Custom stage
27+
* @param stage the stage inside the pipeline
28+
* @returns
29+
*/
30+
public static of(stage: string) {
31+
return new CICDStage(stage);
32+
}
33+
34+
/**
35+
* @param stage the stage inside the pipeline
36+
*/
37+
private constructor(public readonly stage: string) {}
1638
}
1739

1840
/**

framework/test/unit/nag/processing/nag-spark-cicd-pipeline.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ NagSuppressions.addResourceSuppressionsByPath(
102102

103103
NagSuppressions.addResourceSuppressionsByPath(
104104
stack,
105-
'/Stack/TestConstruct/CodePipeline/Pipeline/Staging/IntegrationTests/IntegrationTests/Role',
105+
'/Stack/TestConstruct/CodePipeline/Pipeline/Staging/Staging-IntegrationTests/Staging-IntegrationTests/Role',
106106
[{ id: 'AwsSolutions-IAM5', reason: 'This role is provided by CDK Pipeline construt' }],
107107
true,
108108
);

0 commit comments

Comments
 (0)