Skip to content

Commit 11ed691

Browse files
allisaurusrix0rrr
authored andcommitted
feat(ecs): support private registry authentication (#1737)
Non-ECR registries now accept a secretsmanager Secret with a username and password to use for registry authentication. Fixes #1698. BREAKING CHANGE: `ContainerImage.fromDockerHub` has been renamed to `ContainerImage.fromRegistry`.
1 parent bfb14b6 commit 11ed691

24 files changed

+298
-85
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# AWS ECS - Support for Private Registry Authentication
2+
3+
To address issue [#1698](https://github.com/awslabs/aws-cdk/issues/1698), the ECS construct library should provide a way for customers to specify [`repositoryCredentials`](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerDefinition.html#ECS-Type-ContainerDefinition-repositoryCredentials) on their container.
4+
5+
Minimally, this would mean adding a new string field on `ContainerDefinition`, however this doesn't provide any added value in terms of logical grouping or resource creation. We can instead modify the existing ECS CDK construct [`ContainerImage`](https://github.com/awslabs/aws-cdk/blob/master/packages/%40aws-cdk/aws-ecs/lib/container-image.ts) so that repository credentials are specified along with the image they're meant to access.
6+
7+
## General approach
8+
9+
The [`ecs.ContainerImage`](https://github.com/awslabs/aws-cdk/blob/master/packages/%40aws-cdk/aws-ecs/lib/container-image.ts) class already includes constructs for 3 types of images:
10+
11+
* DockerHubImage
12+
* EcrImage
13+
* AssetImage
14+
15+
DockerHub images are assumed public, however DockerHub also provides private repositories and there's currently no way to specify credentials for them in the CDK.
16+
17+
There's also no explicit way to specify images hosted outside of DockerHub, AWS, or your local machine. Customers hosting their own registries or using another registry host, like Quay.io or JFrog Artifactory, would need to be able to specify both the image URI and the registry credentials in order to pull their images down for ECS tasks.
18+
19+
Fundamentally, specifying images hosted in DockerHub or elsewhere works the same, because when passed an image URI vs. a plain (or namespaced) image name + tag, the Docker daemon does the right thing and tries to pull the image from the specified registery.
20+
21+
Therefore, we should rename the existing `DockerHubImage` type be more generic and add the ability to optionally specify credentials.
22+
23+
24+
## Code changes
25+
26+
Given the above, we should make the following changes to support private registry authentication:
27+
28+
1. Define `RepositoryCredentials` interface & class, add to `IContainerImage`
29+
2. Rename `DockerHubImage` construct to be more generic, optionally accept and set `RepositoryCreds`
30+
31+
32+
# Part 1: How to define registry credentials
33+
34+
For extensibility, we can define a new `IRepositoryCreds` interface to house the AWS Secrets Manager secret with the creds and a new `RepositoryCreds` class which satisfies it using specific constructs (e.g., "fromSecret").
35+
36+
```ts
37+
export interface IRepositoryCreds {
38+
readonly secret: secretsManager.Secret;
39+
}
40+
41+
export class RepositoryCreds {
42+
public readonly secret: secretsManager.Secret;
43+
44+
public static fromSecret(secret: secretsManager.Secret) {
45+
this.secret = secret;
46+
}
47+
48+
public bind(containerDefinition: ContainerDefinition): void {
49+
// grant the execution role read access so the secret can be read prior to image pull
50+
this.secret.grantRead(containerDefinition.taskDefinition.obtainExecutionRole());
51+
}
52+
}
53+
```
54+
55+
The `IContainerImage` interface will be updated to include an optional `repositoryCredentials` property:
56+
```ts
57+
export interface IContainerImage {
58+
// ...
59+
readonly imageName: string;
60+
61+
/**
62+
* NEW: The credentials required to access the image
63+
*/
64+
readonly repositoryCredentials?: IRepositoryCreds;
65+
66+
// ...
67+
bind(containerDefinition: ContainerDefinition): void;
68+
}
69+
```
70+
71+
72+
# Part 2: Rename `DockerHubImage` class to `WebHostedImage`, add optional creds
73+
74+
The `DockerHubImage` construct will be renamed to `WebHostedImage`, and augmented to take in optional "credentials" via keyword props:
75+
```ts
76+
// define props
77+
export interface WebHostedImageProps {
78+
credentials: RepositoryCreds;
79+
}
80+
81+
// "DockerHubImage" class --> "WebHostedImage"
82+
export class WebHostedImage implements IContainerImage {
83+
public readonly imageName: string;
84+
public readonly credentials: IRepositoryCreds;
85+
86+
// add credentials to constructor
87+
constructor(imageName: string, props: WebHostedImageProps) {
88+
this.imageName = imageName
89+
this.credentials = props.credentials
90+
}
91+
92+
public bind(_containerDefinition: ContainerDefinition): void {
93+
// bind repositoryCredentials to ContainerDefinition
94+
this.repositoryCredentials.bind();
95+
}
96+
}
97+
```
98+
99+
We will also update the API on `ContainerImage` to match:
100+
```ts
101+
export class ContainerImage {
102+
//...
103+
public static fromInternet(imageName: string, props: WebHostedImageProps) {
104+
return new WebHostedImage(imageName, props);
105+
}
106+
// ...
107+
108+
}
109+
```
110+
111+
Example use:
112+
```ts
113+
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef');
114+
115+
const secret = secretsManager.Secret.import(stack, 'myRepoSecret', {
116+
secretArn: 'arn:aws:secretsmanager:.....'
117+
})
118+
119+
taskDefinition.AddContainer('myPrivateContainer', {
120+
image: ecs.ContainerImage.fromInternet('userx/test', {
121+
credentials: ecs.RepositoryCreds.fromSecret(secret)
122+
});
123+
});
124+
125+
```

packages/@aws-cdk/aws-ecs/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ cluster.addDefaultAutoScalingGroupCapacity('Capacity', {
3030
const ecsService = new ecs.LoadBalancedEc2Service(this, 'Service', {
3131
cluster,
3232
memoryLimitMiB: 512,
33-
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
33+
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
3434
});
3535
```
3636

@@ -134,7 +134,7 @@ To add containers to a task definition, call `addContainer()`:
134134
```ts
135135
const container = fargateTaskDefinition.addContainer("WebContainer", {
136136
// Use an image from DockerHub
137-
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
137+
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
138138
// ... other options here ...
139139
});
140140
```
@@ -148,7 +148,7 @@ const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', {
148148

149149
const container = ec2TaskDefinition.addContainer("WebContainer", {
150150
// Use an image from DockerHub
151-
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"),
151+
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
152152
memoryLimitMiB: 1024
153153
// ... other options here ...
154154
});
@@ -183,8 +183,8 @@ const taskDefinition = new ecs.TaskDefinition(this, 'TaskDef', {
183183
Images supply the software that runs inside the container. Images can be
184184
obtained from either DockerHub or from ECR repositories, or built directly from a local Dockerfile.
185185

186-
* `ecs.ContainerImage.fromDockerHub(imageName)`: use a publicly available image from
187-
DockerHub.
186+
* `ecs.ContainerImage.fromRegistry(imageName)`: use a public image.
187+
* `ecs.ContainerImage.fromRegistry(imageName, { credentials: mySecret })`: use a private image that requires credentials.
188188
* `ecs.ContainerImage.fromEcrRepository(repo, tag)`: use the given ECR repository as the image
189189
to start. If no tag is provided, "latest" is assumed.
190190
* `ecs.ContainerImage.fromAsset(this, 'Image', { directory: './image' })`: build and upload an

packages/@aws-cdk/aws-ecs/lib/container-definition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ export class ContainerDefinition extends cdk.Construct {
379379
portMappings: this.portMappings.map(renderPortMapping),
380380
privileged: this.props.privileged,
381381
readonlyRootFilesystem: this.props.readonlyRootFilesystem,
382-
repositoryCredentials: undefined, // FIXME
382+
repositoryCredentials: this.props.image.toRepositoryCredentialsJson(),
383383
ulimits: this.ulimits.map(renderUlimit),
384384
user: this.props.user,
385385
volumesFrom: this.volumesFrom.map(renderVolumeFrom),

packages/@aws-cdk/aws-ecs/lib/container-image.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import ecr = require('@aws-cdk/aws-ecr');
22
import cdk = require('@aws-cdk/cdk');
3-
43
import { ContainerDefinition } from './container-definition';
4+
import { CfnTaskDefinition } from './ecs.generated';
55

66
/**
77
* Constructs for types of container images
88
*/
99
export abstract class ContainerImage {
1010
/**
11-
* Reference an image on DockerHub
11+
* Reference an image on DockerHub or another online registry
1212
*/
13-
public static fromDockerHub(name: string) {
14-
return new DockerHubImage(name);
13+
public static fromRegistry(name: string, props: RepositoryImageProps = {}) {
14+
return new RepositoryImage(name, props);
1515
}
1616

1717
/**
@@ -37,8 +37,13 @@ export abstract class ContainerImage {
3737
* Called when the image is used by a ContainerDefinition
3838
*/
3939
public abstract bind(containerDefinition: ContainerDefinition): void;
40+
41+
/**
42+
* Render the Repository credentials to the CloudFormation object
43+
*/
44+
public abstract toRepositoryCredentialsJson(): CfnTaskDefinition.RepositoryCredentialsProperty | undefined;
4045
}
4146

4247
import { AssetImage, AssetImageProps } from './images/asset-image';
43-
import { DockerHubImage } from './images/dockerhub';
4448
import { EcrImage } from './images/ecr';
49+
import { RepositoryImage, RepositoryImageProps } from './images/repository';

packages/@aws-cdk/aws-ecs/lib/images/asset-image.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DockerImageAsset } from '@aws-cdk/assets-docker';
22
import cdk = require('@aws-cdk/cdk');
33
import { ContainerDefinition } from '../container-definition';
44
import { ContainerImage } from '../container-image';
5+
import { CfnTaskDefinition } from '../ecs.generated';
56

67
export interface AssetImageProps {
78
/**
@@ -15,6 +16,7 @@ export interface AssetImageProps {
1516
*/
1617
export class AssetImage extends ContainerImage {
1718
private readonly asset: DockerImageAsset;
19+
1820
constructor(scope: cdk.Construct, id: string, props: AssetImageProps) {
1921
super();
2022
this.asset = new DockerImageAsset(scope, id, { directory: props.directory });
@@ -24,6 +26,10 @@ export class AssetImage extends ContainerImage {
2426
this.asset.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole());
2527
}
2628

29+
public toRepositoryCredentialsJson(): CfnTaskDefinition.RepositoryCredentialsProperty | undefined {
30+
return undefined;
31+
}
32+
2733
public get imageName() {
2834
return this.asset.imageUri;
2935
}

packages/@aws-cdk/aws-ecs/lib/images/dockerhub.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/@aws-cdk/aws-ecs/lib/images/ecr.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ecr = require('@aws-cdk/aws-ecr');
22
import { ContainerDefinition } from '../container-definition';
33
import { ContainerImage } from '../container-image';
4+
import { CfnTaskDefinition } from '../ecs.generated';
45

56
/**
67
* An image from an ECR repository
@@ -18,4 +19,8 @@ export class EcrImage extends ContainerImage {
1819
public bind(containerDefinition: ContainerDefinition): void {
1920
this.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole());
2021
}
22+
23+
public toRepositoryCredentialsJson(): CfnTaskDefinition.RepositoryCredentialsProperty | undefined {
24+
return undefined;
25+
}
2126
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
2+
import { ContainerDefinition } from "../container-definition";
3+
import { ContainerImage } from "../container-image";
4+
import { CfnTaskDefinition } from '../ecs.generated';
5+
6+
export interface RepositoryImageProps {
7+
/**
8+
* Optional secret that houses credentials for the image registry
9+
*/
10+
credentials?: secretsmanager.ISecret;
11+
}
12+
13+
/**
14+
* A container image hosted on DockerHub or another online registry
15+
*/
16+
export class RepositoryImage extends ContainerImage {
17+
public readonly imageName: string;
18+
19+
private credentialsSecret?: secretsmanager.ISecret;
20+
21+
constructor(imageName: string, props: RepositoryImageProps = {}) {
22+
super();
23+
this.imageName = imageName;
24+
this.credentialsSecret = props.credentials;
25+
}
26+
27+
public bind(containerDefinition: ContainerDefinition): void {
28+
if (this.credentialsSecret) {
29+
this.credentialsSecret.grantRead(containerDefinition.taskDefinition.obtainExecutionRole());
30+
}
31+
}
32+
33+
public toRepositoryCredentialsJson(): CfnTaskDefinition.RepositoryCredentialsProperty | undefined {
34+
if (!this.credentialsSecret) { return undefined; }
35+
return {
36+
credentialsParameter: this.credentialsSecret.secretArn
37+
};
38+
}
39+
}

packages/@aws-cdk/aws-ecs/lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export * from './load-balanced-ecs-service';
2121
export * from './load-balanced-fargate-service-applet';
2222

2323
export * from './images/asset-image';
24-
export * from './images/dockerhub';
24+
export * from './images/repository';
2525
export * from './images/ecr';
2626

2727
export * from './log-drivers/aws-log-driver';

packages/@aws-cdk/aws-ecs/lib/load-balanced-fargate-service-applet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class LoadBalancedFargateServiceApplet extends cdk.Stack {
132132
memoryMiB: props.memoryMiB,
133133
publicLoadBalancer: props.publicLoadBalancer,
134134
publicTasks: props.publicTasks,
135-
image: ContainerImage.fromDockerHub(props.image),
135+
image: ContainerImage.fromRegistry(props.image),
136136
desiredCount: props.desiredCount,
137137
environment: props.environment,
138138
certificate,

0 commit comments

Comments
 (0)