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 CHANGED
@@ -1,6 +1,12 @@
1
1
  # TokenInjectableDockerBuilder
2
2
 
3
- The `TokenInjectableDockerBuilder` is a powerful AWS CDK construct that automates the building, pushing, and deployment of Docker images to Amazon Elastic Container Registry (ECR) using AWS CodeBuild and Lambda custom resources. This construct simplifies workflows by enabling token-based Docker image customization.
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.2",
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
+ };