From 2e0824b3b2304275573030e295fdf0ccaab75649 Mon Sep 17 00:00:00 2001 From: Markus Lindqvist Date: Tue, 22 Sep 2020 11:07:34 +0300 Subject: [PATCH] feat(pipelines): Allow specifying a VPC for pipelines.CdkPipeline, standardNpmSynth, and standardYarnSynth (#10453) feat(pipelines): Allow specifying a VPC for pipelines.CdkPipeline, standardNpmSynth, and standardYarnSynth. Fixes #9982. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/pipelines/README.md | 3 + .../lib/actions/publish-assets-action.ts | 19 +++++ packages/@aws-cdk/pipelines/lib/pipeline.ts | 23 +++++ .../lib/synths/simple-synth-action.ts | 23 +++++ .../@aws-cdk/pipelines/test/builds.test.ts | 83 +++++++++++++++++++ .../pipelines/test/pipeline-assets.test.ts | 35 +++++++- packages/@aws-cdk/pipelines/test/testutil.ts | 2 + 7 files changed, 187 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 759554c66fd47..350c99386d429 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -135,6 +135,9 @@ class MyPipelineStack extends Stack { sourceArtifact, cloudAssemblyArtifact, + // Optionally specify a VPC in which the action runs + vpc: new ec2.Vpc(this, 'NpmSynthVpc'), + // Use this if you need a build step (if you're not using ts-node // or if you have TypeScript Lambdas that need to be compiled). buildCommand: 'npm run build', diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts index a16862c20d014..ca2bb7221ce87 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts @@ -1,6 +1,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Construct, Lazy } from '@aws-cdk/core'; @@ -59,6 +60,22 @@ export interface PublishAssetsActionProps { * @default - Automatically generated */ readonly role?: iam.IRole; + + /** + * The VPC where to execute the PublishAssetsAction. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; } /** @@ -85,6 +102,8 @@ export class PublishAssetsAction extends Construct implements codepipeline.IActi buildImage: codebuild.LinuxBuildImage.STANDARD_4_0, privileged: (props.assetType === AssetType.DOCKER_IMAGE) ? true : undefined, }, + vpc: props.vpc, + subnetSelection: props.subnetSelection, buildSpec: codebuild.BuildSpec.fromObject({ version: '0.2', phases: { diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index c7ff6b7985888..9643b413eef08 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/pipeline.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Annotations, App, CfnOutput, Construct, PhysicalName, Stack, Stage, Aspects } from '@aws-cdk/core'; import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; @@ -63,6 +64,22 @@ export interface CdkPipelineProps { * @default - Latest version */ readonly cdkCliVersion?: string; + + /** + * The VPC where to execute the CdkPipeline actions. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; } /** @@ -147,6 +164,8 @@ export class CdkPipeline extends Construct { cdkCliVersion: props.cdkCliVersion, pipeline: this._pipeline, projectName: maybeSuffix(props.pipelineName, '-publish'), + vpc: props.vpc, + subnetSelection: props.subnetSelection, }); Aspects.of(this).add({ visit: () => this._assets.removeAssetsStageIfEmpty() }); @@ -294,6 +313,8 @@ interface AssetPublishingProps { readonly pipeline: codepipeline.Pipeline; readonly cdkCliVersion?: string; readonly projectName?: string; + readonly vpc?: ec2.IVpc; + readonly subnetSelection?: ec2.SubnetSelection; } /** @@ -361,6 +382,8 @@ class AssetPublishing extends Construct { cdkCliVersion: this.props.cdkCliVersion, assetType: command.assetType, role: this.assetRoles[command.assetType], + vpc: this.props.vpc, + subnetSelection: this.props.subnetSelection, }); this.stage.addAction(action); } diff --git a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts index 88d36ae81d3eb..785ef9cc46bc7 100644 --- a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { Construct, Stack } from '@aws-cdk/core'; @@ -88,6 +89,22 @@ export interface SimpleSynthOptions { * @default - No policy statements added to CodeBuild Project Role */ readonly rolePolicyStatements?: iam.PolicyStatement[]; + + /** + * The VPC where to execute the SimpleSynth. + * + * @default - No VPC + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets to use. + * + * Only used if 'vpc' is supplied. + * + * @default - All private subnets. + */ + readonly subnetSelection?: ec2.SubnetSelection; } /** @@ -186,6 +203,8 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { ...options, installCommand: options.installCommand ?? 'npm ci', synthCommand: options.synthCommand ?? 'npx cdk synth', + vpc: options.vpc, + subnetSelection: options.subnetSelection, }); } @@ -201,6 +220,8 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { ...options, installCommand: options.installCommand ?? 'yarn install --frozen-lockfile', synthCommand: options.synthCommand ?? 'npx cdk synth', + vpc: options.vpc, + subnetSelection: options.subnetSelection, }); } @@ -314,6 +335,8 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { const project = new codebuild.PipelineProject(scope, 'CdkBuildProject', { projectName: this.props.projectName, environment, + vpc: this.props.vpc, + subnetSelection: this.props.subnetSelection, buildSpec, environmentVariables, }); diff --git a/packages/@aws-cdk/pipelines/test/builds.test.ts b/packages/@aws-cdk/pipelines/test/builds.test.ts index 41c1187105700..fdb9171aedc73 100644 --- a/packages/@aws-cdk/pipelines/test/builds.test.ts +++ b/packages/@aws-cdk/pipelines/test/builds.test.ts @@ -2,6 +2,7 @@ import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture } from '@aw import '@aws-cdk/assert/jest'; import * as cbuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import * as cdkp from '../lib'; @@ -218,6 +219,88 @@ test('Standard (NPM) synth can output additional artifacts', () => { }); }); +test('Standard (NPM) synth can run in a VPC', () => { + // WHEN + new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ + vpc: new ec2.Vpc(pipelineStack, 'NpmSynthTestVpc'), + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56', + }, + { + Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0', + }, + { + Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE', + }, + ], + VpcId: { + Ref: 'NpmSynthTestVpc5E703F25', + }, + }, + }); +}); + +test('Standard (Yarn) synth can run in a VPC', () => { + // WHEN + new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: cdkp.SimpleSynthAction.standardYarnSynth({ + vpc: new ec2.Vpc(pipelineStack, 'YarnSynthTestVpc'), + sourceArtifact, + cloudAssemblyArtifact, + }), + }); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'YarnSynthTestVpcPrivateSubnet1Subnet2805334B', + }, + { + Ref: 'YarnSynthTestVpcPrivateSubnet2SubnetDCFBF596', + }, + { + Ref: 'YarnSynthTestVpcPrivateSubnet3SubnetE11E0C86', + }, + ], + VpcId: { + Ref: 'YarnSynthTestVpc5F654735', + }, + }, + }); +}); + test('Pipeline action contains a hash that changes as the buildspec changes', () => { const hash1 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact: sa, diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts index 499c0834e16b7..13ae9fcceda87 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts @@ -1,9 +1,9 @@ +import * as path from 'path'; import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, stringLike } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import { Construct, Stack, Stage, StageProps } from '@aws-cdk/core'; -import * as path from 'path'; import * as cdkp from '../lib'; import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; @@ -156,6 +156,39 @@ test('docker image asset publishers use privilegedmode, have right AssumeRole', }); }); +test('docker image asset can use a VPC', () => { + // WHEN + pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: objectLike({ + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'CdkAssetsDockerAsset1SecurityGroup078F5C66', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'TestVpcPrivateSubnet1SubnetCC65D771', + }, + { + Ref: 'TestVpcPrivateSubnet2SubnetDE0C64A2', + }, + { + Ref: 'TestVpcPrivateSubnet3Subnet2311D32F', + }, + ], + VpcId: { + Ref: 'TestVpcE77CE678', + }, + }), + }); +}); + test('can control fix/CLI version used in pipeline selfupdate', () => { // WHEN const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); diff --git a/packages/@aws-cdk/pipelines/test/testutil.ts b/packages/@aws-cdk/pipelines/test/testutil.ts index beb6e0180fa87..821b795105365 100644 --- a/packages/@aws-cdk/pipelines/test/testutil.ts +++ b/packages/@aws-cdk/pipelines/test/testutil.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import { App, AppProps, Construct, Environment, SecretValue, Stack, StackProps, Stage } from '@aws-cdk/core'; import * as cdkp from '../lib'; @@ -45,6 +46,7 @@ export class TestGitHubNpmPipeline extends cdkp.CdkPipeline { sourceArtifact, cloudAssemblyArtifact, }), + vpc: new ec2.Vpc(scope, 'TestVpc'), cloudAssemblyArtifact, ...props, });