Skip to content

Commit

Permalink
feature(codebuild): allow specifying a VPC in Project (aws#1913)
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidChristiansen authored and skinny85 committed Mar 29, 2019
1 parent 6fc179a commit fb9fef2
Show file tree
Hide file tree
Showing 6 changed files with 835 additions and 15 deletions.
48 changes: 48 additions & 0 deletions packages/@aws-cdk/aws-codebuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,51 @@ const project = new codebuild.Project(this, 'MyProject', {
},
});
```

### Definition of VPC configuration in CodeBuild Project

Typically, resources in an VPC are not accessible by AWS CodeBuild. To enable
access, you must provide additional VPC-specific configuration information as
part of your CodeBuild project configuration. This includes the VPC ID, the
VPC subnet IDs, and the VPC security group IDs. VPC-enabled builds are then
able to access resources inside your VPC.

For further Information see https://docs.aws.amazon.com/codebuild/latest/userguide/vpc-support.html

**Use Cases**
VPC connectivity from AWS CodeBuild builds makes it possible to:

* Run integration tests from your build against data in an Amazon RDS database that's isolated on a private subnet.
* Query data in an Amazon ElastiCache cluster directly from tests.
* Interact with internal web services hosted on Amazon EC2, Amazon ECS, or services that use internal Elastic Load Balancing.
* Retrieve dependencies from self-hosted, internal artifact repositories, such as PyPI for Python, Maven for Java, and npm for Node.js.
* Access objects in an Amazon S3 bucket configured to allow access through an Amazon VPC endpoint only.
* Query external web services that require fixed IP addresses through the Elastic IP address of the NAT gateway or NAT instance associated with your subnet(s).

Your builds can access any resource that's hosted in your VPC.

**Enable Amazon VPC Access in your CodeBuild Projects**

Include these settings in your VPC configuration:

* For VPC ID, choose the VPC that CodeBuild uses.
* For Subnets, choose a private subnet SubnetSelection with NAT translation that includes or has routes to the resources used CodeBuild.
* For Security Groups, choose the security groups that CodeBuild uses to allow access to resources in the VPCs.

For example:

```ts
const stack = new cdk.Stack(app, 'aws-cdk-codebuild-project-vpc');
const vpc = new ec2.VpcNetwork(stack, 'MyVPC');
const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup1', {
allowAllOutbound: true,
description: 'Example',
groupName: 'MySecurityGroup',
vpc: vpc,
});
new Project(stack, 'MyProject', {
buildScriptAsset: new assets.ZipDirectoryAsset(stack, 'Bundle', { path: 'script_bundle' }),
securityGroups: [securityGroup],
vpc: vpc
});
```
141 changes: 127 additions & 14 deletions packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assets = require('@aws-cdk/assets');
import { DockerImageAsset, DockerImageAssetProps } from '@aws-cdk/assets-docker';
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import ec2 = require('@aws-cdk/aws-ec2');
import ecr = require('@aws-cdk/aws-ecr');
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
Expand Down Expand Up @@ -195,8 +196,8 @@ export abstract class ProjectBase extends cdk.Construct implements IProject {
const rule = new events.EventRule(this, name, options);
rule.addTarget(target);
rule.addEventPattern({
source: [ 'aws.codebuild' ],
detailType: [ 'CodeBuild Build State Change' ],
source: ['aws.codebuild'],
detailType: ['CodeBuild Build State Change'],
detail: {
'project-name': [
this.projectName
Expand All @@ -216,8 +217,8 @@ export abstract class ProjectBase extends cdk.Construct implements IProject {
const rule = new events.EventRule(this, name, options);
rule.addTarget(target);
rule.addEventPattern({
source: [ 'aws.codebuild' ],
detailType: [ 'CodeBuild Build Phase Change' ],
source: ['aws.codebuild'],
detailType: ['CodeBuild Build Phase Change'],
detail: {
'project-name': [
this.projectName
Expand All @@ -234,7 +235,7 @@ export abstract class ProjectBase extends cdk.Construct implements IProject {
const rule = this.onStateChange(name, target, options);
rule.addEventPattern({
detail: {
'build-status': [ 'IN_PROGRESS' ]
'build-status': ['IN_PROGRESS']
}
});
return rule;
Expand All @@ -247,7 +248,7 @@ export abstract class ProjectBase extends cdk.Construct implements IProject {
const rule = this.onStateChange(name, target, options);
rule.addEventPattern({
detail: {
'build-status': [ 'FAILED' ]
'build-status': ['FAILED']
}
});
return rule;
Expand All @@ -260,7 +261,7 @@ export abstract class ProjectBase extends cdk.Construct implements IProject {
const rule = this.onStateChange(name, target, options);
rule.addEventPattern({
detail: {
'build-status': [ 'SUCCEEDED' ]
'build-status': ['SUCCEEDED']
}
});
return rule;
Expand Down Expand Up @@ -473,8 +474,44 @@ export interface CommonProjectProps {
* The physical, human-readable name of the CodeBuild Project.
*/
readonly projectName?: string;
}

/**
* VPC network to place codebuild network interfaces
*
* Specify this if the codebuild project needs to access resources in a VPC.
*/
readonly vpc?: ec2.IVpcNetwork;

/**
* Where to place the network interfaces within the VPC.
*
* Only used if 'vpc' is supplied.
*
* @default All private subnets
*/
readonly subnetSelection?: ec2.SubnetSelection;

/**
* What security group to associate with the codebuild project's network interfaces.
* If no security group is identified, one will be created automatically.
*
* Only used if 'vpc' is supplied.
*
*/
readonly securityGroups?: ec2.ISecurityGroup[];

/**
* Whether to allow the CodeBuild to send all network traffic
*
* If set to false, you must individually add traffic rules to allow the
* CodeBuild project to connect to network targets.
*
* Only used if 'vpc' is supplied.
*
* @default true
*/
readonly allowAllOutbound?: boolean;
}
export interface ProjectProps extends CommonProjectProps {
/**
* The source of the build.
Expand Down Expand Up @@ -554,6 +591,7 @@ export class Project extends ProjectBase {
private readonly buildImage: IBuildImage;
private readonly _secondarySources: BuildSource[];
private readonly _secondaryArtifacts: BuildArtifacts[];
private _securityGroups: ec2.ISecurityGroup[] = [];

constructor(scope: cdk.Construct, id: string, props: ProjectProps) {
super(scope, id);
Expand Down Expand Up @@ -647,14 +685,19 @@ export class Project extends ProjectBase {
secondarySources: new cdk.Token(() => this.renderSecondarySources()),
secondaryArtifacts: new cdk.Token(() => this.renderSecondaryArtifacts()),
triggers: this.source.buildTriggers(),
vpcConfig: this.configureVpc(props),
});

this.projectArn = resource.projectArn;
this.projectName = resource.ref;
this.projectName = resource.projectName;

this.addToRolePolicy(this.createLoggingPermission());
}

public get securityGroups(): ec2.ISecurityGroup[] {
return this._securityGroups.slice();
}

/**
* Export this Project. Allows referencing this Project in a different CDK Stack.
*/
Expand All @@ -674,6 +717,20 @@ export class Project extends ProjectBase {
}
}

/**
* Add a permission only if there's a policy attached.
* @param statement The permissions statement to add
*/
public addToRoleInlinePolicy(statement: iam.PolicyStatement) {
if (this.role) {
const policy = new iam.Policy(this, 'PolicyDocument', {
policyName: 'CodeBuildEC2Policy',
statements: [statement]
});
this.role.attachInlinePolicy(policy);
}
}

/**
* Adds a secondary source to the Project.
*
Expand Down Expand Up @@ -742,8 +799,7 @@ export class Project extends ProjectBase {
}

private renderEnvironment(env: BuildEnvironment = {},
projectVars: { [name: string]: BuildEnvironmentVariable } = {}):
CfnProject.EnvironmentProperty {
projectVars: { [name: string]: BuildEnvironmentVariable } = {}): CfnProject.EnvironmentProperty {
const vars: { [name: string]: BuildEnvironmentVariable } = {};
const containerVars = env.environmentVariables || {};

Expand Down Expand Up @@ -789,6 +845,63 @@ export class Project extends ProjectBase {
: this._secondaryArtifacts.map((secondaryArtifact) => secondaryArtifact.toArtifactsJSON());
}

/**
* If configured, set up the VPC-related properties
*
* Returns the VpcConfig that should be added to the
* codebuild creation properties.
*/
private configureVpc(props: ProjectProps): CfnProject.VpcConfigProperty | undefined {
if ((props.securityGroups || props.allowAllOutbound !== undefined) && !props.vpc) {
throw new Error(`Cannot configure 'securityGroup' or 'allowAllOutbound' without configuring a VPC`);
}

if (!props.vpc) { return undefined; }

if ((props.securityGroups && props.securityGroups.length > 0) && props.allowAllOutbound !== undefined) {
throw new Error(`Configure 'allowAllOutbound' directly on the supplied SecurityGroup.`);
}

if (props.securityGroups && props.securityGroups.length > 0) {
this._securityGroups = props.securityGroups.slice();
} else {
const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
vpc: props.vpc,
description: 'Automatic generated security group for CodeBuild ' + this.node.uniqueId,
allowAllOutbound: props.allowAllOutbound
});
this._securityGroups = [securityGroup];
}
const subnetSelection: ec2.SubnetSelection = props.subnetSelection ? props.subnetSelection : {
subnetType: ec2.SubnetType.Private
};
this.addToRoleInlinePolicy(new iam.PolicyStatement()
.addAllResources()
.addActions(
'ec2:CreateNetworkInterface',
'ec2:DescribeNetworkInterfaces',
'ec2:DeleteNetworkInterface',
'ec2:DescribeSubnets',
'ec2:DescribeSecurityGroups',
'ec2:DescribeDhcpOptions',
'ec2:DescribeVpcs'
));
this.addToRolePolicy(new iam.PolicyStatement()
.addResource(`arn:aws:ec2:${cdk.Aws.region}:${cdk.Aws.accountId}:network-interface/*`)
.addCondition('StringEquals', {
"ec2:Subnet": [
`arn:aws:ec2:${cdk.Aws.region}:${cdk.Aws.accountId}:subnet/[[subnets]]`
],
"ec2:AuthorizedService": "codebuild.amazonaws.com"
})
.addAction('ec2:CreateNetworkInterfacePermission'));
return {
vpcId: props.vpc.vpcId,
subnets: props.vpc.subnetIds(subnetSelection).map(s => s),
securityGroupIds: this._securityGroups.map(s => s.securityGroupId)
};
}

private parseArtifacts(props: ProjectProps) {
if (props.artifacts) {
return props.artifacts;
Expand All @@ -806,7 +919,7 @@ export class Project extends ProjectBase {

if ((sourceType === CODEPIPELINE_TYPE || artifactsType === CODEPIPELINE_TYPE) &&
(sourceType !== artifactsType)) {
throw new Error('Both source and artifacts must be set to CodePipeline');
throw new Error('Both source and artifacts must be set to CodePipeline');
}
}
}
Expand All @@ -815,9 +928,9 @@ export class Project extends ProjectBase {
* Build machine compute type.
*/
export enum ComputeType {
Small = 'BUILD_GENERAL1_SMALL',
Small = 'BUILD_GENERAL1_SMALL',
Medium = 'BUILD_GENERAL1_MEDIUM',
Large = 'BUILD_GENERAL1_LARGE'
Large = 'BUILD_GENERAL1_LARGE'
}

export interface BuildEnvironment {
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-codebuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@aws-cdk/aws-cloudwatch": "^0.27.0",
"@aws-cdk/aws-codecommit": "^0.27.0",
"@aws-cdk/aws-ecr": "^0.27.0",
"@aws-cdk/aws-ec2": "^0.27.0",
"@aws-cdk/aws-events": "^0.27.0",
"@aws-cdk/aws-iam": "^0.27.0",
"@aws-cdk/aws-kms": "^0.27.0",
Expand All @@ -90,6 +91,7 @@
"@aws-cdk/aws-cloudwatch": "^0.27.0",
"@aws-cdk/aws-codecommit": "^0.27.0",
"@aws-cdk/aws-ecr": "^0.27.0",
"@aws-cdk/aws-ec2": "^0.27.0",
"@aws-cdk/aws-events": "^0.27.0",
"@aws-cdk/aws-iam": "^0.27.0",
"@aws-cdk/aws-kms": "^0.27.0",
Expand Down
Loading

0 comments on commit fb9fef2

Please sign in to comment.