token-injectable-docker-builder 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/lib/index.d.ts +16 -0
- package/lib/index.ts +171 -0
- package/package.json +27 -2
- package/src/isCompleteHandler/index.js +97 -0
- package/src/onEventHandler/index.js +39 -0
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# TokenInjectableDockerBuilder
|
|
2
2
|
|
|
3
|
-
The `TokenInjectableDockerBuilder` is a
|
|
3
|
+
The `TokenInjectableDockerBuilder` is a flexible AWS CDK construct that enabled the usage of AWS CDK tokens in the building, pushing, and deployment of Docker images to Amazon Elastic Container Registry (ECR). It leverages AWS CodeBuild and Lambda custom resources.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
AWS CDK already provides mechanisms for creating deployable assets using Docker, such as [DockerImageAsset](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecr_assets.DockerImageAsset.html) and [DockerImageCode](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.DockerImageCode.html), but these Constructs are limited because they cannot accept CDK tokens as build-args. With the TokenInjectableDockerBuilder, one can inject CDK tokens as build-time args into their Docker-based assets to satisfy a much larger range of dependency relationships.
|
|
8
|
+
|
|
9
|
+
For example, imagine a NextJS frontend Docker image that calls an API Gateway endpoint. Logically, one would first deploy the API Gateway, then deploy the NextJS frontend such that it has reference to the API Gateway endpoint through a [build-time environment variable](https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables). In this case, building the Docker-based asset before deployment time doesn't work since it is dependent on the deployment of the API Gateway.
|
|
4
10
|
|
|
5
11
|
## Features
|
|
6
12
|
|
|
@@ -149,4 +155,10 @@ Ensure you have the following:
|
|
|
149
155
|
|
|
150
156
|
1. **Build Errors**: Check the AWS CodeBuild logs in CloudWatch.
|
|
151
157
|
2. **Lambda Function Errors**: Check the `onEvent` and `isComplete` Lambda logs in CloudWatch.
|
|
152
|
-
3. **Permissions**: Ensure the IAM role for CodeBuild has the required permissions to interact with ECR and CloudWatch.
|
|
158
|
+
3. **Permissions**: Ensure the IAM role for CodeBuild has the required permissions to interact with ECR and CloudWatch.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Support
|
|
163
|
+
|
|
164
|
+
Open an issue on [GitHub](https://github.com/AlexTech314/TokenInjectableDockerBuilder) :)
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DockerImageCode } from 'aws-cdk-lib/aws-lambda';
|
|
2
|
+
import { Construct } from 'constructs';
|
|
3
|
+
import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
|
|
4
|
+
export interface TokenInjectableDockerBuilderProps {
|
|
5
|
+
path: string;
|
|
6
|
+
buildArgs?: {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare class TokenInjectableDockerBuilder extends Construct {
|
|
11
|
+
private readonly ecrRepository;
|
|
12
|
+
private readonly buildTriggerResource;
|
|
13
|
+
constructor(scope: Construct, id: string, props: TokenInjectableDockerBuilderProps);
|
|
14
|
+
getContainerImage(): ContainerImage;
|
|
15
|
+
getDockerImageCode(): DockerImageCode;
|
|
16
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Duration, CustomResource, Stack } from 'aws-cdk-lib';
|
|
3
|
+
import { Project, Source, LinuxBuildImage, BuildSpec } from 'aws-cdk-lib/aws-codebuild';
|
|
4
|
+
import { Repository } from 'aws-cdk-lib/aws-ecr';
|
|
5
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
6
|
+
import { Code, DockerImageCode, Runtime } from 'aws-cdk-lib/aws-lambda';
|
|
7
|
+
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
|
|
8
|
+
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
9
|
+
import { Construct } from 'constructs';
|
|
10
|
+
import { ContainerImage } from 'aws-cdk-lib/aws-ecs';
|
|
11
|
+
import * as crypto from 'crypto';
|
|
12
|
+
import { Function } from 'aws-cdk-lib/aws-lambda';
|
|
13
|
+
|
|
14
|
+
export interface TokenInjectableDockerBuilderProps {
|
|
15
|
+
path: string;
|
|
16
|
+
buildArgs?: { [key: string]: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class TokenInjectableDockerBuilder extends Construct {
|
|
20
|
+
private readonly ecrRepository: Repository;
|
|
21
|
+
private readonly buildTriggerResource: CustomResource;
|
|
22
|
+
|
|
23
|
+
constructor(scope: Construct, id: string, props: TokenInjectableDockerBuilderProps) {
|
|
24
|
+
super(scope, id);
|
|
25
|
+
|
|
26
|
+
const { path: sourcePath, buildArgs } = props; // Default to linux/amd64
|
|
27
|
+
|
|
28
|
+
// Define absolute paths for Lambda handlers
|
|
29
|
+
const onEventHandlerPath = path.resolve(__dirname, '../src/onEventHandler');
|
|
30
|
+
const isCompleteHandlerPath = path.resolve(__dirname, '../src/isCompleteHandler');
|
|
31
|
+
|
|
32
|
+
// Create an ECR repository
|
|
33
|
+
this.ecrRepository = new Repository(this, 'ECRRepository');
|
|
34
|
+
|
|
35
|
+
// Package the source code as an asset
|
|
36
|
+
const sourceAsset = new Asset(this, 'SourceAsset', {
|
|
37
|
+
path: sourcePath, // Path to the Dockerfile or source code
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Transform buildArgs into a string of --build-arg KEY=VALUE
|
|
41
|
+
const buildArgsString = buildArgs
|
|
42
|
+
? Object.entries(buildArgs)
|
|
43
|
+
.map(([key, value]) => `--build-arg ${key}=${value}`)
|
|
44
|
+
.join(' ')
|
|
45
|
+
: '';
|
|
46
|
+
|
|
47
|
+
// Pass the buildArgsString and platform as environment variables
|
|
48
|
+
const environmentVariables: { [name: string]: { value: string } } = {
|
|
49
|
+
ECR_REPO_URI: { value: this.ecrRepository.repositoryUri },
|
|
50
|
+
BUILD_ARGS: { value: buildArgsString },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Create a CodeBuild project
|
|
54
|
+
const codeBuildProject = new Project(this, 'UICodeBuildProject', {
|
|
55
|
+
source: Source.s3({
|
|
56
|
+
bucket: sourceAsset.bucket,
|
|
57
|
+
path: sourceAsset.s3ObjectKey,
|
|
58
|
+
}),
|
|
59
|
+
environment: {
|
|
60
|
+
buildImage: LinuxBuildImage.STANDARD_7_0,
|
|
61
|
+
privileged: true, // Required for Docker builds
|
|
62
|
+
},
|
|
63
|
+
environmentVariables: environmentVariables,
|
|
64
|
+
buildSpec: BuildSpec.fromObject({
|
|
65
|
+
version: '0.2',
|
|
66
|
+
phases: {
|
|
67
|
+
pre_build: {
|
|
68
|
+
commands: [
|
|
69
|
+
'echo "Retrieving AWS Account ID..."',
|
|
70
|
+
'export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)',
|
|
71
|
+
'echo "Logging in to Amazon ECR..."',
|
|
72
|
+
'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
build: {
|
|
76
|
+
commands: [
|
|
77
|
+
'echo Build phase: Building the Docker image...',
|
|
78
|
+
'docker build $BUILD_ARGS -t $ECR_REPO_URI:latest $CODEBUILD_SRC_DIR',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
post_build: {
|
|
82
|
+
commands: [
|
|
83
|
+
'echo Post-build phase: Pushing the Docker image...',
|
|
84
|
+
'docker push $ECR_REPO_URI:latest',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Grant permissions to interact with ECR
|
|
92
|
+
this.ecrRepository.grantPullPush(codeBuildProject);
|
|
93
|
+
|
|
94
|
+
codeBuildProject.role!.addToPrincipalPolicy(
|
|
95
|
+
new PolicyStatement({
|
|
96
|
+
actions: ['ecr:GetAuthorizationToken'],
|
|
97
|
+
resources: ['*'],
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Grant permissions to CodeBuild for CloudWatch Logs
|
|
102
|
+
codeBuildProject.role!.addToPrincipalPolicy(
|
|
103
|
+
new PolicyStatement({
|
|
104
|
+
actions: ['logs:PutLogEvents', 'logs:CreateLogGroup', 'logs:CreateLogStream'],
|
|
105
|
+
resources: [`arn:aws:logs:${Stack.of(this).region}:${Stack.of(this).account}:*`],
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Create Node.js Lambda function for onEvent
|
|
110
|
+
const onEventHandlerFunction = new Function(this, 'OnEventHandlerFunction', {
|
|
111
|
+
runtime: Runtime.NODEJS_18_X, // Use Node.js runtime
|
|
112
|
+
code: Code.fromAsset(onEventHandlerPath), // Path to handler code
|
|
113
|
+
handler: 'index.handler', // Entry point (adjust as needed)
|
|
114
|
+
timeout: Duration.minutes(15),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
onEventHandlerFunction.addToRolePolicy(
|
|
118
|
+
new PolicyStatement({
|
|
119
|
+
actions: ['codebuild:StartBuild'],
|
|
120
|
+
resources: [codeBuildProject.projectArn], // Restrict to specific project
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Create Node.js Lambda function for isComplete
|
|
125
|
+
const isCompleteHandlerFunction = new Function(this, 'IsCompleteHandlerFunction', {
|
|
126
|
+
runtime: Runtime.NODEJS_18_X,
|
|
127
|
+
code: Code.fromAsset(isCompleteHandlerPath),
|
|
128
|
+
handler: 'index.handler',
|
|
129
|
+
timeout: Duration.minutes(15),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
isCompleteHandlerFunction.addToRolePolicy(
|
|
133
|
+
new PolicyStatement({
|
|
134
|
+
actions: [
|
|
135
|
+
'codebuild:BatchGetBuilds',
|
|
136
|
+
'codebuild:ListBuildsForProject',
|
|
137
|
+
'logs:GetLogEvents',
|
|
138
|
+
'logs:DescribeLogStreams',
|
|
139
|
+
'logs:DescribeLogGroups'
|
|
140
|
+
],
|
|
141
|
+
resources: ['*'],
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Create a custom resource provider
|
|
146
|
+
const provider = new Provider(this, 'CustomResourceProvider', {
|
|
147
|
+
onEventHandler: onEventHandlerFunction,
|
|
148
|
+
isCompleteHandler: isCompleteHandlerFunction,
|
|
149
|
+
queryInterval: Duration.minutes(1),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Define the custom resource
|
|
153
|
+
this.buildTriggerResource = new CustomResource(this, 'BuildTriggerResource', {
|
|
154
|
+
serviceToken: provider.serviceToken,
|
|
155
|
+
properties: {
|
|
156
|
+
ProjectName: codeBuildProject.projectName,
|
|
157
|
+
Trigger: crypto.randomUUID(),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.buildTriggerResource.node.addDependency(codeBuildProject);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public getContainerImage(): ContainerImage {
|
|
165
|
+
return ContainerImage.fromEcrRepository(this.ecrRepository, 'latest');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public getDockerImageCode(): DockerImageCode {
|
|
169
|
+
return DockerImageCode.fromEcr(this.ecrRepository);
|
|
170
|
+
}
|
|
171
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-injectable-docker-builder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"types": "lib/index.d.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -21,5 +21,30 @@
|
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"aws-cdk-lib": "2.166.0",
|
|
23
23
|
"constructs": "^10.0.0"
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"aws",
|
|
27
|
+
"cdk",
|
|
28
|
+
"aws-cdk",
|
|
29
|
+
"docker",
|
|
30
|
+
"ecr",
|
|
31
|
+
"lambda",
|
|
32
|
+
"custom-resource",
|
|
33
|
+
"docker-build",
|
|
34
|
+
"codebuild",
|
|
35
|
+
"token-injection",
|
|
36
|
+
"docker-image",
|
|
37
|
+
"aws-codebuild",
|
|
38
|
+
"aws-ecr",
|
|
39
|
+
"docker-builder",
|
|
40
|
+
"cdk-construct",
|
|
41
|
+
"lambda-custom-resource",
|
|
42
|
+
"container-image",
|
|
43
|
+
"aws-lambda",
|
|
44
|
+
"aws-cdk-lib",
|
|
45
|
+
"cloud-development-kit",
|
|
46
|
+
"ci-cd",
|
|
47
|
+
"aws-ci-cd",
|
|
48
|
+
"infrastructure-as-code"
|
|
49
|
+
]
|
|
25
50
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { CodeBuildClient, ListBuildsForProjectCommand, BatchGetBuildsCommand } = require('@aws-sdk/client-codebuild');
|
|
2
|
+
const { CloudWatchLogsClient, GetLogEventsCommand } = require('@aws-sdk/client-cloudwatch-logs');
|
|
3
|
+
|
|
4
|
+
exports.handler = async (event, context) => {
|
|
5
|
+
console.log('isCompleteHandler Event:', JSON.stringify(event, null, 2));
|
|
6
|
+
|
|
7
|
+
// Initialize AWS SDK v3 clients
|
|
8
|
+
const codebuildClient = new CodeBuildClient({ region: process.env.AWS_REGION });
|
|
9
|
+
const cloudwatchlogsClient = new CloudWatchLogsClient({ region: process.env.AWS_REGION });
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const projectName = event.ResourceProperties.ProjectName;
|
|
13
|
+
|
|
14
|
+
if (!projectName) {
|
|
15
|
+
throw new Error('ProjectName is required in ResourceProperties');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(`Checking status for CodeBuild project: ${projectName}`);
|
|
19
|
+
|
|
20
|
+
// Retrieve the latest build for the given project
|
|
21
|
+
const listBuildsCommand = new ListBuildsForProjectCommand({
|
|
22
|
+
projectName: projectName,
|
|
23
|
+
sortOrder: 'DESCENDING',
|
|
24
|
+
maxResults: 1,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const listBuildsResp = await codebuildClient.send(listBuildsCommand);
|
|
28
|
+
const buildIds = listBuildsResp.ids;
|
|
29
|
+
|
|
30
|
+
if (!buildIds || buildIds.length === 0) {
|
|
31
|
+
throw new Error(`No builds found for project: ${projectName}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const buildId = buildIds[0];
|
|
35
|
+
console.log(`Latest Build ID: ${buildId}`);
|
|
36
|
+
|
|
37
|
+
// Get build details
|
|
38
|
+
const batchGetBuildsCommand = new BatchGetBuildsCommand({
|
|
39
|
+
ids: [buildId],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const buildDetailsResp = await codebuildClient.send(batchGetBuildsCommand);
|
|
43
|
+
const build = buildDetailsResp.builds[0];
|
|
44
|
+
|
|
45
|
+
if (!build) {
|
|
46
|
+
throw new Error(`Build details not found for Build ID: ${buildId}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const buildStatus = build.buildStatus;
|
|
50
|
+
console.log(`Build Status: ${buildStatus}`);
|
|
51
|
+
|
|
52
|
+
if (buildStatus === 'IN_PROGRESS') {
|
|
53
|
+
// Build is still in progress
|
|
54
|
+
console.log('Build is still in progress.');
|
|
55
|
+
return { IsComplete: false };
|
|
56
|
+
} else if (buildStatus === 'SUCCEEDED') {
|
|
57
|
+
// Build succeeded
|
|
58
|
+
console.log('Build succeeded.');
|
|
59
|
+
return { IsComplete: true };
|
|
60
|
+
} else if (['FAILED', 'FAULT', 'STOPPED', 'TIMED_OUT'].includes(buildStatus)) {
|
|
61
|
+
// Build failed; retrieve last 5 log lines
|
|
62
|
+
const logsInfo = build.logs;
|
|
63
|
+
if (logsInfo && logsInfo.groupName && logsInfo.streamName) {
|
|
64
|
+
console.log(`Retrieving logs from CloudWatch Logs Group: ${logsInfo.groupName}, Stream: ${logsInfo.streamName}`);
|
|
65
|
+
|
|
66
|
+
const getLogEventsCommand = new GetLogEventsCommand({
|
|
67
|
+
logGroupName: logsInfo.groupName,
|
|
68
|
+
logStreamName: logsInfo.streamName,
|
|
69
|
+
startFromHead: false, // Start from the end to get latest logs
|
|
70
|
+
limit: 5,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const logEventsResp = await cloudwatchlogsClient.send(getLogEventsCommand);
|
|
74
|
+
const logEvents = logEventsResp.events;
|
|
75
|
+
const lastFiveMessages = logEvents.map((event) => event.message).reverse().join('\n');
|
|
76
|
+
|
|
77
|
+
const errorMessage = `Build failed with status: ${buildStatus}\nLast 5 build logs:\n${lastFiveMessages}`;
|
|
78
|
+
console.error(errorMessage);
|
|
79
|
+
|
|
80
|
+
// Throw an error to indicate failure to the CDK provider
|
|
81
|
+
throw new Error(errorMessage);
|
|
82
|
+
} else {
|
|
83
|
+
const errorMessage = `Build failed with status: ${buildStatus}, but logs are not available.`;
|
|
84
|
+
console.error(errorMessage);
|
|
85
|
+
throw new Error(errorMessage);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const errorMessage = `Unknown build status: ${buildStatus}`;
|
|
89
|
+
console.error(errorMessage);
|
|
90
|
+
throw new Error(errorMessage);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error in isCompleteHandler:', error);
|
|
94
|
+
// Rethrow the error to inform the CDK provider of the failure
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { CodeBuildClient, StartBuildCommand } = require('@aws-sdk/client-codebuild');
|
|
2
|
+
|
|
3
|
+
exports.handler = async (event, context) => {
|
|
4
|
+
console.log('Event:', JSON.stringify(event, null, 2));
|
|
5
|
+
|
|
6
|
+
// Initialize the AWS SDK v3 CodeBuild client
|
|
7
|
+
const codebuildClient = new CodeBuildClient({ region: process.env.AWS_REGION });
|
|
8
|
+
|
|
9
|
+
// Set the PhysicalResourceId
|
|
10
|
+
let physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId;
|
|
11
|
+
|
|
12
|
+
if (event.RequestType === 'Create' || event.RequestType === 'Update') {
|
|
13
|
+
const params = {
|
|
14
|
+
projectName: event.ResourceProperties.ProjectName,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const command = new StartBuildCommand(params); // Create the command
|
|
19
|
+
const build = await codebuildClient.send(command); // Send the command
|
|
20
|
+
console.log('Started build:', JSON.stringify(build, null, 2));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Error starting build:', error);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
PhysicalResourceId: physicalResourceId,
|
|
26
|
+
Data: {},
|
|
27
|
+
Reason: error.message,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
} else if (event.RequestType === 'Delete') {
|
|
31
|
+
// No action needed for delete, but ensure PhysicalResourceId remains the same
|
|
32
|
+
console.log('Delete request received. No action required.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
PhysicalResourceId: physicalResourceId,
|
|
37
|
+
Data: {},
|
|
38
|
+
};
|
|
39
|
+
};
|