-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ECS private registry support design #1737
Conversation
|
||
There's also no 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. | ||
|
||
Therefore, to support the use of private registries we would need to add credentials to the `DockerHubImage` type (as optional), and add a new `PrivateImage` type which could house credentials (required) and the private registry URI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a tingling spidey sense that says we keep those 2 things in the same class, since it feels to me that they don't differ that much.
Their behavior seems like it would be much the same, only differing in the presence of a 'registry' field (which could even be encoded into the imageName
, though I'm not sure that it should).
The EcrImage
is notably different in that it also sets up IAM permissions, neither DockerHubImage
nor PrivateImage
would do that.
|
||
```ts | ||
export interface IRepositoryCreds { | ||
readonly credentialsParameter: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do private registry credentials work? Do they have to be a SecretsManager secret with predefined JSON entries (probably username
and password
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, yes. (Though in the future maybe it would support different cred sources? e.g., SSM Param store.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for simplicity's sake I'd still prefer to just take a secretsmanager.SecretString
for now. Let's not overdesign now for a feature that may never come.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case it's ambiguous above: ECS expects this field to contain an AWS Secrets Manager ARN or secret name (if within the same region as the stack), not the secret value itself.
// sets credentialsParameter from secret.secretArn | ||
} | ||
|
||
public static withCredentialsParameter(param: string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these literal credentials, or the name of a secret?
If it's the latter, there's no need to make this distinction here. There is a secretsmanager.SecretString
which can be constructed from a Secret
object (a secret that gets deployed in the same CDK application) or constructed by hand from a secret that's been prepopulated into the AWS account. In that case, I think the whole RepositoryCreds
objects can be replaced with a SecretString
object.
With permissions and everything I agree that the existing API surface there is clunky, but I'm all in favor of simplifying, making the existing objects richer if we can, so that we need fewer classes. I don't want to go into too much design work here in case I'm completely misunderstanding the problem, so I'll shut up now, but get back to me if you want to discuss more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thought here was this is an escape hatch if someone has the ARN they want to set but don't want to construct a separate Secret
construct (or maybe in the future if it supports diff kinds of cred resources, this would still work w/o needing to be updated to accommodate a resource type).
Another diff is that fromSecret
would add "read" access to the Secret
to the execution role, while this one would not. But maybe adding it now is jumping the gun?
Re: using SecretString
: I steered away from it based on the package REAMDE warning that using it may lead to the creds surfacing in plain text, but maybe I misunderstood when exactly that could happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an unfortunate case of the docs being too informative and thereby confusing.
They are talking about 2 different SecretStrings
, btw. The docs are talking about the
SecretString
attribute to create a Secret
, but the SecretString
I'm talking about is for refencing an existing (or created) secret.
What the docs are trying to tell you is that creating a Secret
via CloudFormation only allows it to be populated by a randomly generated password, because doing otherwise would mean putting the literal secret value you want to put into into the Secret somewhere in your CloudFormation template... defeating the purpose of it being a secret. A randomly generated secret can still be useful for setting the admin login of an RDS instance, where you need some secret but you don't really care what the value is.
For our purposes, people are going to have to manually prepopulate a secret into their account, which we can then read using the SecretString
class. It's not documented in that README, which is a shame, but it's documented here: https://integ-docs-aws.amazon.com/CDK/latest/userguide/how_to_get_ext_values.html#passing_secrets_manager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What the docs are trying to tell you is that creating a
Secret
via CloudFormation only allows it to be populated by a randomly generated password
But you can also import an existing secret (example in README) into a Secret
construct, which is the use case I was considering most common.
For our purposes, people are going to have to manually prepopulate a secret into their account
Do you mean into AWS Secrets Manager, or the 3rd party account that houses the registry? If the former, what makes SecretString
more appropriate here?
To clarify: ECS is expecting the actual ARN of the credential secret here, not the secret value itself. (Apologies if a previous response was misleading.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was expecting that.
I mean users populate their own Secret, then import into CDK using a name or ARN.
How the credentials are fed into the actual registry is out of scope I think :) (unless it's an ECR registry, in which case it could be an autogenerated Secret)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per offline discussion: agreed that a Secret
construct is appropriate here.
```ts | ||
export class DockerHubImage implements IContainerImage { | ||
// add repositoryCredentials to constructor | ||
constructor(public readonly imageName: string, public readonly repositoryCredentials: RepositoryCreds) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're settling on keyword arguments as much as we can for "complex configuration" things, even if it's only for one parameter.
So that would mean defining something like:
export interface DockerHubImageProps {
credentials: RepositoryCreds;
}
// Take as second argument
// To be used as:
taskDefinition.AddContainer('myPrivateContainer', {
image: ecs.ContainerImage.fromDockerHub('userx/test', {
credentials: ecs.RepositoryCreds.fromSecret(secret)
})
});
|
||
For non-DockerHub privately hosted images, we can add a new subclass: | ||
```ts | ||
export class PrivateImage implements IContainerImage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This confirms my suspicion that PrivateImage
looks a whole lot like DockerHubImage
:)
} | ||
|
||
public bind(_containerDefinition: ContainerDefinition): void { | ||
// Nothing to do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing to do
is no longer true, right? We should be calling this.repositoryCredentials.bind()
in here if I'm understanding correctly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right; I have bind()
defined on RepositoryCreds
but I suppose it needs to be actually called at some point :P
Oh oops. Didn't mean to approve yet :) |
f8f430c
to
1b461da
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this all makes perfect sense.
Let's get to implementing :)
|
||
```ts | ||
export interface IRepositoryCreds { | ||
readonly credentialsParameter: string | ||
readonly secret: secretsManager.Secret; | ||
} | ||
|
||
export class RepositoryCreds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would get rid of this class and just call the secret grant directly in ContainerImage
.
|
||
The `DockerHubImage` construct, which currently takes a string for image name, can be extended to take in an (optional) "repositoryCredentials" field of type `IRepositoryCreds`: | ||
The `DockerHubImage` construct will be renamed to `WebHostedImage`, and augmented to take in optional "credentials" via keyword props: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind this rename at all, but being the pedant that I am, is "Web" the right term to use? It doesn't really have anything to do with a linked network of HTML pages... InternetHostedImage
would be a more correct term.
Not saying better, just more correct.
} | ||
// define props | ||
export interface WebHostedImageProps { | ||
credentials: RepositoryCreds; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think they default to being optional (with a ?
)
export class PrivateImage implements IContainerImage { | ||
constructor(public readonly image: string, public readonly repositoryCredentials: RepositoryCreds) { | ||
// add credentials to constructor | ||
constructor(imageName: string, props: WebHostedImageProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since all properties will be optional, default to props: WebHostedImageProps = {}
.
1b461da
to
10d4c1c
Compare
78f47a1
to
7877598
Compare
@rix0rrr updated per feedback re: abstracting render func & limiting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved modulo build failures
@@ -24,6 +26,10 @@ export class AssetImage extends ContainerImage { | |||
this.asset.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); | |||
} | |||
|
|||
public renderRepositoryCredentials(): undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsii should give you an error here because you changed the return type of this function while overriding it. C# does not allow narrowing the return type, so we have to disallow that everywhere as well.
You need to declare the return type as CfnTaskDefinition.RepositoryCredentialsProperty | undefined
, even though we could give a narrower type.
@@ -18,4 +19,8 @@ export class EcrImage extends ContainerImage { | |||
public bind(containerDefinition: ContainerDefinition): void { | |||
this.repository.grantPull(containerDefinition.taskDefinition.obtainExecutionRole()); | |||
} | |||
|
|||
public renderRepositoryCredentials(): undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See some renames...
packages/@aws-cdk/aws-ecs/README.md
Outdated
@@ -148,7 +148,7 @@ const ec2TaskDefinition = new ecs.Ec2TaskDefinition(this, 'TaskDef', { | |||
|
|||
const container = ec2TaskDefinition.addContainer("WebContainer", { | |||
// Use an image from DockerHub | |||
image: ecs.ContainerImage.fromDockerHub("amazon/amazon-ecs-sample"), | |||
image: ecs.ContainerImage.fromInternet("amazon/amazon-ecs-sample"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about we call this fromRegistry
("registry" is the official term in the dockerverse)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ECR is also a registry but we don't want to include that. But maybe that ambiguity is okay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is okay, especially given we will have both “fromEcr” and “fromRegistry” next to each other which disambiguates
public static fromDockerHub(name: string) { | ||
return new DockerHubImage(name); | ||
public static fromInternet(name: string, props: InternetHostedImageProps = {}) { | ||
return new InternetHostedImage(name, props); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RegistryImage
/** | ||
* Repository Credential resources | ||
*/ | ||
export interface RepositoryCreds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering why we need this interface which adds another unneeded indirection instead of flat:
ContainerImage.fromRegistry('name', { credentialsSecret: secret });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is in fact how it works. This interface is not used for input, it's for output.
/** | ||
* Render the Repository credentials to the CloudFormation object | ||
*/ | ||
public abstract renderRepositoryCredentials(): CfnTaskDefinition.RepositoryCredentialsProperty | undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually we call these methods toRepositoryCredentialsJson
(JSON indicates it's low-level)
/** | ||
* Optional credentials for a private image registry | ||
*/ | ||
protected abstract readonly credentials?: RepositoryCreds; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But you know what... this propery can probably be removed, no? I don't think it's used anywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(replying for context) You're right, it's a holdover from when we were checking it directly from ContainerDefinition
to make a decision re: calling toRepositoryCredentialsJson
, but once we moved to using multiple return types this check & field became unnecessary.
Pull Request Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.