Skip to content
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

Merged
merged 12 commits into from
Mar 13, 2019
Merged

Conversation

allisaurus
Copy link
Contributor


Pull Request Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.

@allisaurus allisaurus requested a review from a team as a code owner February 12, 2019 00:52
@SoManyHs SoManyHs self-requested a review February 12, 2019 00:54
@SoManyHs SoManyHs added the @aws-cdk/aws-ecs Related to Amazon Elastic Container label Feb 12, 2019
design/aws-ecs-priv-registry-support.md Show resolved Hide resolved
design/aws-ecs-priv-registry-support.md Show resolved Hide resolved
design/aws-ecs-priv-registry-support.md Show resolved Hide resolved

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.
Copy link
Contributor

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
Copy link
Contributor

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?)

Copy link
Contributor Author

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.)

Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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.)

Copy link
Contributor

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)

Copy link
Contributor Author

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) {
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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

@rix0rrr
Copy link
Contributor

rix0rrr commented Feb 12, 2019

Oh oops. Didn't mean to approve yet :)

@rix0rrr rix0rrr self-assigned this Feb 18, 2019
@allisaurus
Copy link
Contributor Author

@rix0rrr @SoManyHs Updated doc to reflect feedback. Please let me know if you have any additional comments (including new name for "DockerHubImage"). Thanks!

Copy link
Contributor

@rix0rrr rix0rrr left a 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 :)

design/aws-ecs-priv-registry-support.md Show resolved Hide resolved
design/aws-ecs-priv-registry-support.md Show resolved Hide resolved

```ts
export interface IRepositoryCreds {
readonly credentialsParameter: string
readonly secret: secretsManager.Secret;
}

export class RepositoryCreds {
Copy link
Contributor

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:
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

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 = {}.

@allisaurus allisaurus force-pushed the ecs-private-registry branch from 78f47a1 to 7877598 Compare March 8, 2019 01:15
@allisaurus
Copy link
Contributor Author

@rix0rrr updated per feedback re: abstracting render func & limiting credendials property access.

Copy link
Contributor

@rix0rrr rix0rrr left a 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 {
Copy link
Contributor

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Copy link
Contributor

@eladb eladb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See some renames...

@@ -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"),
Copy link
Contributor

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)

Copy link
Contributor

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.

Copy link
Contributor

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);
Copy link
Contributor

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 {
Copy link
Contributor

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

Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

@allisaurus allisaurus Mar 13, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-ecs Related to Amazon Elastic Container
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants