Skip to content

Commit dcc9e59

Browse files
authored
feat(codepipeline): add ability to not reuse cross-region support Stacks (#18043)
This provides the change proposed in feature request #18018 by adding the new flag. closes #18018 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent e828c22 commit dcc9e59

File tree

4 files changed

+247
-1
lines changed

4 files changed

+247
-1
lines changed

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ export interface PipelineProps {
146146
* @default - false (key rotation is disabled)
147147
*/
148148
readonly enableKeyRotation?: boolean;
149+
150+
/**
151+
* Reuse the same cross region support stack for all pipelines in the App.
152+
*
153+
* @default - true (Use the same support stack for all pipelines in App)
154+
*/
155+
readonly reuseCrossRegionSupportStacks?: boolean;
149156
}
150157

151158
abstract class PipelineBase extends Resource implements IPipeline {
@@ -342,6 +349,7 @@ export class Pipeline extends PipelineBase {
342349
private readonly _crossAccountSupport: { [account: string]: Stack } = {};
343350
private readonly crossAccountKeys: boolean;
344351
private readonly enableKeyRotation?: boolean;
352+
private readonly reuseCrossRegionSupportStacks: boolean;
345353

346354
constructor(scope: Construct, id: string, props: PipelineProps = {}) {
347355
super(scope, id, {
@@ -364,6 +372,8 @@ export class Pipeline extends PipelineBase {
364372
throw new Error("Setting 'enableKeyRotation' to true also requires 'crossAccountKeys' to be enabled");
365373
}
366374

375+
this.reuseCrossRegionSupportStacks = props.reuseCrossRegionSupportStacks ?? true;
376+
367377
// If a bucket has been provided, use it - otherwise, create a bucket.
368378
let propsBucket = this.getArtifactBucketFromProps(props);
369379

@@ -631,7 +641,7 @@ export class Pipeline extends PipelineBase {
631641
}
632642

633643
const app = this.supportScope();
634-
const supportStackId = `cross-region-stack-${pipelineAccount}:${actionRegion}`;
644+
const supportStackId = `cross-region-stack-${this.reuseCrossRegionSupportStacks ? pipelineAccount : pipelineStack.stackName}:${actionRegion}`;
635645
let supportStack = app.node.tryFindChild(supportStackId) as CrossRegionSupportStack;
636646
if (!supportStack) {
637647
supportStack = new CrossRegionSupportStack(app, supportStackId, {

packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts

+100
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import * as codepipeline from '../lib';
99
import { FakeBuildAction } from './fake-build-action';
1010
import { FakeSourceAction } from './fake-source-action';
1111

12+
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
13+
// eslint-disable-next-line no-duplicate-imports, import/order
14+
import { Construct } from 'constructs';
15+
1216
/* eslint-disable quote-props */
1317

1418
describe('', () => {
@@ -335,7 +339,61 @@ describe('', () => {
335339
expect(supportStackArtifact.cloudFormationExecutionRoleArn).toEqual(
336340
'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2');
337341

342+
});
343+
344+
test('generates the same support stack containing the replication Bucket without the need to bootstrap in that environment for multiple pipelines', () => {
345+
const app = new cdk.App();
346+
347+
new ReusePipelineStack(app, 'PipelineStackA', {
348+
env: { region: 'us-west-2', account: '123456789012' },
349+
});
350+
new ReusePipelineStack(app, 'PipelineStackB', {
351+
env: { region: 'us-west-2', account: '123456789012' },
352+
});
353+
354+
const assembly = app.synth();
355+
// 2 Pipeline Stacks and 1 support stack for both pipeline stacks.
356+
expect(assembly.stacks.length).toEqual(3);
357+
assembly.getStackByName('PipelineStackA-support-eu-south-1');
358+
expect(() => {
359+
assembly.getStackByName('PipelineStackB-support-eu-south-1');
360+
}).toThrowError(/Unable to find stack with stack name/);
361+
362+
});
363+
364+
test('generates the unique support stack containing the replication Bucket without the need to bootstrap in that environment for multiple pipelines', () => {
365+
const app = new cdk.App();
366+
367+
new ReusePipelineStack(app, 'PipelineStackA', {
368+
env: { region: 'us-west-2', account: '123456789012' },
369+
reuseCrossRegionSupportStacks: false,
370+
});
371+
new ReusePipelineStack(app, 'PipelineStackB', {
372+
env: { region: 'us-west-2', account: '123456789012' },
373+
reuseCrossRegionSupportStacks: false,
374+
});
375+
376+
const assembly = app.synth();
377+
// 2 Pipeline Stacks and 1 support stack for each pipeline stack.
378+
expect(assembly.stacks.length).toEqual(4);
379+
const supportStackAArtifact = assembly.getStackByName('PipelineStackA-support-eu-south-1');
380+
const supportStackBArtifact = assembly.getStackByName('PipelineStackB-support-eu-south-1');
381+
382+
const supportStackATemplate = supportStackAArtifact.template;
383+
expect(supportStackATemplate).toHaveResourceLike('AWS::S3::Bucket', {
384+
BucketName: 'pipelinestacka-support-eueplicationbucket8934e91f26961aa6cbfa',
385+
});
386+
expect(supportStackATemplate).toHaveResourceLike('AWS::KMS::Alias', {
387+
AliasName: 'alias/pport-eutencryptionalias02f1cda3732942f6c529',
388+
});
338389

390+
const supportStackBTemplate = supportStackBArtifact.template;
391+
expect(supportStackBTemplate).toHaveResourceLike('AWS::S3::Bucket', {
392+
BucketName: 'pipelinestackb-support-eueplicationbucketdf7c0e10245faa377228',
393+
});
394+
expect(supportStackBTemplate).toHaveResourceLike('AWS::KMS::Alias', {
395+
AliasName: 'alias/pport-eutencryptionaliasdef3fd3fec63bc54980e',
396+
});
339397
});
340398
});
341399

@@ -466,3 +524,45 @@ describe('test with shared setup', () => {
466524
});
467525
});
468526
});
527+
528+
529+
interface ReusePipelineStackProps extends cdk.StackProps {
530+
reuseCrossRegionSupportStacks?: boolean;
531+
}
532+
533+
class ReusePipelineStack extends cdk.Stack {
534+
public constructor(scope: Construct, id: string, props: ReusePipelineStackProps ) {
535+
super(scope, id, props);
536+
const sourceOutput = new codepipeline.Artifact();
537+
const buildOutput = new codepipeline.Artifact();
538+
new codepipeline.Pipeline(this, 'Pipeline', {
539+
reuseCrossRegionSupportStacks: props.reuseCrossRegionSupportStacks,
540+
stages: [
541+
{
542+
stageName: 'Source',
543+
actions: [new FakeSourceAction({
544+
actionName: 'Source',
545+
output: sourceOutput,
546+
})],
547+
},
548+
{
549+
stageName: 'Build',
550+
actions: [new FakeBuildAction({
551+
actionName: 'Build',
552+
input: sourceOutput,
553+
region: 'eu-south-1',
554+
output: buildOutput,
555+
})],
556+
},
557+
{
558+
stageName: 'Deploy',
559+
actions: [new FakeBuildAction({
560+
actionName: 'Deploy',
561+
input: buildOutput,
562+
region: 'eu-south-1',
563+
})],
564+
},
565+
],
566+
});
567+
}
568+
}

packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline.ts

+8
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,13 @@ export interface CodePipelineProps {
197197
* @default - a new underlying pipeline is created.
198198
*/
199199
readonly codePipeline?: cp.Pipeline;
200+
201+
/**
202+
* Reuse the same cross region support stack for all pipelines in the App.
203+
*
204+
* @default - true (Use the same support stack for all pipelines in App)
205+
*/
206+
readonly reuseCrossRegionSupportStacks?: boolean;
200207
}
201208

202209
/**
@@ -341,6 +348,7 @@ export class CodePipeline extends PipelineBase {
341348
this._pipeline = new cp.Pipeline(this, 'Pipeline', {
342349
pipelineName: this.props.pipelineName,
343350
crossAccountKeys: this.props.crossAccountKeys ?? false,
351+
reuseCrossRegionSupportStacks: this.props.reuseCrossRegionSupportStacks,
344352
// This is necessary to make self-mutation work (deployments are guaranteed
345353
// to happen only after the builds of the latest pipeline definition).
346354
restartExecutionOnUpdate: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as ccommit from '@aws-cdk/aws-codecommit';
2+
import * as sqs from '@aws-cdk/aws-sqs';
3+
import * as cdk from '@aws-cdk/core';
4+
import { Construct } from 'constructs';
5+
import * as cdkp from '../../lib';
6+
import { PIPELINE_ENV, TestApp } from '../testhelpers';
7+
8+
let app: TestApp;
9+
10+
beforeEach(() => {
11+
app = new TestApp();
12+
});
13+
14+
afterEach(() => {
15+
app.cleanup();
16+
});
17+
18+
const testStageEnv: Required<cdk.Environment> = {
19+
account: '123456789012',
20+
region: 'us-east-1',
21+
};
22+
23+
describe('CodePipeline support stack reuse', () => {
24+
test('CodePipeline generates the same support stack containing the replication Bucket without the need to bootstrap in that environment for multiple pipelines', () => {
25+
new ReuseCodePipelineStack(app, 'PipelineStackA', {
26+
env: PIPELINE_ENV,
27+
});
28+
new ReuseCodePipelineStack(app, 'PipelineStackB', {
29+
env: PIPELINE_ENV,
30+
});
31+
const assembly = app.synth();
32+
// 2 Pipeline Stacks and 1 support stack for both pipeline stacks.
33+
expect(assembly.stacks.length).toEqual(3);
34+
assembly.getStackByName(`PipelineStackA-support-${testStageEnv.region}`);
35+
expect(() => {
36+
assembly.getStackByName(`PipelineStackB-support-${testStageEnv.region}`);
37+
}).toThrowError(/Unable to find stack with stack name/);
38+
});
39+
40+
test('CodePipeline generates the unique support stack containing the replication Bucket without the need to bootstrap in that environment for multiple pipelines', () => {
41+
new ReuseCodePipelineStack(app, 'PipelineStackA', {
42+
env: PIPELINE_ENV,
43+
reuseCrossRegionSupportStacks: false,
44+
});
45+
new ReuseCodePipelineStack(app, 'PipelineStackB', {
46+
env: PIPELINE_ENV,
47+
reuseCrossRegionSupportStacks: false,
48+
});
49+
const assembly = app.synth();
50+
// 2 Pipeline Stacks and 1 support stack for each pipeline stack.
51+
expect(assembly.stacks.length).toEqual(4);
52+
const supportStackAArtifact = assembly.getStackByName(`PipelineStackA-support-${testStageEnv.region}`);
53+
const supportStackBArtifact = assembly.getStackByName(`PipelineStackB-support-${testStageEnv.region}`);
54+
55+
const supportStackATemplate = supportStackAArtifact.template;
56+
expect(supportStackATemplate).toHaveResourceLike('AWS::S3::Bucket', {
57+
BucketName: 'pipelinestacka-support-useplicationbucket80db3753a0ebbf052279',
58+
});
59+
expect(supportStackATemplate).toHaveResourceLike('AWS::KMS::Alias', {
60+
AliasName: 'alias/pport-ustencryptionalias5cad45754e1ff088476b',
61+
});
62+
63+
const supportStackBTemplate = supportStackBArtifact.template;
64+
expect(supportStackBTemplate).toHaveResourceLike('AWS::S3::Bucket', {
65+
BucketName: 'pipelinestackb-support-useplicationbucket1d556ec7f959b336abf8',
66+
});
67+
expect(supportStackBTemplate).toHaveResourceLike('AWS::KMS::Alias', {
68+
AliasName: 'alias/pport-ustencryptionalias668c7ffd0de17c9867b0',
69+
});
70+
});
71+
});
72+
73+
interface ReuseCodePipelineStackProps extends cdk.StackProps {
74+
reuseCrossRegionSupportStacks?: boolean;
75+
}
76+
class ReuseCodePipelineStack extends cdk.Stack {
77+
public constructor(scope: Construct, id: string, props: ReuseCodePipelineStackProps ) {
78+
super(scope, id, props);
79+
const repo = new ccommit.Repository(this, 'Repo', {
80+
repositoryName: 'MyRepo',
81+
});
82+
83+
const cdkInput = cdkp.CodePipelineSource.codeCommit(
84+
repo,
85+
'main',
86+
);
87+
88+
const synthStep = new cdkp.ShellStep('Synth', {
89+
input: cdkInput,
90+
installCommands: ['npm ci'],
91+
commands: [
92+
'npm run build',
93+
'npx cdk synth',
94+
],
95+
});
96+
97+
const pipeline = new cdkp.CodePipeline(this, 'Pipeline', {
98+
synth: synthStep,
99+
selfMutation: true,
100+
crossAccountKeys: true,
101+
reuseCrossRegionSupportStacks: props.reuseCrossRegionSupportStacks,
102+
});
103+
104+
const stage = new ReuseStage(
105+
this,
106+
`SampleStage-${testStageEnv.account}-${testStageEnv.region}`,
107+
{
108+
env: testStageEnv,
109+
},
110+
);
111+
pipeline.addStage(stage);
112+
113+
}
114+
}
115+
116+
class ReuseStage extends cdk.Stage {
117+
public constructor(scope: Construct, id: string, props: cdk.StageProps) {
118+
super(scope, id, props);
119+
new ReuseStack(this, 'SampleStack', {});
120+
}
121+
}
122+
123+
class ReuseStack extends cdk.Stack {
124+
public constructor(scope: Construct, id: string, props: cdk.StackProps) {
125+
super(scope, id, props);
126+
new sqs.Queue(this, 'Queue');
127+
}
128+
}

0 commit comments

Comments
 (0)