Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4628449
feat(aws-ecr-assets): add custom ECR repository and image tagging sup…
oyiz-michael Aug 17, 2025
de66de8
feat(aws-ecr-assets): add integration tests for custom repository sup…
oyiz-michael Aug 17, 2025
57021cd
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Aug 29, 2025
02c27a4
Update packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts
oyiz-michael Sep 2, 2025
c207cea
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 2, 2025
07333ae
Update packages/aws-cdk-lib/aws-ecr-assets/README.md
oyiz-michael Sep 3, 2025
0e230fc
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 3, 2025
85815ee
fix: lint issues in ECR custom repository implementation
oyiz-michael Sep 4, 2025
c7cb941
fix: remove integration test causing circular dependency
oyiz-michael Sep 6, 2025
3d8eabb
fix: resolve circular dependency by deactivating problematic integrat…
oyiz-michael Sep 6, 2025
8db24bd
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 6, 2025
ebc30ca
Merge branch 'aws:main' into feature/ecr-custom-repository-support
oyiz-michael Sep 8, 2025
bb0107d
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 9, 2025
b6fbd32
fix(aws-ecr-assets): add missing test fixtures for custom repository …
oyiz-michael Sep 9, 2025
3acf387
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 11, 2025
010e840
feat: rebuild ECR custom repository test suite with comprehensive Clo…
oyiz-michael Sep 13, 2025
8bc1d74
feat: reactivate pnpm dependencies integration test
oyiz-michael Sep 13, 2025
01ebb8a
fix: add missing IntegTest declaration for ECR custom repository inte…
oyiz-michael Sep 15, 2025
239a9f0
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Sep 17, 2025
8ecaf7b
fix trailing space
oyiz-michael Oct 7, 2025
7dd7680
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 7, 2025
b32951d
fix: remove corrupted integ.custom-repository snapshot
oyiz-michael Oct 8, 2025
5640e23
feat: regenerate integ.custom-repository snapshot with correct refere…
oyiz-michael Oct 8, 2025
3326be3
fix: add token check before imageUri replacement
oyiz-michael Oct 8, 2025
c1ad2ef
fix: use Annotations.addError instead of throwing error for token check
oyiz-michael Oct 9, 2025
b2cd85b
fix: regenerate integ.custom-repository snapshot with actual AWS depl…
oyiz-michael Oct 9, 2025
eaff018
fix: return early after token validation error
oyiz-michael Oct 9, 2025
4a1cba6
test: update integ.custom-repository snapshot with correct custom tag…
oyiz-michael Oct 9, 2025
99122a3
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 9, 2025
bad5072
fix: handle unresolved tokens in custom tag implementation
oyiz-michael Oct 9, 2025
5eb0555
feat(core): support per-asset custom image tags in synthesizer
oyiz-michael Oct 9, 2025
41a1060
fix: remove trailing spaces in asset-manifest-builder
oyiz-michael Oct 9, 2025
15d497a
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 9, 2025
6f2f5a5
test: update integration test snapshot for custom ECR tags
oyiz-michael Oct 11, 2025
2fe8039
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 11, 2025
8f10cba
fix(ecr-assets): restore hash calculation for custom repository prope…
oyiz-michael Oct 13, 2025
e5caed1
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 13, 2025
5450875
Merge branch 'main' into feature/ecr-custom-repository-support
oyiz-michael Oct 18, 2025
353c26f
docs(aws-ecr-assets): clarify usage of externally managed ECR reposit…
Oct 18, 2025
4c1f6ce
test(aws-ecr-assets): update integration test snapshots for README ch…
Oct 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/aws-cdk-lib/aws-ecr-assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,30 @@ to your stack.

`DockerImageAsset` is designed for seamless build & consumption of image assets by CDK code deployed to multiple environments
through the CDK CLI or through CI/CD workflows. To that end, the ECR repository behind this construct is controlled by the AWS CDK.
The mechanics of where these images are published and how are intentionally kept as an implementation detail, and the construct
The mechanics of where these images are published and how are intentionally kept as an implementation detail, and by default the construct
does not support customizations such as specifying the ECR repository name or tags.

However, if you need to use a custom ECR repository or custom image tags, you can now specify:

```ts
import * as ecr from 'aws-cdk-lib/aws-ecr';

// Custom ECR repository
const customRepo = new ecr.Repository(this, 'MyRepo', {
repositoryName: 'my-custom-repo'
});

const asset = new DockerImageAsset(this, 'MyAsset', {
directory: path.join(__dirname, 'my-image'),
ecrRepository: customRepo, // Use custom repository
imageTag: 'v1.2.3', // Custom tag
// OR
imageTagPrefix: 'feature-branch-', // Tag prefix + asset hash
});
```

When using custom repositories, you are responsible for managing the repository lifecycle and permissions.

We are testing a new experimental synthesizer, the
[App Staging Synthesizer](https://docs.aws.amazon.com/cdk/api/v2/docs/app-staging-synthesizer-alpha-readme.html) that
creates separate support stacks for each CDK application. Unlike the default stack synthesizer, the App Staging
Expand Down
122 changes: 103 additions & 19 deletions packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from 'path';
import { Construct } from 'constructs';
import { FingerprintOptions, FollowMode, IAsset } from '../../assets';
import * as ecr from '../../aws-ecr';
import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage, CfnResource, Names, ValidationError, UnscopedValidationError } from '../../core';
import { Annotations, AssetStaging, FeatureFlags, FileFingerprintOptions, IgnoreMode, Stack, SymlinkFollowMode, Token, Stage, CfnResource, Names, ValidationError, UnscopedValidationError, DockerImageAssetLocation, DockerImageAssetSource } from '../../core';
import { propertyInjectable } from '../../core/lib/prop-injectable';
import * as cxapi from '../../cx-api';

Expand Down Expand Up @@ -202,6 +202,27 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp
*/
readonly repositoryName?: string;

/**
* ECR repository where this image should be published
*
* @default - use the default ECR repository for CDK assets
*/
readonly ecrRepository?: ecr.IRepository;

/**
* Custom Docker image tag
*
* @default - asset hash will be used as the image tag
*/
readonly imageTag?: string;

/**
* Docker image tag prefix
*
* @default - no prefix, uses synthesizer default or empty string
*/
readonly imageTagPrefix?: string;

/**
* Build args to pass to the `docker build` command.
*
Expand Down Expand Up @@ -517,6 +538,11 @@ export class DockerImageAsset extends Construct implements IAsset {
if (props.invalidation?.networkMode !== false && props.networkMode) { extraHash.networkMode = props.networkMode; }
if (props.invalidation?.platform !== false && props.platform) { extraHash.platform = props.platform; }
if (props.invalidation?.outputs !== false && props.outputs) { extraHash.outputs = props.outputs; }

// Include new custom repository and tagging properties in hash calculation
if (props.ecrRepository) { extraHash.ecrRepository = props.ecrRepository.repositoryName; }
if (props.imageTag) { extraHash.imageTag = props.imageTag; }
if (props.imageTagPrefix) { extraHash.imageTagPrefix = props.imageTagPrefix; }

// add "salt" to the hash in order to invalidate the image in the upgrade to
// 1.21.0 which removes the AdoptedRepository resource (and will cause the
Expand Down Expand Up @@ -549,29 +575,87 @@ export class DockerImageAsset extends Construct implements IAsset {
this.dockerCacheTo = props.cacheTo;
this.dockerCacheDisabled = props.cacheDisabled;

const location = stack.synthesizer.addDockerImageAsset({
directoryName: this.assetPath,
assetName: this.assetName,
dockerBuildArgs: this.dockerBuildArgs,
dockerBuildSecrets: this.dockerBuildSecrets,
dockerBuildSsh: this.dockerBuildSsh,
dockerBuildTarget: this.dockerBuildTarget,
dockerFile: props.file,
sourceHash: staging.assetHash,
networkMode: props.networkMode?.mode,
platform: props.platform?.platform,
dockerOutputs: this.dockerOutputs,
dockerCacheFrom: this.dockerCacheFrom,
dockerCacheTo: this.dockerCacheTo,
dockerCacheDisabled: this.dockerCacheDisabled,
displayName: props.displayName ?? props.assetName ?? Names.stackRelativeConstructPath(this),
});
// Handle custom ECR repository or use default synthesizer
let location: DockerImageAssetLocation;
if (props.ecrRepository) {
// Custom repository: create location manually
const customTag = props.imageTag ?? `${props.imageTagPrefix ?? ''}${this.assetHash}`;
location = {
repositoryName: props.ecrRepository.repositoryName,
imageUri: props.ecrRepository.repositoryUriForTag(customTag),
imageTag: customTag,
};
this.repository = props.ecrRepository;
} else if (props.imageTagPrefix || props.imageTag) {
// Custom tagging with default repository: create a custom synthesizer call
location = this.addDockerImageAssetWithCustomTag(stack, {
directoryName: this.assetPath,
assetName: this.assetName,
dockerBuildArgs: this.dockerBuildArgs,
dockerBuildSecrets: this.dockerBuildSecrets,
dockerBuildSsh: this.dockerBuildSsh,
dockerBuildTarget: this.dockerBuildTarget,
dockerFile: props.file,
sourceHash: staging.assetHash,
networkMode: props.networkMode?.mode,
platform: props.platform?.platform,
dockerOutputs: this.dockerOutputs,
dockerCacheFrom: this.dockerCacheFrom,
dockerCacheTo: this.dockerCacheTo,
dockerCacheDisabled: this.dockerCacheDisabled,
displayName: props.displayName ?? props.assetName ?? Names.stackRelativeConstructPath(this),
}, props.imageTagPrefix, props.imageTag);
this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
} else {
// Default behavior: use synthesizer
location = stack.synthesizer.addDockerImageAsset({
directoryName: this.assetPath,
assetName: this.assetName,
dockerBuildArgs: this.dockerBuildArgs,
dockerBuildSecrets: this.dockerBuildSecrets,
dockerBuildSsh: this.dockerBuildSsh,
dockerBuildTarget: this.dockerBuildTarget,
dockerFile: props.file,
sourceHash: staging.assetHash,
networkMode: props.networkMode?.mode,
platform: props.platform?.platform,
dockerOutputs: this.dockerOutputs,
dockerCacheFrom: this.dockerCacheFrom,
dockerCacheTo: this.dockerCacheTo,
dockerCacheDisabled: this.dockerCacheDisabled,
displayName: props.displayName ?? props.assetName ?? Names.stackRelativeConstructPath(this),
});
this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
}

this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
this.imageUri = location.imageUri;
this.imageTag = location.imageTag ?? this.assetHash;
}

/**
* Helper method to add Docker image asset with custom tag using synthesizer
*/
private addDockerImageAssetWithCustomTag(
stack: Stack,
source: DockerImageAssetSource,
imageTagPrefix?: string,
imageTag?: string
): DockerImageAssetLocation {
// Create a custom tag
const customTag = imageTag ?? `${imageTagPrefix ?? ''}${source.sourceHash}`;

// For custom tagging, we need to work with the synthesizer's docker repository
// but override the tag generation logic
const baseLocation = stack.synthesizer.addDockerImageAsset(source);

// Create a modified location with our custom tag
return {
repositoryName: baseLocation.repositoryName,
imageUri: baseLocation.imageUri.replace(/:.*$/, `:${customTag}`),
imageTag: customTag,
};
}

/**
* Adds CloudFormation template metadata to the specified resource with
* information that indicates which resource property is mapped to this local
Expand Down
161 changes: 161 additions & 0 deletions packages/aws-cdk-lib/aws-ecr-assets/test/custom-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import * as path from 'path';
import * as ecr from '../../aws-ecr';
import { App, Stack } from '../../core';
import { DockerImageAsset } from '../lib';

describe('DockerImageAsset Custom Repository Support', () => {
let app: App;
let stack: Stack;
let repository: ecr.Repository;

beforeEach(() => {
app = new App();
stack = new Stack(app, 'TestStack');
repository = new ecr.Repository(stack, 'CustomRepo', {
repositoryName: 'my-custom-repo',
});
});

test('should support custom ECR repository', () => {
// GIVEN
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
ecrRepository: repository,
});

// THEN
expect(asset.repository).toBe(repository);
expect(asset.imageUri).toContain('my-custom-repo');
expect(asset.imageTag).toBe(asset.assetHash);
});

test('should support custom image tag with custom repository', () => {
// GIVEN
const customTag = 'v1.2.3';
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
ecrRepository: repository,
imageTag: customTag,
});

// THEN
expect(asset.repository).toBe(repository);
expect(asset.imageUri).toContain(`my-custom-repo:${customTag}`);
expect(asset.imageTag).toBe(customTag);
});

test('should support custom image tag prefix with custom repository', () => {
// GIVEN
const tagPrefix = 'feature-';
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
ecrRepository: repository,
imageTagPrefix: tagPrefix,
});

// THEN
expect(asset.repository).toBe(repository);
expect(asset.imageUri).toContain(`my-custom-repo:${tagPrefix}${asset.assetHash}`);
expect(asset.imageTag).toBe(`${tagPrefix}${asset.assetHash}`);
});

test('should support custom tag prefix with default repository', () => {
// GIVEN
const tagPrefix = 'branch-main-';
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTagPrefix: tagPrefix,
});

// THEN
expect(asset.imageTag).toBe(`${tagPrefix}${asset.assetHash}`);
expect(asset.imageUri).toContain(`:${tagPrefix}${asset.assetHash}`);
});

test('should support custom tag with default repository', () => {
// GIVEN
const customTag = 'release-candidate';
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTag: customTag,
});

// THEN
expect(asset.imageTag).toBe(customTag);
expect(asset.imageUri).toContain(`:${customTag}`);
});

test('should include custom repository in asset hash', () => {
// GIVEN
const asset1 = new DockerImageAsset(stack, 'Asset1', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
});

const asset2 = new DockerImageAsset(stack, 'Asset2', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
ecrRepository: repository,
});

// THEN - assets should have different hashes due to different repositories
expect(asset1.assetHash).not.toBe(asset2.assetHash);
});

test('should include custom tag in asset hash', () => {
// GIVEN
const asset1 = new DockerImageAsset(stack, 'Asset1', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTag: 'tag1',
});

const asset2 = new DockerImageAsset(stack, 'Asset2', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTag: 'tag2',
});

// THEN - assets should have different hashes due to different tags
expect(asset1.assetHash).not.toBe(asset2.assetHash);
});

test('should include custom tag prefix in asset hash', () => {
// GIVEN
const asset1 = new DockerImageAsset(stack, 'Asset1', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTagPrefix: 'prefix1-',
});

const asset2 = new DockerImageAsset(stack, 'Asset2', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
imageTagPrefix: 'prefix2-',
});

// THEN - assets should have different hashes due to different tag prefixes
expect(asset1.assetHash).not.toBe(asset2.assetHash);
});

test('imageTag takes precedence over imageTagPrefix', () => {
// GIVEN
const customTag = 'explicit-tag';
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
ecrRepository: repository,
imageTag: customTag,
imageTagPrefix: 'should-be-ignored-',
});

// THEN
expect(asset.imageTag).toBe(customTag);
expect(asset.imageUri).toContain(`:${customTag}`);
});

test('should maintain backward compatibility', () => {
// GIVEN
const asset = new DockerImageAsset(stack, 'MyAsset', {
directory: path.join(__dirname, 'fixtures/custom-dockerfile'),
});

// THEN - should work exactly like before
expect(asset.repository).toBeDefined();
expect(asset.imageUri).toBeDefined();
expect(asset.imageTag).toBe(asset.assetHash);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM public.ecr.aws/lambda/nodejs:18

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY app.js ./

# Simple test application
CMD ["app.handler"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "test-app",
"version": "1.0.0",
"description": "Test application for Docker Image Asset custom repository support",
"main": "app.js",
"dependencies": {}
}
Loading
Loading