Skip to content

Commit e7ad990

Browse files
Sander KnapeElad Ben-Israel
Sander Knape
authored and
Elad Ben-Israel
committed
feat(codebuild): add support for local cache modes (#2529)
Fixes #1956
1 parent cfe46f6 commit e7ad990

File tree

6 files changed

+206
-25
lines changed

6 files changed

+206
-25
lines changed

packages/@aws-cdk/aws-codebuild/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ aws codebuild import-source-credentials --server-type GITHUB --auth-type PERSONA
109109

110110
This source type can be used to build code from a BitBucket repository.
111111

112+
## Caching
113+
114+
You can save time when your project builds by using a cache. A cache can store reusable pieces of your build environment and use them across multiple builds. Your build project can use one of two types of caching: Amazon S3 or local. In general, S3 caching is a good option for small and intermediate build artifacts that are more expensive to build than to download. Local caching is a good option for large intermediate build artifacts because the cache is immediately available on the build host.
115+
116+
### S3 Caching
117+
118+
With S3 caching, the cache is stored in an S3 bucket which is available from multiple hosts.
119+
120+
```typescript
121+
new codebuild.Project(this, 'Project', {
122+
source: new codebuild.CodePipelineSource(),
123+
cache: codebuild.Cache.bucket(new Bucket(this, 'Bucket'))
124+
});
125+
```
126+
127+
### Local Caching
128+
129+
With local caching, the cache is stored on the codebuild instance itself. CodeBuild cannot guarantee a reuse of instance. For example, when a build starts and caches files locally, if two subsequent builds start at the same time afterwards only one of those builds would get the cache. Three different cache modes are supported:
130+
131+
* `LocalCacheMode.Source` caches Git metadata for primary and secondary sources.
132+
* `LocalCacheMode.DockerLayer` caches existing Docker layers.
133+
* `LocalCacheMode.Custom` caches directories you specify in the buildspec file.
134+
135+
```typescript
136+
new codebuild.Project(this, 'Project', {
137+
source: new codebuild.CodePipelineSource(),
138+
cache: codebuild.Cache.local(LocalCacheMode.DockerLayer, LocalCacheMode.Custom)
139+
});
140+
```
141+
112142
## Environment
113143

114144
By default, projects use a small instance with an Ubuntu 18.04 image. You
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { IBucket } from "@aws-cdk/aws-s3";
2+
import { Aws, Fn } from "@aws-cdk/cdk";
3+
import { CfnProject } from "./codebuild.generated";
4+
import { IProject } from "./project";
5+
6+
export interface BucketCacheOptions {
7+
/**
8+
* The prefix to use to store the cache in the bucket
9+
*/
10+
readonly prefix?: string;
11+
}
12+
13+
/**
14+
* Local cache modes to enable for the CodeBuild Project
15+
*/
16+
export enum LocalCacheMode {
17+
/**
18+
* Caches Git metadata for primary and secondary sources
19+
*/
20+
Source = 'LOCAL_SOURCE_CACHE',
21+
22+
/**
23+
* Caches existing Docker layers
24+
*/
25+
DockerLayer = 'LOCAL_DOCKER_LAYER_CACHE',
26+
27+
/**
28+
* Caches directories you specify in the buildspec file
29+
*/
30+
Custom = 'LOCAL_CUSTOM_CACHE',
31+
}
32+
33+
/**
34+
* Cache options for CodeBuild Project.
35+
* A cache can store reusable pieces of your build environment and use them across multiple builds.
36+
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-caching.html
37+
*/
38+
export abstract class Cache {
39+
public static none(): Cache {
40+
return { _toCloudFormation: () => undefined, _bind: () => { return; } };
41+
}
42+
43+
/**
44+
* Create a local caching strategy.
45+
* @param modes the mode(s) to enable for local caching
46+
*/
47+
public static local(...modes: LocalCacheMode[]): Cache {
48+
return {
49+
_toCloudFormation: () => ({
50+
type: 'LOCAL',
51+
modes
52+
}),
53+
_bind: () => { return; }
54+
};
55+
}
56+
57+
/**
58+
* Create an S3 caching strategy.
59+
* @param bucket the S3 bucket to use for caching
60+
* @param options additional options to pass to the S3 caching
61+
*/
62+
public static bucket(bucket: IBucket, options?: BucketCacheOptions): Cache {
63+
return {
64+
_toCloudFormation: () => ({
65+
type: 'S3',
66+
location: Fn.join('/', [bucket.bucketName, options && options.prefix || Aws.noValue])
67+
}),
68+
_bind: (project) => {
69+
bucket.grantReadWrite(project);
70+
}
71+
};
72+
}
73+
74+
/**
75+
* @internal
76+
*/
77+
public abstract _toCloudFormation(): CfnProject.ProjectCacheProperty | undefined;
78+
79+
/**
80+
* @internal
81+
*/
82+
public abstract _bind(project: IProject): void;
83+
}

packages/@aws-cdk/aws-codebuild/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './pipeline-project';
22
export * from './project';
33
export * from './source';
44
export * from './artifacts';
5+
export * from './cache';
56

67
// AWS::CodeBuild CloudFormation Resources:
78
export * from './codebuild.generated';

packages/@aws-cdk/aws-codebuild/lib/project.ts

+12-23
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import ecr = require('@aws-cdk/aws-ecr');
66
import events = require('@aws-cdk/aws-events');
77
import iam = require('@aws-cdk/aws-iam');
88
import kms = require('@aws-cdk/aws-kms');
9-
import s3 = require('@aws-cdk/aws-s3');
10-
import { Aws, CfnOutput, Construct, Fn, IResource, Resource, Token } from '@aws-cdk/cdk';
9+
import { Aws, CfnOutput, Construct, IResource, Resource, Token } from '@aws-cdk/cdk';
1110
import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts';
11+
import { Cache } from './cache';
1212
import { CfnProject } from './codebuild.generated';
1313
import { BuildSource, NoSource, SourceType } from './source';
1414

@@ -392,21 +392,16 @@ export interface CommonProjectProps {
392392
readonly role?: iam.IRole;
393393

394394
/**
395-
* Encryption key to use to read and write artifacts
395+
* Encryption key to use to read and write artifacts.
396396
* If not specified, a role will be created.
397397
*/
398398
readonly encryptionKey?: kms.IEncryptionKey;
399399

400400
/**
401-
* Bucket to store cached source artifacts
402-
* If not specified, source artifacts will not be cached.
401+
* Caching strategy to use.
402+
* @default Cache.none
403403
*/
404-
readonly cacheBucket?: s3.IBucket;
405-
406-
/**
407-
* Subdirectory to store cached artifacts
408-
*/
409-
readonly cacheDir?: string;
404+
readonly cache?: Cache;
410405

411406
/**
412407
* Build environment to use for the build.
@@ -618,17 +613,6 @@ export class Project extends ProjectBase {
618613
});
619614
this.grantPrincipal = this.role;
620615

621-
let cache: CfnProject.ProjectCacheProperty | undefined;
622-
if (props.cacheBucket) {
623-
const cacheDir = props.cacheDir != null ? props.cacheDir : Aws.noValue;
624-
cache = {
625-
type: 'S3',
626-
location: Fn.join('/', [props.cacheBucket.bucketName, cacheDir]),
627-
};
628-
629-
props.cacheBucket.grantReadWrite(this.role);
630-
}
631-
632616
this.buildImage = (props.environment && props.environment.buildImage) || LinuxBuildImage.STANDARD_1_0;
633617

634618
// let source "bind" to the project. this usually involves granting permissions
@@ -639,6 +623,11 @@ export class Project extends ProjectBase {
639623
const artifacts = this.parseArtifacts(props);
640624
artifacts._bind(this);
641625

626+
const cache = props.cache || Cache.none();
627+
628+
// give the caching strategy the option to grant permissions to any required resources
629+
cache._bind(this);
630+
642631
// Inject download commands for asset if requested
643632
const environmentVariables = props.environmentVariables || {};
644633
const buildSpec = props.buildSpec || {};
@@ -696,7 +685,7 @@ export class Project extends ProjectBase {
696685
environment: this.renderEnvironment(props.environment, environmentVariables),
697686
encryptionKey: props.encryptionKey && props.encryptionKey.keyArn,
698687
badgeEnabled: props.badge,
699-
cache,
688+
cache: cache._toCloudFormation(),
700689
name: props.projectName,
701690
timeoutInMinutes: props.timeout,
702691
secondarySources: new Token(() => this.renderSecondarySources()),

packages/@aws-cdk/aws-codebuild/test/integ.caching.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import s3 = require('@aws-cdk/aws-s3');
33
import cdk = require('@aws-cdk/cdk');
44
import codebuild = require('../lib');
5+
import { Cache } from '../lib/cache';
56

67
const app = new cdk.App();
78

@@ -12,7 +13,7 @@ const bucket = new s3.Bucket(stack, 'CacheBucket', {
1213
});
1314

1415
new codebuild.Project(stack, 'MyProject', {
15-
cacheBucket: bucket,
16+
cache: Cache.bucket(bucket),
1617
buildSpec: {
1718
build: {
1819
commands: ['echo Hello']

packages/@aws-cdk/aws-codebuild/test/test.project.ts

+78-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert';
1+
import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert';
22
import assets = require('@aws-cdk/assets');
3+
import { Bucket } from '@aws-cdk/aws-s3';
34
import cdk = require('@aws-cdk/cdk');
45
import { Test } from 'nodeunit';
56
import codebuild = require('../lib');
7+
import { Cache, LocalCacheMode } from '../lib/cache';
68

79
// tslint:disable:object-literal-key-quotes
810

@@ -161,4 +163,79 @@ export = {
161163

162164
test.done();
163165
},
166+
167+
'project with s3 cache bucket'(test: Test) {
168+
// GIVEN
169+
const stack = new cdk.Stack();
170+
171+
// WHEN
172+
new codebuild.Project(stack, 'Project', {
173+
source: new codebuild.CodePipelineSource(),
174+
cache: Cache.bucket(new Bucket(stack, 'Bucket'), {
175+
prefix: "cache-prefix"
176+
})
177+
});
178+
179+
// THEN
180+
expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', {
181+
Cache: {
182+
Type: "S3",
183+
Location: {
184+
"Fn::Join": [
185+
"/",
186+
[
187+
{
188+
"Ref": "Bucket83908E77"
189+
},
190+
"cache-prefix"
191+
]
192+
]
193+
}
194+
},
195+
}));
196+
197+
test.done();
198+
},
199+
200+
'project with local cache modes'(test: Test) {
201+
// GIVEN
202+
const stack = new cdk.Stack();
203+
204+
// WHEN
205+
new codebuild.Project(stack, 'Project', {
206+
source: new codebuild.CodePipelineSource(),
207+
cache: Cache.local(LocalCacheMode.Custom, LocalCacheMode.DockerLayer, LocalCacheMode.Source)
208+
});
209+
210+
// THEN
211+
expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', {
212+
Cache: {
213+
Type: "LOCAL",
214+
Modes: [
215+
"LOCAL_CUSTOM_CACHE",
216+
"LOCAL_DOCKER_LAYER_CACHE",
217+
"LOCAL_SOURCE_CACHE"
218+
]
219+
},
220+
}));
221+
222+
test.done();
223+
},
224+
225+
'project by default has no cache modes'(test: Test) {
226+
// GIVEN
227+
const stack = new cdk.Stack();
228+
229+
// WHEN
230+
new codebuild.Project(stack, 'Project', {
231+
source: new codebuild.CodePipelineSource()
232+
});
233+
234+
// THEN
235+
expect(stack).to(not(haveResourceLike('AWS::CodeBuild::Project', {
236+
Cache: {}
237+
})));
238+
239+
test.done();
240+
},
164241
};

0 commit comments

Comments
 (0)