deploy-time-build
Version:
Build during CDK deployment.
251 lines (240 loc) • 8 kB
text/typescript
import { BatchGetBuildsCommand, CodeBuildClient, StartBuildCommand } from '@aws-sdk/client-codebuild';
import type { ResourceProperties } from '../../src/types';
import Crypto from 'crypto';
import { setTimeout } from 'timers/promises';
const cb = new CodeBuildClient({});
type Event = {
RequestType: 'Create' | 'Update' | 'Delete';
ResponseURL: string;
StackId: string;
RequestId: string;
ResourceType: string;
LogicalResourceId: string;
ResourceProperties: ResourceProperties;
};
export const handler = async (event: Event, context: any) => {
console.log(JSON.stringify(event));
try {
if (event.RequestType == 'Create' || event.RequestType == 'Update') {
const props = event.ResourceProperties;
const commonEnvironments = [
{
name: 'responseURL',
value: event.ResponseURL,
},
{
name: 'stackId',
value: event.StackId,
},
{
name: 'requestId',
value: event.RequestId,
},
{
name: 'logicalResourceId',
value: event.LogicalResourceId,
},
];
const newPhysicalId = Crypto.randomBytes(16).toString('hex');
let command: StartBuildCommand;
switch (props.type) {
case 'NodejsBuild':
command = new StartBuildCommand({
projectName: props.codeBuildProjectName,
environmentVariablesOverride: [
...commonEnvironments,
{
name: 'input',
value: JSON.stringify(
props.sources.map((source) => ({
assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`,
extractPath: source.extractPath,
commands: (source.commands ?? []).join(' && '),
}))
),
},
{
name: 'buildCommands',
value: props.buildCommands.join(' && '),
},
{
name: 'destinationBucketName',
value: props.destinationBucketName,
},
{
name: 'destinationObjectKey',
// This should be random to always trigger a BucketDeployment update process
value: `${newPhysicalId}.zip`,
},
{
name: 'workingDirectory',
value: props.workingDirectory,
},
{
name: 'outputSourceDirectory',
value: props.outputSourceDirectory,
},
{
name: 'projectName',
value: props.codeBuildProjectName,
},
{
name: 'outputEnvFile',
value: props.outputEnvFile.toString(),
},
{
name: 'envFileKey',
value: `deploy-time-build/${event.StackId.split('/')[1]}/${event.LogicalResourceId}/${newPhysicalId}.env`,
},
{
name: 'envNames',
value: Object.keys(props.environment ?? {}).join(','),
},
...Object.entries(props.environment ?? {}).map(([name, value]) => ({
name,
value,
})),
],
});
break;
case 'SociIndexBuild':
command = new StartBuildCommand({
projectName: props.codeBuildProjectName,
environmentVariablesOverride: [
...commonEnvironments,
{
name: 'repositoryName',
value: props.repositoryName,
},
{
name: 'imageTag',
value: props.imageTag,
},
{
name: 'projectName',
value: props.codeBuildProjectName,
},
],
});
break;
case 'SociIndexV2Build':
command = new StartBuildCommand({
projectName: props.codeBuildProjectName,
environmentVariablesOverride: [
...commonEnvironments,
{
name: 'repositoryName',
value: props.repositoryName,
},
{
name: 'inputImageTag',
value: props.inputImageTag,
},
{
name: 'outputImageTag',
value: props.outputImageTag,
},
{
name: 'projectName',
value: props.codeBuildProjectName,
},
],
});
break;
case 'ContainerImageBuild': {
const imageTag = props.imageTag ?? `${props.tagPrefix ?? ''}${newPhysicalId}`;
command = new StartBuildCommand({
projectName: props.codeBuildProjectName,
environmentVariablesOverride: [
...commonEnvironments,
{
name: 'repositoryUri',
value: props.repositoryUri,
},
{
name: 'repositoryAuthUri',
value: props.repositoryUri.split('/')[0],
},
{
name: 'buildCommand',
value: props.buildCommand,
},
{
name: 'imageTag',
value: imageTag,
},
{
name: 'projectName',
value: props.codeBuildProjectName,
},
{
name: 'sourceS3Url',
value: props.sourceS3Url,
},
],
});
break;
}
default:
throw new Error(`invalid event type ${props}}`);
}
// start code build project
let retries = 0;
while (retries < 10) {
try {
await cb.send(command);
break;
} catch (error: any) {
// Sometimes AccessDeniedException is thrown due to IAM propagation delay. It should be resolved with retry.
if (error.name === 'AccessDeniedException') {
retries++;
console.log(`AccessDeniedException encountered, retrying (${retries})...`);
await setTimeout(5000);
} else {
throw error;
}
}
}
// Sometimes CodeBuild build fails before running buildspec, without calling the CFn callback.
// We can poll the status of a build for a few minutes and sendStatus if such errors are detected.
// if (build.build?.id == null) {
// throw new Error('build id is null');
// }
// for (let i=0; i< 20; i++) {
// const res = await cb.send(new BatchGetBuildsCommand({ ids: [build.build.id] }));
// const status = res.builds?.[0].buildStatus;
// if (status == null) {
// throw new Error('build status is null');
// }
// await new Promise((resolve) => setTimeout(resolve, 5000));
// }
} else {
// Do nothing on a DELETE event.
await sendStatus('SUCCESS', event, context);
}
} catch (e) {
console.log(e);
const err = e as Error;
await sendStatus('FAILED', event, context, err.message);
}
};
const sendStatus = async (status: 'SUCCESS' | 'FAILED', event: Event, context: any, reason?: string) => {
const responseBody = JSON.stringify({
Status: status,
Reason: reason ?? 'See the details in CloudWatch Log Stream: ' + context.logStreamName,
PhysicalResourceId: context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: false,
Data: {}, //responseData
});
await fetch(event.ResponseURL, {
method: 'PUT',
body: responseBody,
headers: {
'Content-Type': '',
'Content-Length': responseBody.length.toString(),
},
});
};