Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env node
import * as path from 'path';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as cdk from 'aws-cdk-lib';
import * as assets from 'aws-cdk-lib/aws-ecr-assets';
import { IntegTest } from '@aws-cdk/integ-tests-alpha';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'integ-custom-repository');

// Create a custom ECR repository
const customRepo = new ecr.Repository(stack, 'CustomRepo', {
repositoryName: 'cdk-integ-custom-repo',
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// Test 1: Basic custom repository usage
const basicCustomAsset = new assets.DockerImageAsset(stack, 'BasicCustomAsset', {
directory: path.join(__dirname, 'demo-image'),
ecrRepository: customRepo,
});

// Test 2: Custom repository with explicit tag
const taggedCustomAsset = new assets.DockerImageAsset(stack, 'TaggedCustomAsset', {
directory: path.join(__dirname, 'demo-image'),
ecrRepository: customRepo,
imageTag: 'v1.2.3',
});

// Test 3: Custom repository with tag prefix
const prefixedCustomAsset = new assets.DockerImageAsset(stack, 'PrefixedCustomAsset', {
directory: path.join(__dirname, 'demo-image'),
ecrRepository: customRepo,
imageTagPrefix: 'branch-main-',
});

// Test 4: Default repository with custom tag
const defaultRepoCustomTag = new assets.DockerImageAsset(stack, 'DefaultRepoCustomTag', {
directory: path.join(__dirname, 'demo-image'),
imageTag: 'custom-tag-v2',
});

// Test 5: Default repository with tag prefix
const defaultRepoPrefixed = new assets.DockerImageAsset(stack, 'DefaultRepoPrefixed', {
directory: path.join(__dirname, 'demo-image'),
imageTagPrefix: 'feature-',
});

// Test 6: Verify imageTag takes precedence over imageTagPrefix
const precedenceTest = new assets.DockerImageAsset(stack, 'PrecedenceTest', {
directory: path.join(__dirname, 'demo-image'),
ecrRepository: customRepo,
imageTag: 'explicit-wins',
imageTagPrefix: 'ignored-',
});

// Output key values for verification
new cdk.CfnOutput(stack, 'CustomRepoName', {
value: customRepo.repositoryName,
});

new cdk.CfnOutput(stack, 'BasicCustomAssetUri', {
value: basicCustomAsset.imageUri,
});

new cdk.CfnOutput(stack, 'TaggedCustomAssetTag', {
value: taggedCustomAsset.imageTag,
});

new cdk.CfnOutput(stack, 'PrefixedCustomAssetTag', {
value: prefixedCustomAsset.imageTag,
});

new cdk.CfnOutput(stack, 'DefaultRepoCustomTagTag', {
value: defaultRepoCustomTag.imageTag,
});

new cdk.CfnOutput(stack, 'DefaultRepoPrefixedTag', {
value: defaultRepoPrefixed.imageTag,
});

new cdk.CfnOutput(stack, 'PrecedenceTestTag', {
value: precedenceTest.imageTag,
});

// Register the integration test
new IntegTest(app, 'EcrCustomRepositoryTest', {
testCases: [stack],
});
23 changes: 21 additions & 2 deletions packages/aws-cdk-lib/aws-ecr-assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,27 @@ using alternative container runtimes like Finch.

`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
does not support customizations such as specifying the ECR repository name or tags.
The mechanics of where these images are published and how are intentionally kept as an implementation detail, and by default the construct itself sets the ECR repository, name, and tags.

However, if you need to use your own custom ECR repository or custom image tags, you can specify the `ecrRepository`, `imageTag`, or `imageTagPrefix` properties explicitly.

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

```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
});

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
Expand Down
79 changes: 74 additions & 5 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 @@ -477,7 +498,7 @@ export class DockerImageAsset extends Construct implements IAsset {

const defaultIgnoreMode = FeatureFlags.of(this).isEnabled(cxapi.DOCKER_IGNORE_SUPPORT)
? IgnoreMode.DOCKER : IgnoreMode.GLOB;
let ignoreMode = props.ignoreMode ?? defaultIgnoreMode;
const ignoreMode = props.ignoreMode ?? defaultIgnoreMode;

let exclude: string[] = props.exclude || [];

Expand Down Expand Up @@ -518,6 +539,11 @@ export class DockerImageAsset extends Construct implements IAsset {
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
// deletion of the ECR repository the app used).
Expand Down Expand Up @@ -549,7 +575,9 @@ export class DockerImageAsset extends Construct implements IAsset {
this.dockerCacheTo = props.cacheTo;
this.dockerCacheDisabled = props.cacheDisabled;

const location = stack.synthesizer.addDockerImageAsset({
// Handle custom ECR repository or use default synthesizer
let location: DockerImageAssetLocation;
const locationProps = {
directoryName: this.assetPath,
assetName: this.assetName,
dockerBuildArgs: this.dockerBuildArgs,
Expand All @@ -565,13 +593,54 @@ export class DockerImageAsset extends Construct implements IAsset {
dockerCacheTo: this.dockerCacheTo,
dockerCacheDisabled: this.dockerCacheDisabled,
displayName: props.displayName ?? props.assetName ?? Names.stackRelativeConstructPath(this),
});
};
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, locationProps, props.imageTagPrefix, props.imageTag);
this.repository = ecr.Repository.fromRepositoryName(this, 'Repository', location.repositoryName);
} else {
// Default behavior: use synthesizer
location = stack.synthesizer.addDockerImageAsset(locationProps);
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
Loading