token-injectable-docker-builder 1.13.3 → 2.0.0
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/.jsii +260 -143
- package/API.md +196 -136
- package/README.md +156 -71
- package/ecrReplication/ecrReplication.js +156 -0
- package/isComplete/isComplete.js +63 -4
- package/lib/build-spec.d.ts +24 -0
- package/lib/build-spec.js +104 -0
- package/lib/builder.d.ts +206 -0
- package/lib/builder.js +289 -0
- package/lib/constants.d.ts +7 -0
- package/lib/constants.js +11 -0
- package/lib/ecr.d.ts +16 -0
- package/lib/ecr.js +30 -0
- package/lib/index.d.ts +2 -261
- package/lib/index.js +6 -402
- package/lib/provider.d.ts +63 -0
- package/lib/provider.js +212 -0
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -16,13 +16,14 @@ For example, a Next.js frontend Docker image may require an API Gateway URL as a
|
|
|
16
16
|
|
|
17
17
|
- **Build and Push Docker Images**: Automatically builds and pushes Docker images to ECR.
|
|
18
18
|
- **Token Support**: Supports custom build arguments for Docker builds, including CDK tokens resolved at deployment time.
|
|
19
|
-
- **
|
|
19
|
+
- **Per-stack Lambda singleton**: Every builder in a stack automatically shares one pair of `onEvent` / `isComplete` Lambdas via `TokenInjectableDockerBuilderProvider`. Two builders cost the same Lambda overhead as one.
|
|
20
|
+
- **Cross-region replication**: Pass `replicaRegions: ['us-west-2', ...]` to replicate the built image to additional regions via ECR's native replication. Use `builder.dockerImageCodeFor(scope, region)` / `builder.containerImageFor(scope, region)` in a consumer stack in another region. The custom resource waits for replicas to land before signalling complete, so downstream stacks deploy safely.
|
|
20
21
|
- **Custom Install and Pre-Build Commands**: Allows specifying custom commands to run during the `install` and `pre_build` phases of the CodeBuild build process.
|
|
21
22
|
- **VPC Configuration**: Supports deploying the CodeBuild project within a VPC, with customizable security groups and subnet selection.
|
|
22
23
|
- **Docker Login**: Supports Docker login using credentials stored in AWS Secrets Manager.
|
|
23
|
-
- **ECR
|
|
24
|
+
- **Safe ECR retention by default**: Untagged images expire after 30 days; tagged images are never deleted (Lambda pins by digest, so deleting an in-use tag would break the next config update).
|
|
24
25
|
- **Integration with ECS and Lambda**: Provides outputs for use in AWS ECS and AWS Lambda.
|
|
25
|
-
- **
|
|
26
|
+
- **Configurable Build Polling**: Tune how often the provider checks for build completion via `TokenInjectableDockerBuilderProvider.getOrCreate(this, { queryInterval })` (defaults to 30 seconds).
|
|
26
27
|
- **Custom Dockerfile**: Specify a custom Dockerfile name via the `file` property (e.g. `Dockerfile.production`), allowing multiple Docker images from the same source directory.
|
|
27
28
|
- **ECR Docker Layer Caching**: By default, builds use `docker buildx` with ECR as a remote cache backend, reducing build times by reusing layers across deploys. Set `cacheDisabled: true` to force a clean build from scratch.
|
|
28
29
|
- **Platform Support**: Build images for `linux/amd64` (x86_64) or `linux/arm64` (Graviton) using native CodeBuild instances — no emulation, no QEMU. ARM builds are faster and cheaper.
|
|
@@ -55,19 +56,19 @@ pip install token-injectable-docker-builder
|
|
|
55
56
|
|
|
56
57
|
### `TokenInjectableDockerBuilderProvider`
|
|
57
58
|
|
|
58
|
-
A singleton construct that creates the `onEvent` and `isComplete` Lambda functions once per stack.
|
|
59
|
+
A singleton construct that creates the `onEvent` and `isComplete` Lambda functions once per stack. Every `TokenInjectableDockerBuilder` in the same stack automatically reuses this singleton, so two builders cost the same Lambda overhead as one. You only need to call this yourself if you want to customize `queryInterval`.
|
|
59
60
|
|
|
60
61
|
#### Static Methods
|
|
61
62
|
|
|
62
63
|
| Method | Description |
|
|
63
64
|
|---|---|
|
|
64
|
-
| `getOrCreate(scope, props?)` | Returns the existing provider for the stack, or creates one if it doesn't exist. |
|
|
65
|
+
| `getOrCreate(scope, props?)` | Returns the existing provider for the stack, or creates one if it doesn't exist. Called automatically by every `TokenInjectableDockerBuilder`. |
|
|
65
66
|
|
|
66
67
|
#### Properties in `TokenInjectableDockerBuilderProviderProps`
|
|
67
68
|
|
|
68
69
|
| Property | Type | Required | Description |
|
|
69
70
|
|---|---|---|---|
|
|
70
|
-
| `queryInterval` | `Duration` | No | How often the provider polls for build completion. Defaults to `Duration.seconds(30)`. |
|
|
71
|
+
| `queryInterval` | `Duration` | No | How often the provider polls for build completion. Defaults to `Duration.seconds(30)`. To override, call `getOrCreate` explicitly **before** creating any builders. |
|
|
71
72
|
|
|
72
73
|
#### Instance Properties
|
|
73
74
|
|
|
@@ -79,7 +80,7 @@ A singleton construct that creates the `onEvent` and `isComplete` Lambda functio
|
|
|
79
80
|
|
|
80
81
|
| Method | Description |
|
|
81
82
|
|---|---|
|
|
82
|
-
| `registerProject(project, ecrRepo, encryptionKey?)` | Grants the shared Lambdas permission to start builds and access ECR for a specific CodeBuild project. Called automatically
|
|
83
|
+
| `registerProject(project, ecrRepo, encryptionKey?)` | Grants the shared Lambdas permission to start builds and access ECR for a specific CodeBuild project. Called automatically by `TokenInjectableDockerBuilder`'s constructor. |
|
|
83
84
|
|
|
84
85
|
---
|
|
85
86
|
|
|
@@ -97,7 +98,7 @@ A singleton construct that creates the `onEvent` and `isComplete` Lambda functio
|
|
|
97
98
|
|----------------------------|-----------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
98
99
|
| `path` | `string` | Yes | The file path to the Dockerfile or source code directory. |
|
|
99
100
|
| `buildArgs` | `{ [key: string]: string }` | No | Build arguments to pass to the Docker build process. These are transformed into `--build-arg` flags. To use in Dockerfile, leverage the `ARG` keyword. For more details, please see the [official Docker docs](https://docs.docker.com/build/building/variables/). |
|
|
100
|
-
| `provider` | `TokenInjectableDockerBuilderProvider` | No | Shared provider for the custom resource Lambdas.
|
|
101
|
+
| `provider` | `TokenInjectableDockerBuilderProvider` | No | Shared provider for the custom resource Lambdas. Defaults to the per-stack singleton — `TokenInjectableDockerBuilderProvider.getOrCreate(this)`. Only pass this explicitly when you need a non-default `queryInterval`. |
|
|
101
102
|
| `dockerLoginSecretArn` | `string` | No | ARN of an AWS Secrets Manager secret for Docker credentials. Skips login if not provided. |
|
|
102
103
|
| `vpc` | `IVpc` | No | The VPC in which the CodeBuild project will be deployed. If provided, the CodeBuild project will be launched within the specified VPC. |
|
|
103
104
|
| `securityGroups` | `ISecurityGroup[]` | No | The security groups to attach to the CodeBuild project. These should define the network access rules for the CodeBuild project. |
|
|
@@ -105,63 +106,64 @@ A singleton construct that creates the `onEvent` and `isComplete` Lambda functio
|
|
|
105
106
|
| `installCommands` | `string[]` | No | Custom commands to run during the `install` phase of the CodeBuild build process. Will be executed before the Docker image is built. Useful for installing necessary dependencies for running pre-build scripts. |
|
|
106
107
|
| `preBuildCommands` | `string[]` | No | Custom commands to run during the `pre_build` phase of the CodeBuild build process. Will be executed before the Docker image is built. Useful for running pre-build scripts, such as fetching configs. |
|
|
107
108
|
| `kmsEncryption` | `boolean` | No | Whether to enable KMS encryption for the ECR repository. If `true`, a KMS key will be created for encrypting ECR images; otherwise, AES-256 encryption is used. Defaults to `false`. |
|
|
108
|
-
| `completenessQueryInterval`| `Duration` | No | The query interval for checking if the CodeBuild project has completed. This determines how frequently the custom resource polls for build completion. Defaults to `Duration.seconds(30)`. Ignored when `provider` is set (the provider's `queryInterval` is used instead). |
|
|
109
109
|
| `exclude` | `string[]` | No | A list of file paths in the Docker directory to exclude from the S3 asset bundle. If a `.dockerignore` file is present in the source directory, its contents will be used if this prop is not set. Defaults to an empty list or `.dockerignore` contents. |
|
|
110
110
|
| `file` | `string` | No | The name of the Dockerfile to use for the build. Passed as `--file` to `docker build`. Useful when a project has multiple Dockerfiles (e.g. `Dockerfile.production`, `Dockerfile.admin`). Defaults to `Dockerfile`. |
|
|
111
111
|
| `cacheDisabled` | `boolean` | No | When `true`, disables Docker layer caching. Every build runs from scratch. Use for debugging, corrupted cache, or major dependency changes. Defaults to `false`. |
|
|
112
112
|
| `platform` | `'linux/amd64' \| 'linux/arm64'` | No | Target platform for the Docker image. When set to `'linux/arm64'`, uses a native ARM/Graviton CodeBuild instance for fast builds without emulation. Defaults to `'linux/amd64'`. |
|
|
113
113
|
| `buildLogGroup` | `ILogGroup` | No | CloudWatch log group for CodeBuild build logs. When provided with RETAIN removal policy, logs survive rollbacks and stack deletion. If not provided, CodeBuild uses default logging (logs are deleted on rollback). |
|
|
114
|
-
| `
|
|
114
|
+
| `retainBuildLogs` | `boolean` | No | When `true`, the construct creates a CloudWatch log group at `/docker-builder/<projectName>` **outside** of CloudFormation and routes CodeBuild output there. Because the log group is managed imperatively, it survives stack rollbacks. 7-day retention applies. Defaults to `false`. |
|
|
115
115
|
| `ecrPullThroughCachePrefixes` | `string[]` | No | ECR pull-through cache repository prefixes to grant pull access to. Use when your Dockerfile references base images from ECR pull-through cache (e.g. `docker-hub/library/node:20-slim`, `ghcr/org/image:tag`). The CodeBuild role is granted `ecr:BatchGetImage`, `ecr:GetDownloadUrlForLayer`, and `ecr:BatchCheckLayerAvailability` on repositories matching each prefix. Example: `['docker-hub', 'ghcr']`. Defaults to no pull-through cache access. |
|
|
116
|
+
| `replicaRegions` | `string[]` | No | Additional regions to replicate the image to via ECR's native registry replication. Enables `dockerImageCodeFor(scope, region)` / `containerImageFor(scope, region)` for consumer stacks in those regions. See [Cross-Region Replication](#cross-region-replication) for details and caveats. Defaults to `[]` (no replication). |
|
|
116
117
|
|
|
117
118
|
#### Instance Properties
|
|
118
119
|
|
|
119
120
|
| Property | Type | Description |
|
|
120
121
|
|---|---|---|
|
|
121
|
-
| `containerImage` | `ContainerImage` | An ECS-compatible container image referencing the built Docker image
|
|
122
|
-
| `dockerImageCode` | `DockerImageCode` | A Lambda-compatible Docker image code referencing the built Docker image
|
|
122
|
+
| `containerImage` | `ContainerImage` | An ECS-compatible container image referencing the built Docker image **in the primary region**. |
|
|
123
|
+
| `dockerImageCode` | `DockerImageCode` | A Lambda-compatible Docker image code referencing the built Docker image **in the primary region**. |
|
|
124
|
+
| `repositoryName` | `string` | The ECR repository name (same name across all replica regions). |
|
|
125
|
+
| `imageTag` | `string` | The resolved image tag (CFN token; available at deploy time). |
|
|
126
|
+
|
|
127
|
+
#### Instance Methods (cross-region)
|
|
128
|
+
|
|
129
|
+
| Method | Description |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `containerImageFor(scope, region)` | Returns an ECS `ContainerImage` pointing at the same tag in `region`. Requires the region to be the primary or in `replicaRegions`. |
|
|
132
|
+
| `dockerImageCodeFor(scope, region)` | Returns a Lambda `DockerImageCode` pointing at the same tag in `region`. Same constraints as above. |
|
|
133
|
+
| `repositoryUriFor(region)` | Returns the regional ECR URI `<account>.dkr.ecr.<region>.amazonaws.com/<repoName>` as a string token. |
|
|
123
134
|
|
|
124
135
|
---
|
|
125
136
|
|
|
126
137
|
## Usage Examples
|
|
127
138
|
|
|
128
|
-
###
|
|
139
|
+
### Multiple Images in One Stack
|
|
129
140
|
|
|
130
|
-
|
|
141
|
+
Builders in the same stack automatically share a single pair of `onEvent` / `isComplete` Lambdas — there is no per-builder Lambda overhead. Just instantiate as many builders as you need.
|
|
131
142
|
|
|
132
143
|
#### TypeScript/NPM Example
|
|
133
144
|
|
|
134
145
|
```typescript
|
|
135
146
|
import * as cdk from 'aws-cdk-lib';
|
|
136
|
-
import {
|
|
137
|
-
TokenInjectableDockerBuilder,
|
|
138
|
-
TokenInjectableDockerBuilderProvider,
|
|
139
|
-
} from 'token-injectable-docker-builder';
|
|
147
|
+
import { TokenInjectableDockerBuilder } from 'token-injectable-docker-builder';
|
|
140
148
|
import * as ecs from 'aws-cdk-lib/aws-ecs';
|
|
141
149
|
|
|
142
150
|
export class MultiImageStack extends cdk.Stack {
|
|
143
151
|
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
|
|
144
152
|
super(scope, id, props);
|
|
145
153
|
|
|
146
|
-
//
|
|
147
|
-
const provider = TokenInjectableDockerBuilderProvider.getOrCreate(this);
|
|
148
|
-
|
|
149
|
-
// Build multiple Docker images sharing the same provider
|
|
154
|
+
// Each builder reuses the same per-stack provider automatically.
|
|
150
155
|
const apiBuilder = new TokenInjectableDockerBuilder(this, 'ApiImage', {
|
|
151
156
|
path: './src/api',
|
|
152
|
-
provider,
|
|
153
157
|
});
|
|
154
158
|
|
|
155
159
|
const workerBuilder = new TokenInjectableDockerBuilder(this, 'WorkerImage', {
|
|
156
160
|
path: './src/worker',
|
|
157
|
-
provider,
|
|
158
161
|
});
|
|
159
162
|
|
|
160
|
-
|
|
163
|
+
new TokenInjectableDockerBuilder(this, 'FrontendImage', {
|
|
161
164
|
path: './src/frontend',
|
|
162
165
|
buildArgs: { API_URL: 'https://api.example.com' },
|
|
163
166
|
platform: 'linux/arm64', // Build natively on Graviton
|
|
164
|
-
provider,
|
|
165
167
|
});
|
|
166
168
|
|
|
167
169
|
// Use in ECS task definitions
|
|
@@ -176,36 +178,48 @@ export class MultiImageStack extends cdk.Stack {
|
|
|
176
178
|
|
|
177
179
|
```python
|
|
178
180
|
from aws_cdk import aws_ecs as ecs, core as cdk
|
|
179
|
-
from token_injectable_docker_builder import
|
|
180
|
-
TokenInjectableDockerBuilder,
|
|
181
|
-
TokenInjectableDockerBuilderProvider,
|
|
182
|
-
)
|
|
181
|
+
from token_injectable_docker_builder import TokenInjectableDockerBuilder
|
|
183
182
|
|
|
184
183
|
class MultiImageStack(cdk.Stack):
|
|
185
184
|
def __init__(self, scope: cdk.App, id: str, **kwargs):
|
|
186
185
|
super().__init__(scope, id, **kwargs)
|
|
187
186
|
|
|
188
|
-
#
|
|
189
|
-
provider = TokenInjectableDockerBuilderProvider.get_or_create(self)
|
|
190
|
-
|
|
191
|
-
# Build multiple Docker images sharing the same provider
|
|
187
|
+
# Each builder reuses the same per-stack provider automatically.
|
|
192
188
|
api_builder = TokenInjectableDockerBuilder(self, "ApiImage",
|
|
193
189
|
path="./src/api",
|
|
194
|
-
provider=provider,
|
|
195
190
|
)
|
|
196
191
|
|
|
197
192
|
worker_builder = TokenInjectableDockerBuilder(self, "WorkerImage",
|
|
198
193
|
path="./src/worker",
|
|
199
|
-
provider=provider,
|
|
200
194
|
)
|
|
201
195
|
|
|
202
|
-
|
|
196
|
+
TokenInjectableDockerBuilder(self, "FrontendImage",
|
|
203
197
|
path="./src/frontend",
|
|
204
198
|
build_args={"API_URL": "https://api.example.com"},
|
|
205
|
-
provider=provider,
|
|
206
199
|
)
|
|
207
200
|
```
|
|
208
201
|
|
|
202
|
+
#### Overriding `queryInterval`
|
|
203
|
+
|
|
204
|
+
If you need to tune how often the provider polls for build completion, create the provider singleton explicitly **before** instantiating any builders:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { Duration } from 'aws-cdk-lib';
|
|
208
|
+
import {
|
|
209
|
+
TokenInjectableDockerBuilder,
|
|
210
|
+
TokenInjectableDockerBuilderProvider,
|
|
211
|
+
} from 'token-injectable-docker-builder';
|
|
212
|
+
|
|
213
|
+
const provider = TokenInjectableDockerBuilderProvider.getOrCreate(this, {
|
|
214
|
+
queryInterval: Duration.seconds(15),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
new TokenInjectableDockerBuilder(this, 'ApiImage', {
|
|
218
|
+
path: './src/api',
|
|
219
|
+
provider, // optional — the builder would resolve the same singleton anyway
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
209
223
|
### Simple Usage Example
|
|
210
224
|
|
|
211
225
|
This example demonstrates the basic usage of the `TokenInjectableDockerBuilder`, where a Next.js frontend Docker image requires an API Gateway URL as a build argument to create a reference from the UI to the associated API in a given deployment.
|
|
@@ -234,8 +248,6 @@ export class SimpleStack extends cdk.Stack {
|
|
|
234
248
|
buildArgs: {
|
|
235
249
|
API_URL: api.url, // Pass the API Gateway URL as a build argument
|
|
236
250
|
},
|
|
237
|
-
// Optionally override the default completeness query interval:
|
|
238
|
-
// completenessQueryInterval: cdk.Duration.seconds(45),
|
|
239
251
|
});
|
|
240
252
|
|
|
241
253
|
// Use in ECS
|
|
@@ -287,8 +299,6 @@ class SimpleStack(cdk.Stack):
|
|
|
287
299
|
build_args={
|
|
288
300
|
"API_URL": api.url, # Pass the API Gateway URL as a build argument
|
|
289
301
|
},
|
|
290
|
-
# Optionally override the default completeness query interval:
|
|
291
|
-
# completeness_query_interval=Duration.seconds(45)
|
|
292
302
|
)
|
|
293
303
|
|
|
294
304
|
# Use in ECS
|
|
@@ -363,8 +373,6 @@ export class AdvancedStack extends cdk.Stack {
|
|
|
363
373
|
// Replace with your actual command to fetch configs
|
|
364
374
|
'curl -o config.json https://internal-api.example.com/config',
|
|
365
375
|
],
|
|
366
|
-
// Optionally override the default completeness query interval:
|
|
367
|
-
// completenessQueryInterval: cdk.Duration.seconds(45),
|
|
368
376
|
});
|
|
369
377
|
|
|
370
378
|
// Use in ECS
|
|
@@ -432,8 +440,6 @@ class AdvancedStack(cdk.Stack):
|
|
|
432
440
|
# Replace with your actual command to fetch configs
|
|
433
441
|
'curl -o config.json https://internal-api.example.com/config',
|
|
434
442
|
],
|
|
435
|
-
# Optionally override the default completeness query interval:
|
|
436
|
-
# completeness_query_interval=Duration.seconds(45)
|
|
437
443
|
)
|
|
438
444
|
|
|
439
445
|
# Use in ECS
|
|
@@ -463,24 +469,19 @@ When your Dockerfile uses base images from an ECR pull-through cache (e.g. to av
|
|
|
463
469
|
|
|
464
470
|
```typescript
|
|
465
471
|
import * as cdk from 'aws-cdk-lib';
|
|
466
|
-
import {
|
|
467
|
-
TokenInjectableDockerBuilder,
|
|
468
|
-
TokenInjectableDockerBuilderProvider,
|
|
469
|
-
} from 'token-injectable-docker-builder';
|
|
472
|
+
import { TokenInjectableDockerBuilder } from 'token-injectable-docker-builder';
|
|
470
473
|
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
471
474
|
|
|
472
475
|
export class PullThroughCacheStack extends cdk.Stack {
|
|
473
476
|
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
|
|
474
477
|
super(scope, id, props);
|
|
475
478
|
|
|
476
|
-
const provider = TokenInjectableDockerBuilderProvider.getOrCreate(this);
|
|
477
479
|
const node20Slim = `${this.account}.dkr.ecr.${this.region}.amazonaws.com/docker-hub/library/node:20-slim`;
|
|
478
480
|
|
|
479
481
|
const apiImage = new TokenInjectableDockerBuilder(this, 'ApiImage', {
|
|
480
482
|
path: './src',
|
|
481
483
|
file: 'api/Dockerfile',
|
|
482
484
|
platform: 'linux/arm64',
|
|
483
|
-
provider,
|
|
484
485
|
buildArgs: { NODE_20_SLIM: node20Slim },
|
|
485
486
|
ecrPullThroughCachePrefixes: ['docker-hub', 'ghcr'],
|
|
486
487
|
});
|
|
@@ -503,6 +504,58 @@ In this advanced example:
|
|
|
503
504
|
|
|
504
505
|
---
|
|
505
506
|
|
|
507
|
+
### Cross-Region Replication
|
|
508
|
+
|
|
509
|
+
Set `replicaRegions` to make the built image available in additional regions, then reference it from a consumer stack in any of those regions.
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import * as cdk from 'aws-cdk-lib';
|
|
513
|
+
import { TokenInjectableDockerBuilder } from 'token-injectable-docker-builder';
|
|
514
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
515
|
+
|
|
516
|
+
const app = new cdk.App();
|
|
517
|
+
|
|
518
|
+
// Builder stack — us-east-1
|
|
519
|
+
const builderStack = new cdk.Stack(app, 'BuilderStack', {
|
|
520
|
+
env: { account: '123456789012', region: 'us-east-1' },
|
|
521
|
+
crossRegionReferences: true,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const apiImage = new TokenInjectableDockerBuilder(builderStack, 'ApiImage', {
|
|
525
|
+
path: './src/api',
|
|
526
|
+
replicaRegions: ['us-west-2', 'eu-west-1'],
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Consumer stack — us-west-2
|
|
530
|
+
const consumerStack = new cdk.Stack(app, 'ConsumerStack', {
|
|
531
|
+
env: { account: '123456789012', region: 'us-west-2' },
|
|
532
|
+
crossRegionReferences: true,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
new lambda.DockerImageFunction(consumerStack, 'ApiLambda', {
|
|
536
|
+
code: apiImage.dockerImageCodeFor(consumerStack, 'us-west-2'),
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**How it works**
|
|
541
|
+
|
|
542
|
+
1. The builder pushes to its primary-region ECR repository as usual.
|
|
543
|
+
2. The provider singleton manages a single registry-replication custom resource that calls `PutReplicationConfiguration` with merged rules (one rule per unique destination set, filtered to each managed repository name).
|
|
544
|
+
3. ECR asynchronously replicates the image to every region in `replicaRegions`. Most images replicate in under 30 minutes; rare cases take longer.
|
|
545
|
+
4. The build's `isComplete` Lambda polls each replica region's ECR via `BatchGetImage` and only returns `IsComplete=true` once every replica has the tag. The Provider's `totalTimeout` is bumped to 1 hour to accommodate replication lag.
|
|
546
|
+
5. The consumer stack's `dockerImageCodeFor` / `containerImageFor` imports the replicated repository by name in the consumer's region and references the tag via CDK's cross-region SSM mechanism.
|
|
547
|
+
|
|
548
|
+
**Caveats to read before enabling**
|
|
549
|
+
|
|
550
|
+
- **Stacks must have a concrete `env`**: env-agnostic stacks (where `region` is a token) don't work with `crossRegionReferences`. Pass `env: { account, region }` explicitly on both builder and consumer stacks.
|
|
551
|
+
- **Replicas don't inherit settings**: ECR replication does NOT copy encryption (KMS), lifecycle policies, or repository policies. Replicated repos default to AES-256 encryption with no lifecycle rules. If you need stricter replica configuration, set up [ECR repository creation templates](https://docs.aws.amazon.com/AmazonECR/latest/userguide/repository-creation-templates.html) separately.
|
|
552
|
+
- **Replicas persist on stack delete**: AWS does NOT auto-delete replicated repositories when the source replication rule is removed. After `cdk destroy`, manually delete leftover repos: `aws ecr delete-repository --region <replica> --repository-name <name> --force`.
|
|
553
|
+
- **Registry-level limits enforced at synth time**: ECR allows 10 rules per registry and 25 unique destinations across all rules per AWS account. The construct enforces both caps **at synth time** — `cdk synth` will throw a clear error if the union of `replicaRegions` across all builders in the stack would exceed either limit, instead of failing at deploy time inside the replication CR. Rule-grouping by destination set keeps you under the 10-rules cap until you have more than 10 *distinct* destination sets (e.g. ten builders each replicating to a different single region). Note that this is a best-effort check: rules created outside this construct (other stacks, manual setup) aren't visible at synth time, so the runtime API can still surface them.
|
|
554
|
+
- **Cross-partition is unsupported**: e.g. `us-east-1` → `cn-north-1` won't work. The construct will throw at synth time when both partition values are concrete and differ.
|
|
555
|
+
- **Replication latency**: deploys can extend by up to ~30 min in rare cases while `isComplete` waits for replicas. The CDK Provider framework caps `totalTimeout` at 2 hours.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
506
559
|
## How It Works
|
|
507
560
|
|
|
508
561
|
1. **Docker Source**: Packages the source code or Dockerfile specified in the `path` property as an S3 asset.
|
|
@@ -511,22 +564,25 @@ In this advanced example:
|
|
|
511
564
|
- Executes any custom `installCommands` and `preBuildCommands` during the build process.
|
|
512
565
|
- Pushes the image to an ECR repository.
|
|
513
566
|
- By default, uses `docker buildx` with ECR registry cache to speed up builds.
|
|
514
|
-
3. **Custom Resource
|
|
515
|
-
- Triggers the build
|
|
516
|
-
- Monitors
|
|
517
|
-
-
|
|
567
|
+
3. **Custom Resource** (one pair of Lambdas per stack):
|
|
568
|
+
- Triggers the build using `onEvent`.
|
|
569
|
+
- Monitors build status using `isComplete`, polling at the interval set on the provider singleton (defaults to 30 seconds; override via `TokenInjectableDockerBuilderProvider.getOrCreate(this, { queryInterval })`).
|
|
570
|
+
- The same Lambda pair handles every builder in the stack — they are not duplicated per builder.
|
|
518
571
|
4. **Outputs**:
|
|
519
572
|
- `.containerImage`: Returns the Docker image for ECS.
|
|
520
573
|
- `.dockerImageCode`: Returns the Docker image code for Lambda.
|
|
521
574
|
|
|
522
575
|
### Resource Comparison
|
|
523
576
|
|
|
524
|
-
|
|
577
|
+
The provider singleton means a stack's Lambda overhead is fixed — adding more builders only adds CodeBuild projects and ECR repositories.
|
|
578
|
+
|
|
579
|
+
| Scenario | Lambdas (total) | CodeBuild Projects | ECR Repos |
|
|
525
580
|
|---|---|---|---|
|
|
526
|
-
|
|
|
527
|
-
| 5 images
|
|
528
|
-
| 10 images
|
|
529
|
-
|
|
581
|
+
| 1 image | 5 (2 user + 3 framework) | 1 | 1 |
|
|
582
|
+
| 5 images | 5 | 5 | 5 |
|
|
583
|
+
| 10 images | 5 | 10 | 10 |
|
|
584
|
+
|
|
585
|
+
The 3 framework Lambdas are CDK's [`Provider`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.custom_resources.Provider.html) framework internals (`framework.onEvent`, `framework.isComplete`, `framework.onTimeout`).
|
|
530
586
|
|
|
531
587
|
---
|
|
532
588
|
|
|
@@ -539,40 +595,69 @@ The construct automatically grants permissions for:
|
|
|
539
595
|
- Pull from ECR pull-through cache prefixes when `ecrPullThroughCachePrefixes` is provided (e.g. `['docker-hub', 'ghcr']`).
|
|
540
596
|
- Access to AWS Secrets Manager if `dockerLoginSecretArn` is provided.
|
|
541
597
|
- Access to the KMS key for encryption.
|
|
542
|
-
- **
|
|
598
|
+
- **Shared Provider Lambdas** (one pair per stack):
|
|
543
599
|
- Start and monitor CodeBuild builds.
|
|
544
600
|
- Access CloudWatch Logs.
|
|
545
601
|
- Access to the KMS key for encryption.
|
|
546
602
|
- Pull and push images to ECR.
|
|
547
603
|
|
|
548
|
-
|
|
604
|
+
Every new builder calls `provider.registerProject()` under the hood, incrementally adding `codebuild:StartBuild` for its project ARN and `ecr:PullPush` for its repository.
|
|
549
605
|
|
|
550
606
|
---
|
|
551
607
|
|
|
552
608
|
## Notes
|
|
553
609
|
|
|
554
|
-
- **
|
|
610
|
+
- **Provider Singleton**: One pair of `onEvent` / `isComplete` Lambdas is created the first time a builder is instantiated in a stack and reused by every subsequent builder in the same stack. You generally do not need to touch `TokenInjectableDockerBuilderProvider` directly — only call `getOrCreate` yourself if you want to change `queryInterval`.
|
|
555
611
|
- **Build Arguments**: Pass custom arguments via `buildArgs` as `--build-arg` flags. CDK tokens can be used to inject dynamic values resolved at deployment time.
|
|
556
612
|
- **Custom Commands**: Use `installCommands` and `preBuildCommands` to run custom shell commands during the build process. This can be useful for installing dependencies or fetching configuration files.
|
|
557
613
|
- **VPC Configuration**: If your build process requires access to resources within a VPC, you can specify the VPC, security groups, and subnet selection.
|
|
558
614
|
- **Docker Login**: If you need to log in to a private Docker registry before building the image, provide the ARN of a secret in AWS Secrets Manager containing the Docker credentials.
|
|
559
|
-
- **ECR
|
|
560
|
-
- **Build Query Interval**:
|
|
615
|
+
- **ECR Retention (safe by default)**: Tagged images are kept indefinitely; untagged images are removed after 30 days. There is no count-based expiration — Lambda pins images by digest internally and count-based deletion would silently remove an image that an in-use Lambda is still pinned to, breaking the next config update with `Image ID cannot be found`.
|
|
616
|
+
- **Build Query Interval**: Tune polling frequency with `TokenInjectableDockerBuilderProvider.getOrCreate(this, { queryInterval })`. Call this **before** instantiating any builders, otherwise the builder will create the singleton with the default 30 second interval.
|
|
561
617
|
- **Custom Dockerfile**: Use the `file` property to specify a Dockerfile other than the default `Dockerfile`. This is passed as the `--file` flag to `docker build`.
|
|
562
618
|
- **Docker Layer Caching**: By default, builds use ECR as a remote cache backend (via `docker buildx`), which can reduce build times by up to 25%. Set `cacheDisabled: true` when you need a clean build—for example, when debugging, the cache is corrupted, or after major dependency upgrades.
|
|
563
619
|
- **Platform / Architecture**: Set `platform: 'linux/arm64'` to build ARM64/Graviton images using a native ARM CodeBuild instance. Defaults to `'linux/amd64'` (x86_64). Native builds are faster and cheaper than cross-compilation with QEMU.
|
|
564
|
-
- **Build Log Retention**: Pass `buildLogGroup` with a log group that has RETAIN removal policy to
|
|
620
|
+
- **Build Log Retention**: Pass `buildLogGroup` with a log group that has RETAIN removal policy, or set `retainBuildLogs: true` to let the construct manage a `/docker-builder/<projectName>` log group imperatively (survives rollbacks; 7-day retention).
|
|
565
621
|
- **ECR Pull-Through Cache**: When using ECR pull-through cache for base images (e.g. to avoid Docker Hub rate limits), pass `ecrPullThroughCachePrefixes: ['docker-hub', 'ghcr']` so the CodeBuild role can pull from those cached repositories. Your ECR registry must have a pull-through cache rule and registry policy configured separately.
|
|
566
|
-
|
|
622
|
+
|
|
623
|
+
### Migrating from v1
|
|
624
|
+
|
|
625
|
+
**Recipe for the common case** (you're on `^1.x` and want to move to `^2.x`):
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
npm install token-injectable-docker-builder@^2
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
That's the entire required code change for most users. The construct handles the CFN-level migration internally.
|
|
632
|
+
|
|
633
|
+
**What happens on the first `cdk deploy` after upgrading:**
|
|
634
|
+
|
|
635
|
+
1. Every `BuildTriggerResource` in your stack is **replaced** by `BuildTriggerResourceV2`. CFN does this because the construct deliberately renames its internal custom resource between v1 and v2 — this is what sidesteps CFN's `Modifying service token is not allowed` rule when the CR's serviceToken changes from v1's per-instance provider to v2's singleton provider. **Without this rename, the upgrade would fail at the CFN level for users who didn't pass `provider` explicitly in v1.**
|
|
636
|
+
2. Each replacement triggers **one fresh CodeBuild run per builder** (5–10 min each in parallel). Image tags transition from v1's random-UUID style to v2's deterministic-hash style.
|
|
637
|
+
3. Downstream `DockerImageFunction` / `FargateTaskDefinition` resources update in place to the new tag. Lambda's blue/green update is transparent; ECS does a normal rolling deploy.
|
|
638
|
+
4. v1's per-instance `OnEventHandlerFunction` / `IsCompleteHandlerFunction` / `CustomResourceProvider` Lambdas (one set per builder) are deleted. The singleton at `<Stack>/TokenInjectableDockerBuilderProvider/...` is the only provider left.
|
|
639
|
+
5. ECR repositories keep their logical IDs and contents — **no images are lost**.
|
|
640
|
+
|
|
641
|
+
**Behavior changes that come for free (no code edit needed):**
|
|
642
|
+
|
|
643
|
+
- ECR retention switches from "keep 3 tagged images" (v1 default with `maxImageCount: 3`) to "keep all tagged images, untagged-after-30-days only". v1's count-based expiration could silently delete an image a Lambda was pinned to.
|
|
644
|
+
- `imageTag` is now a deterministic SHA-256 hash of all build inputs. v1 regenerated a UUID on every synth, causing a build on every deploy; v2 only rebuilds when source / buildArgs / platform / commands actually change.
|
|
645
|
+
|
|
646
|
+
**Breaking changes that may require a source edit:**
|
|
647
|
+
|
|
648
|
+
- **`completenessQueryInterval`** was removed from `TokenInjectableDockerBuilderProps`. If you set it, move the value to `TokenInjectableDockerBuilderProvider.getOrCreate(this, { queryInterval })` (call before any builder; it now applies stack-wide). If you didn't set it, no edit needed.
|
|
649
|
+
- **`maxImageCount`** was removed entirely. If you set it, just delete the prop from your builder props (no replacement; the behavior is now non-configurable).
|
|
650
|
+
|
|
651
|
+
**This migration path is exercised end-to-end by `npm run integ-migration`** — see `test/migration/before-v1.ts` and `test/migration/after-v2.ts` for the executable recipe. The literal source-code diff between the two files is the import path; everything else is identical.
|
|
567
652
|
|
|
568
653
|
---
|
|
569
654
|
|
|
570
655
|
## Troubleshooting
|
|
571
656
|
|
|
572
|
-
1. **Build Errors**: Check the CodeBuild logs in CloudWatch Logs for detailed error messages. If you pass `buildLogGroup` with RETAIN removal policy, logs persist even after rollbacks. Otherwise, logs are deleted when the CodeBuild project is removed during rollback.
|
|
573
|
-
2. **Lambda Errors**: Check the `onEvent` and `isComplete` Lambda function logs in CloudWatch Logs
|
|
657
|
+
1. **Build Errors**: Check the CodeBuild logs in CloudWatch Logs for detailed error messages. If you pass `buildLogGroup` with RETAIN removal policy, or set `retainBuildLogs: true`, logs persist even after rollbacks. Otherwise, logs are deleted when the CodeBuild project is removed during rollback.
|
|
658
|
+
2. **Lambda Errors**: Check the singleton `onEvent` and `isComplete` Lambda function logs in CloudWatch Logs (under `TokenInjectableDockerBuilderProvider/...`). All builders in the stack flow through the same Lambdas — filter by `ProjectName` in the logs to isolate a specific builder.
|
|
574
659
|
3. **"Image manifest, config or layer media type not supported" (Lambda)**: Docker Buildx v0.10+ adds provenance attestations by default, producing OCI image indexes that Lambda rejects. This construct disables them with `--provenance=false --sbom=false` so images are Lambda-compatible. If you see this error, ensure you're using a recent version of the construct.
|
|
575
|
-
4. **Permissions**: Ensure IAM roles have the required permissions for CodeBuild, ECR, Secrets Manager, and KMS if applicable.
|
|
660
|
+
4. **Permissions**: Ensure IAM roles have the required permissions for CodeBuild, ECR, Secrets Manager, and KMS if applicable. `registerProject()` is called automatically by the builder's constructor — you do not need to call it manually.
|
|
576
661
|
5. **Network Access**: If the build requires network access (e.g., to download dependencies or access internal APIs), ensure that the VPC configuration allows necessary network connectivity, and adjust security group rules accordingly.
|
|
577
662
|
|
|
578
663
|
---
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ECRClient,
|
|
3
|
+
DescribeRegistryCommand,
|
|
4
|
+
PutReplicationConfigurationCommand,
|
|
5
|
+
} = require('@aws-sdk/client-ecr');
|
|
6
|
+
|
|
7
|
+
const region = process.env.AWS_REGION;
|
|
8
|
+
const ecr = new ECRClient({ region });
|
|
9
|
+
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canonicalDestinationKey(destinations) {
|
|
15
|
+
return destinations
|
|
16
|
+
.map((d) => `${d.region}:${d.registryId}`)
|
|
17
|
+
.sort()
|
|
18
|
+
.join('|');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ruleManagesRepo(rule, repoName) {
|
|
22
|
+
if (!rule.repositoryFilters) return false;
|
|
23
|
+
return rule.repositoryFilters.some(
|
|
24
|
+
(f) => f.filterType === 'PREFIX_MATCH' && f.filter === repoName,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the merged set of rules.
|
|
30
|
+
*
|
|
31
|
+
* `existingRules` is the current registry config returned by DescribeRegistry.
|
|
32
|
+
* `managedSpecs` is the array of { repositoryName, destinations[] } to (re)write.
|
|
33
|
+
* `stripNames` is the set of repo names to remove from existing rules first
|
|
34
|
+
* (used on both Create/Update — strip the previous version of our rules
|
|
35
|
+
* before re-emitting fresh ones — and Delete, where managedSpecs is empty
|
|
36
|
+
* and we only want to strip).
|
|
37
|
+
*
|
|
38
|
+
* Strategy:
|
|
39
|
+
* 1. Strip out any filters matching `stripNames` from existing rules. If a
|
|
40
|
+
* rule's filter list becomes empty, drop the rule entirely.
|
|
41
|
+
* 2. For each managedSpec, group by canonical destination set. One ECR rule
|
|
42
|
+
* per destination set, holding all our managed repo filters that share
|
|
43
|
+
* that destination set. This keeps us under the 10-rules-per-registry cap
|
|
44
|
+
* until the user has more than 10 unique destination sets across all
|
|
45
|
+
* builders in the account.
|
|
46
|
+
* 3. Preserve every other rule untouched.
|
|
47
|
+
*/
|
|
48
|
+
function buildMergedRules(existingRules, managedSpecs, stripNames) {
|
|
49
|
+
const stripSet = stripNames instanceof Set ? stripNames : new Set(stripNames);
|
|
50
|
+
|
|
51
|
+
const preserved = (existingRules || []).filter((rule) => {
|
|
52
|
+
if (!rule.repositoryFilters || rule.repositoryFilters.length === 0) {
|
|
53
|
+
// A rule with no filters means "replicate everything" — not ours.
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const remainingFilters = rule.repositoryFilters.filter(
|
|
57
|
+
(f) => !(f.filterType === 'PREFIX_MATCH' && stripSet.has(f.filter)),
|
|
58
|
+
);
|
|
59
|
+
if (remainingFilters.length === 0) return false;
|
|
60
|
+
rule.repositoryFilters = remainingFilters;
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const groupsByDestKey = new Map();
|
|
65
|
+
for (const spec of managedSpecs) {
|
|
66
|
+
if (!spec.destinations || spec.destinations.length === 0) continue;
|
|
67
|
+
const key = canonicalDestinationKey(spec.destinations);
|
|
68
|
+
let group = groupsByDestKey.get(key);
|
|
69
|
+
if (!group) {
|
|
70
|
+
group = { destinations: spec.destinations, repos: new Set() };
|
|
71
|
+
groupsByDestKey.set(key, group);
|
|
72
|
+
}
|
|
73
|
+
group.repos.add(spec.repositoryName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const generated = [];
|
|
77
|
+
for (const group of groupsByDestKey.values()) {
|
|
78
|
+
generated.push({
|
|
79
|
+
destinations: group.destinations,
|
|
80
|
+
repositoryFilters: [...group.repos].map((repoName) => ({
|
|
81
|
+
filter: repoName,
|
|
82
|
+
filterType: 'PREFIX_MATCH',
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...preserved, ...generated];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function putWithRetry(rules) {
|
|
91
|
+
const maxAttempts = 5;
|
|
92
|
+
let lastErr;
|
|
93
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
94
|
+
try {
|
|
95
|
+
await ecr.send(new PutReplicationConfigurationCommand({
|
|
96
|
+
replicationConfiguration: { rules },
|
|
97
|
+
}));
|
|
98
|
+
return;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
lastErr = err;
|
|
101
|
+
const retryable = err.name === 'LimitExceededException'
|
|
102
|
+
|| err.name === 'ValidationException'
|
|
103
|
+
|| err.name === 'ServerException'
|
|
104
|
+
|| err.name === 'ThrottlingException';
|
|
105
|
+
if (!retryable) throw err;
|
|
106
|
+
const delay = 500 * 2 ** attempt;
|
|
107
|
+
console.warn(`PutReplicationConfiguration ${err.name}, retrying in ${delay}ms`);
|
|
108
|
+
await sleep(delay);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw lastErr;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
exports.handler = async (event) => {
|
|
115
|
+
console.log('Event:', JSON.stringify(event, null, 2));
|
|
116
|
+
|
|
117
|
+
const physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId;
|
|
118
|
+
const isDelete = event.RequestType === 'Delete';
|
|
119
|
+
|
|
120
|
+
// ResourceProperties.ManagedSpecs is present on every event type — for
|
|
121
|
+
// Delete events, CFN sends the last-known property values, so we can
|
|
122
|
+
// still identify which repo names are "ours" and strip them.
|
|
123
|
+
const currentManagedSpecs = JSON.parse(
|
|
124
|
+
event.ResourceProperties?.ManagedSpecs || '[]',
|
|
125
|
+
);
|
|
126
|
+
const previousManagedSpecs = JSON.parse(
|
|
127
|
+
event.OldResourceProperties?.ManagedSpecs || '[]',
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// Names to strip from existing rules: union of current and previous repo
|
|
131
|
+
// names. On Create/Update: strip the prior version of our rules and
|
|
132
|
+
// re-emit fresh ones. On Delete: strip everything we ever managed.
|
|
133
|
+
const stripNames = new Set([
|
|
134
|
+
...currentManagedSpecs.map((s) => s.repositoryName),
|
|
135
|
+
...previousManagedSpecs.map((s) => s.repositoryName),
|
|
136
|
+
]);
|
|
137
|
+
const specsToWrite = isDelete ? [] : currentManagedSpecs;
|
|
138
|
+
|
|
139
|
+
const describe = await ecr.send(new DescribeRegistryCommand({}));
|
|
140
|
+
const existingRules = describe.replicationConfiguration?.rules || [];
|
|
141
|
+
|
|
142
|
+
const merged = buildMergedRules(existingRules, specsToWrite, stripNames);
|
|
143
|
+
console.log('Merged rules:', JSON.stringify(merged, null, 2));
|
|
144
|
+
|
|
145
|
+
await putWithRetry(merged);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
PhysicalResourceId: physicalResourceId,
|
|
149
|
+
Data: {
|
|
150
|
+
RulesApplied: String(merged.length),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Exported for unit tests; not part of the Lambda handler contract.
|
|
156
|
+
exports._internal = { buildMergedRules, canonicalDestinationKey };
|