Skip to content
This repository was archived by the owner on Jul 16, 2024. It is now read-only.

Commit 4705a16

Browse files
flochazFlorian Chazal
and
Florian Chazal
authored
chore(cdkDeployer): Add custom build spec option (#527)
Co-authored-by: Florian Chazal <[email protected]>
1 parent 038b688 commit 4705a16

File tree

4 files changed

+289
-6
lines changed

4 files changed

+289
-6
lines changed

core/API.md

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

core/src/common/cdk-deployer-build.ts

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,161 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT-0
33

4+
import { BuildSpec } from "aws-cdk-lib/aws-codebuild";
5+
6+
const defaultDestroyBuildSpec = `
7+
version: 0.2
8+
env:
9+
variables:
10+
CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET
11+
CFN_STACK_ID: CFN_STACK_ID_NOT_SET
12+
CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET
13+
CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET
14+
phases:
15+
pre_build:
16+
on-failure: ABORT
17+
commands:
18+
- echo "Default destroy buildspec"
19+
- cd $CODEBUILD_SRC_DIR/$CDK_APP_LOCATION
20+
- npm install -g aws-cdk && sudo apt-get install python3 && python -m
21+
ensurepip --upgrade && python -m pip install --upgrade pip && python -m
22+
pip install -r requirements.txt
23+
- \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"
24+
- 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'
25+
- cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION
26+
build:
27+
on-failure: ABORT
28+
commands:
29+
- \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"
30+
- 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'
31+
- cdk destroy --force --all --require-approval never
32+
`
33+
34+
const defaultDeployBuildSpec = `
35+
version: 0.2
36+
env:
37+
variables:
38+
CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET
39+
CFN_STACK_ID: CFN_STACK_ID_NOT_SET
40+
CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET
41+
CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET
42+
PARAMETERS: PARAMETERS_NOT_SET
43+
STACKNAME: STACKNAME_NOT_SET
44+
phases:
45+
pre_build:
46+
on-failure: ABORT
47+
commands:
48+
- echo "Default deploy buildspec"
49+
- cd $CODEBUILD_SRC_DIR/$CDK_APP_LOCATION
50+
- npm install -g aws-cdk && sudo apt-get install python3 && python -m
51+
ensurepip --upgrade && python -m pip install --upgrade pip && python -m
52+
pip install -r requirements.txt
53+
- \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"
54+
- 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'
55+
- cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION
56+
build:
57+
on-failure: ABORT
58+
commands:
59+
- \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"
60+
- 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'
61+
- cdk deploy $STACKNAME $PARAMETERS --require-approval=never
62+
`
63+
464
// workaround to get a Lambda function with inline code and packaged into the ARA library
565
// We need inline code to ensure it's deployable via a CloudFormation template
666
// TODO modify the PreBundledFunction to allow for inline Lambda in addtion to asset based Lambda
7-
export const startBuild = "const respond = async function(event, context, responseStatus, responseData, physicalResourceId, noEcho) {\n return new Promise((resolve, reject) => {\n var responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: \"See the details in CloudWatch Log Stream: \" + context.logGroupName + \" \" + context.logStreamName,\n PhysicalResourceId: physicalResourceId || context.logStreamName,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: noEcho || false,\n Data: responseData\n });\n \n console.log(\"Response body:\", responseBody);\n \n var https = require(\"https\");\n var url = require(\"url\");\n \n var parsedUrl = url.parse(event.ResponseURL);\n var options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: \"PUT\",\n headers: {\n \"content-type\": \"\",\n \"content-length\": responseBody.length\n }\n };\n \n var request = https.request(options, function(response) {\n console.log(\"Status code: \" + response.statusCode);\n console.log(\"Status message: \" + response.statusMessage);\n resolve();\n });\n \n request.on(\"error\", function(error) {\n console.log(\"respond(..) failed executing https.request(..): \" + error);\n resolve();\n });\n \n request.write(responseBody);\n request.end();\n });\n};\n\nconst AWS = require('aws-sdk');\n\nexports.handler = async function (event, context) {\n console.log(JSON.stringify(event, null, 4));\n try {\n const projectName = event.ResourceProperties.ProjectName;\n const codebuild = new AWS.CodeBuild();\n \n console.log(`Starting new build of project ${projectName}`);\n \n const { build } = await codebuild.startBuild({\n projectName,\n // Pass CFN related parameters through the build for extraction by the\n // completion handler.\n buildspecOverride: event.RequestType === 'Delete' ? \n `\nversion: 0.2\nenv:\n variables:\n CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET\n CFN_STACK_ID: CFN_STACK_ID_NOT_SET\n CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET\n CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET\nphases:\n pre_build:\n on-failure: ABORT\n commands:\n - cd $CODEBUILD_SRC_DIR/$CDK_APP_LOCATION\n - npm install -g aws-cdk && sudo apt-get install python3 && python -m\n ensurepip --upgrade && python -m pip install --upgrade pip && python -m\n pip install -r requirements.txt\n - \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"\n - 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'\n - cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION\n build:\n on-failure: ABORT\n commands:\n - \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"\n - 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'\n - cdk destroy --force --all --require-approval never\n `\n :\n `\nversion: 0.2\nenv:\n variables:\n CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET\n CFN_STACK_ID: CFN_STACK_ID_NOT_SET\n CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET\n CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET\n PARAMETERS: PARAMETERS_NOT_SET\n STACKNAME: STACKNAME_NOT_SET\nphases:\n pre_build:\n on-failure: ABORT\n commands:\n - cd $CODEBUILD_SRC_DIR/$CDK_APP_LOCATION\n - npm install -g aws-cdk && sudo apt-get install python3 && python -m\n ensurepip --upgrade && python -m pip install --upgrade pip && python -m\n pip install -r requirements.txt\n - \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"\n - 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'\n - cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION\n build:\n on-failure: ABORT\n commands:\n - \"export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)\"\n - 'echo \"AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID\"'\n - cdk deploy $STACKNAME $PARAMETERS --require-approval=never\n `,\n environmentVariablesOverride: [\n {\n name: 'CFN_RESPONSE_URL',\n value: event.ResponseURL\n },\n {\n name: 'CFN_STACK_ID',\n value: event.StackId\n },\n {\n name: 'CFN_REQUEST_ID',\n value: event.RequestId\n },\n {\n name: 'CFN_LOGICAL_RESOURCE_ID',\n value: event.LogicalResourceId\n },\n {\n name: 'BUILD_ROLE_ARN',\n value: event.ResourceProperties.BuildRoleArn\n }\n ]\n }).promise();\n console.log(`Build id ${build.id} started - resource completion handled by EventBridge`);\n } catch(error) {\n console.error(error);\n await respond(event, context, 'FAILED', { Error: error });\n }\n};"
67+
export const startBuild = (deployBuildSpec?: BuildSpec, destroyBuildSpec?: BuildSpec) => { return `
68+
const respond = async function(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
69+
return new Promise((resolve, reject) => {
70+
var responseBody = JSON.stringify({
71+
Status: responseStatus,
72+
Reason: \"See the details in CloudWatch Log Stream: \" + context.logGroupName + \" \" + context.logStreamName,
73+
PhysicalResourceId: physicalResourceId || context.logStreamName,
74+
StackId: event.StackId,
75+
RequestId: event.RequestId,
76+
LogicalResourceId: event.LogicalResourceId,
77+
NoEcho: noEcho || false,
78+
Data: responseData
79+
});
80+
81+
console.log(\"Response body:\", responseBody);
82+
83+
var https = require(\"https\");
84+
var url = require(\"url\");
85+
86+
var parsedUrl = url.parse(event.ResponseURL);
87+
var options = {
88+
hostname: parsedUrl.hostname,
89+
port: 443,
90+
path: parsedUrl.path,
91+
method: \"PUT\",
92+
headers: {
93+
\"content-type\": \"\",
94+
\"content-length\": responseBody.length
95+
}
96+
};
97+
98+
var request = https.request(options, function(response) {
99+
console.log(\"Status code: \" + response.statusCode);
100+
console.log(\"Status message: \" + response.statusMessage);
101+
resolve();
102+
});
103+
104+
request.on(\"error\", function(error) {
105+
console.log(\"respond(..) failed executing https.request(..): \" + error);
106+
resolve();
107+
});
108+
109+
request.write(responseBody);
110+
request.end();
111+
});
112+
};
113+
114+
const AWS = require('aws-sdk');
115+
116+
exports.handler = async function (event, context) {
117+
console.log(JSON.stringify(event, null, 4));
118+
try {
119+
const projectName = event.ResourceProperties.ProjectName;
120+
const codebuild = new AWS.CodeBuild();
121+
122+
console.log(\`Starting new build of project \${projectName}\`);
123+
124+
const { build } = await codebuild.startBuild({
125+
projectName,
126+
// Pass CFN related parameters through the build for extraction by the
127+
// completion handler.
128+
buildspecOverride: event.RequestType === 'Delete' ? \`${destroyBuildSpec ? `${destroyBuildSpec.toBuildSpec()}` : defaultDestroyBuildSpec}\` : \`${deployBuildSpec ? `${deployBuildSpec.toBuildSpec()}` : defaultDeployBuildSpec}\`,
129+
environmentVariablesOverride: [
130+
{
131+
name: 'CFN_RESPONSE_URL',
132+
value: event.ResponseURL
133+
},
134+
{
135+
name: 'CFN_STACK_ID',
136+
value: event.StackId
137+
},
138+
{
139+
name: 'CFN_REQUEST_ID',
140+
value: event.RequestId
141+
},
142+
{
143+
name: 'CFN_LOGICAL_RESOURCE_ID',
144+
value: event.LogicalResourceId
145+
},
146+
{
147+
name: 'BUILD_ROLE_ARN',
148+
value: event.ResourceProperties.BuildRoleArn
149+
}
150+
]
151+
}).promise();
152+
console.log(\`Build id \${build.id} started - resource completion handled by EventBridge\`);
153+
} catch(error) {
154+
console.error(error);
155+
await respond(event, context, 'FAILED', { Error: error });
156+
}
157+
};
158+
`};
8159

9160
export const reportBuild = `
10161
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

core/src/common/cdk-deployer.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as cdk from 'aws-cdk-lib';
55
import { Construct } from 'constructs';
66
import { ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
77
import { Aws, CfnParameter, CustomResource, DefaultStackSynthesizer, Duration } from 'aws-cdk-lib';
8-
import { ComputeType, LinuxBuildImage, Project, Source, CfnProject } from 'aws-cdk-lib/aws-codebuild';
8+
import { ComputeType, LinuxBuildImage, Project, Source, CfnProject, BuildSpec } from 'aws-cdk-lib/aws-codebuild';
99
import { SingletonKey } from '../singleton-kms-key';
1010
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
1111
import { Rule } from 'aws-cdk-lib/aws-events';
@@ -59,6 +59,16 @@ export interface CdkDeployerProps extends cdk.StackProps {
5959
* CLICK_TO_DEPLOY: the CDK application is deployed through a click on a github README button
6060
*/
6161
readonly deploymentType: DeploymentType;
62+
63+
/**
64+
* Deploy CodeBuild buildspec file name at the root of the cdk app folder
65+
*/
66+
readonly deployBuildSpec?: BuildSpec;
67+
68+
/**
69+
* Destroy Codebuild buildspec file name at the root of the cdk app folder
70+
*/
71+
readonly destroyBuildSpec?: BuildSpec;
6272
}
6373

6474
/**
@@ -319,7 +329,7 @@ export class CdkDeployer extends cdk.Stack {
319329

320330
const startBuildFunction = new Function(this, 'StartBuildFunction', {
321331
runtime: Runtime.NODEJS_16_X,
322-
code: Code.fromInline(startBuild),
332+
code: Code.fromInline(startBuild(props.deployBuildSpec, props.destroyBuildSpec)),
323333
handler: 'index.handler',
324334
timeout: Duration.seconds(60),
325335
role: startBuildRole,

core/test/unit/cdk-deployer.test.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import { CdkDeployer, DeploymentType } from '../../src/common/cdk-deployer';
1212

1313
import { Match, Template } from 'aws-cdk-lib/assertions';
14+
import { BuildSpec } from 'aws-cdk-lib/aws-codebuild';
1415

1516

16-
describe ('CdkDeployer test', () => {
17+
describe ('CdkDeployer test default', () => {
1718

1819
const app = new App();
1920
const CdkDeployerStack = new CdkDeployer(app, {
@@ -491,11 +492,11 @@
491492
);
492493
});
493494

494-
test('CdkDeployer creates the proper StartBuild function', () => {
495+
test('CdkDeployer creates the proper StartBuild function using default build spec', () => {
495496
template.hasResourceProperties('AWS::Lambda::Function',
496497
Match.objectLike({
497498
"Code": {
498-
"ZipFile": Match.anyValue(),
499+
"ZipFile": Match.stringLikeRegexp('Default'),
499500
},
500501
"Role": Match.anyValue(),
501502
"Handler": "index.handler",
@@ -528,4 +529,99 @@
528529
}),
529530
);
530531
});
532+
});
533+
534+
535+
describe ('CdkDeployer test custom buildspec from object', () => {
536+
537+
const app = new App();
538+
const CdkDeployerStack = new CdkDeployer(app, {
539+
deploymentType: DeploymentType.CLICK_TO_DEPLOY,
540+
githubRepository: 'aws-samples/aws-analytics-reference-architecture',
541+
cdkAppLocation: 'refarch/aws-native',
542+
deployBuildSpec: BuildSpec.fromObject({
543+
version: '0.2',
544+
phases: {
545+
install: {
546+
'runtime-versions': {
547+
nodejs: 16,
548+
},
549+
commands: [
550+
'echo "Custom BuildSpec"',
551+
'npm install -g aws-cdk',
552+
],
553+
},
554+
build: {
555+
commands: [
556+
'npm install',
557+
'npm run build',
558+
'npm run cdk synth',
559+
],
560+
},
561+
},
562+
}),
563+
cdkParameters: {
564+
Foo: {
565+
default: 'no-value',
566+
type: 'String',
567+
},
568+
Bar: {
569+
default: 'some-value',
570+
type: 'String',
571+
},
572+
},
573+
});
574+
575+
const template = Template.fromStack(CdkDeployerStack);
576+
577+
test('CdkDeployer creates the proper StartBuild function using custom build spec', () => {
578+
template.hasResourceProperties('AWS::Lambda::Function',
579+
Match.objectLike({
580+
"Code": {
581+
"ZipFile": Match.stringLikeRegexp('Custom BuildSpec'),
582+
},
583+
"Role": Match.anyValue(),
584+
"Handler": "index.handler",
585+
"Runtime": "nodejs16.x",
586+
"Timeout": 60
587+
}),
588+
);
589+
});
590+
});
591+
592+
describe ('CdkDeployer test custom buildspec from file', () => {
593+
594+
const app = new App();
595+
const CdkDeployerStack = new CdkDeployer(app, {
596+
deploymentType: DeploymentType.CLICK_TO_DEPLOY,
597+
githubRepository: 'aws-samples/aws-analytics-reference-architecture',
598+
cdkAppLocation: 'refarch/aws-native',
599+
deployBuildSpec: BuildSpec.fromSourceFilename('custom-buildspec.yaml'),
600+
cdkParameters: {
601+
Foo: {
602+
default: 'no-value',
603+
type: 'String',
604+
},
605+
Bar: {
606+
default: 'some-value',
607+
type: 'String',
608+
},
609+
},
610+
});
611+
612+
const template = Template.fromStack(CdkDeployerStack);
613+
614+
test('CdkDeployer creates the proper StartBuild function using custom build spec', () => {
615+
template.hasResourceProperties('AWS::Lambda::Function',
616+
Match.objectLike({
617+
"Code": {
618+
"ZipFile": Match.stringLikeRegexp('custom-buildspec.yaml'),
619+
},
620+
"Role": Match.anyValue(),
621+
"Handler": "index.handler",
622+
"Runtime": "nodejs16.x",
623+
"Timeout": 60
624+
}),
625+
);
626+
});
531627
});

0 commit comments

Comments
 (0)