Skip to content
This repository was archived by the owner on Nov 19, 2025. It is now read-only.

Support secrets with "local up"#808

Merged
efekarakus merged 7 commits intoaws:ecs-localfrom
efekarakus:local/up-with-secrets
Jun 26, 2019
Merged

Support secrets with "local up"#808
efekarakus merged 7 commits intoaws:ecs-localfrom
efekarakus:local/up-with-secrets

Conversation

@efekarakus
Copy link
Copy Markdown
Contributor

@efekarakus efekarakus commented Jun 24, 2019

Issue #, if available: #797

Description of changes:

  1. Retrieves the secret ARNs from the Compose file by parsing the service labels.
  2. Queries either SSM or SecretsManager to get the decrypted secrets.
  3. Starts the local task with the secrets as env variables.

Enter [N/A] in the box, if an item is not applicable to your change.

Testing

  • Unit tests passed
  • Integration tests passed
  • Unit tests added for new functionality
  • Listed manual checks and their outputs in the comments (example)
  • Link to issue or PR for the integration tests:

Documentation

  • Contacted our doc writer
  • Updated our README

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

@efekarakus efekarakus requested review from a team, SoManyHs and iamhopaul123 June 24, 2019 22:42
@efekarakus efekarakus changed the title Local/up with secrets Support secrets with "local up" Jun 24, 2019
@efekarakus
Copy link
Copy Markdown
Contributor Author

Manual Tests

Given

My remote task definition is defined as:

{
    "taskDefinition": {
        "taskDefinitionArn": "arn:aws:ecs:us-east-1:111111111:task-definition/TaskForECSLocalWithSecrets:10",
        "containerDefinitions": [
            {
                "name": "nginx",
                "image": "nginx",
                "cpu": 0,
                "portMappings": [],
                "essential": true,
                "environment": [],
                "mountPoints": [],
                "volumesFrom": [],
                "secrets": [
                    {
                        "name": "DEFAULT_DB_PASSWORD",
                        "valueFrom": "TEST_DB_PASSWORD"
                    },
                    {
                        "name": "IAD_DB_PASSWORD",
                        "valueFrom": "arn:aws:ssm:us-east-1:111111111:parameter/TEST_DB_PASSWORD"
                    },
                    {
                        "name": "TestSecret1",
                        "valueFrom": "arn:aws:secretsmanager:us-east-1:111111111:secret:alpha/efe/local-j0gCbT"
                    },
                    {
                        "name": "DUB_DB_PASSWORD",
                        "valueFrom": "arn:aws:ssm:eu-west-1:111111111:parameter/TEST_DUB_DB_PASSWORD"
                    },
                    {
                        "name": "PDX_DB_PASSWORD",
                        "valueFrom": "arn:aws:ssm:us-west-2:111111111:parameter/TEST_PDX_DB_PASSWORD"
                    }
                ]
            }
        ],
        "family": "TaskForECSLocalWithSecrets",
        "executionRoleArn": "arn:aws:iam::111111111:role/ecsTaskExecutionRole",
        "networkMode": "awsvpc",
        "revision": 10,
        "volumes": [],
        "status": "ACTIVE",
        "requiresAttributes": [
            {
                "name": "ecs.capability.secrets.asm.environment-variables"
            },
            {
                "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
            },
            {
                "name": "ecs.capability.task-eni"
            },
            {
                "name": "ecs.capability.secrets.ssm.environment-variables"
            }
        ],
        "placementConstraints": [],
        "compatibilities": [
            "EC2",
            "FARGATE"
        ],
        "requiresCompatibilities": [
            "EC2"
        ],
        "cpu": "256",
        "memory": "512"
    }
}

When

Ran the following command:

$ ecs-cli local up -t arn:aws:ecs:us-east-1:685593908319:task-definition/TaskForECSLocalWithSecrets:10
docker-compose.local.yml file already exists. Do you want to write over this file? [y/N]
y
Successfully wrote docker-compose.local.yml
INFO[0001] The network ecs-local-network already exists 
INFO[0001] The amazon-ecs-local-container-endpoints container already exists with ID 05e41907637fe79a8ad7a0d4fea4b862c3b37aea801afdbc59822337e76d8722 
INFO[0001] Started container with ID 05e41907637fe79a8ad7a0d4fea4b862c3b37aea801afdbc59822337e76d8722 
Recreating local-cmds_nginx_1 ... done

Then

✅ The environment variables passed to the container are correct:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=daecdc705a5d
DEFAULT_DB_PASSWORD=1234
DUB_DB_PASSWORD=DUB1234
IAD_DB_PASSWORD=1234
PDX_DB_PASSWORD=PDX1234
TestSecret1=DevX
NGINX_VERSION=1.17.0
NJS_VERSION=0.3.2
PKG_RELEASE=1~stretch
HOME=/root

@efekarakus efekarakus force-pushed the local/up-with-secrets branch from e7426f5 to 2e80b3d Compare June 25, 2019 04:27
secretARN := aws.StringValue(cs.secret.ValueFrom)
parsedARN, err := arn.Parse(secretARN)
if err != nil {
if strings.Contains(err.Error(), "arn: invalid prefix") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am a little bit confused about this: arn.Parse errors "arn: invalid prefix" when the secretARN does not begin with "arn:". So why does ServiceName return to "ssm" if it errors out as "arn: invalid prefix"? It could be an invalid secretARN.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question! I tried to explain it in the comment inside this if-block.The ECS Task Definition also accepts a value that's not an ARN which is just the parameter name in SSM (see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html#secrets-logconfig). So this function will also default to SSM.

If it's just an invalid ARN then we will try to look it up in SSM and the request will fail, so the user will receive an error while trying to start the container with local up. Does that help?

Copy link
Copy Markdown
Contributor

@iamhopaul123 iamhopaul123 Jun 25, 2019

Choose a reason for hiding this comment

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

I see. So SSM is the default one you set. Then, that will make sense!

func upCommand() cli.Command {
return cli.Command{
Name: "up",
Usage: "Create a Compose file from an ECS task definition and run it.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is that kind of too much overlap compared to createCommand? Except for running it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes but that's expected. We split create and up because it's possible that a user wants to modify the Compose file after running create and then they can just run up.

I just created a backlog task to change the behavior of local create slightly: #809

// structure to a docker compose schema, which will be written to a
// docker-compose.local.yml file.

// Package converter converts an ecs.TaskDefinition or a yaml file to a docker compose schema.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment here a tad confusing, since the task definition file can also be JSON.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 agreed, changed to // Package converter translates entities to a docker compose schema and vice versa.

// ConvertToDockerCompose creates the payload from an ECS Task Definition to be written as a docker compose file
func ConvertToDockerCompose(taskDefinition *ecs.TaskDefinition) ([]byte, error) {
services := []composeV3.ServiceConfig{}
var services []composeV3.ServiceConfig
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is instantiating this with a var (e.g. nil slice) because we want the JSON marshalling to return nil versus an empty slice (in line 68)? (See: https://golang.org/pkg/encoding/json/#Marshal)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't think that much about it :P, it's just because that's the recommended way of initializing empty slices: https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure. Ultimately, we should have a check that there is at least one service defined anyway; whether its pre-marshalled form is nil or empty shouldn't matter that much i don't think.

Comment thread ecs-cli/modules/cli/local/up_app.go Outdated

type secretsManagerDecrypter struct {
client *secretsmanager.SecretsManager
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmmmm what do you think about having these client objects live somewhere else? I feel like the _app.go files should only contain "controller" logic rather than "model" logic (if we're thinking about this from a MVC sort of perspective). It feels sort of weird to have these and their DecryptMethod functions defined here.

Also, the fact that this is duplicated maybe hints at having a secretsDecrypter interface that can be implemented by either SSM or SecretsManager.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmmmm what do you think about having these client objects live somewhere else?

That makes sense, I moved it to a package under local/secrets/clients.

Also, the fact that this is duplicated maybe hints at having a secretsDecrypter interface that can be implemented by either SSM or SecretsManager.

That's actually exactly what happens now. These two structs implement the SecretDecrypter interface defined here: https://github.com/aws/amazon-ecs-cli/pull/808/files#diff-798f5fba8ffe42058a2ac18e2adb3d7cR32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, got it. Any reason not to put the interface in the same place as the implementation? (non-blocking)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just to follow the guidelines:

Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.

https://github.com/golang/go/wiki/CodeReviewComments#interfaces

Comment thread ecs-cli/modules/cli/local/up_app.go Outdated

decrypted := ""
err = nil
if service == secretsmanager.ServiceName {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: You could use a switch case statement here instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread ecs-cli/modules/cli/local/up_app.go
Comment thread ecs-cli/modules/cli/local/up_app.go
Comment thread ecs-cli/modules/cli/local/up_app.go
@efekarakus efekarakus force-pushed the local/up-with-secrets branch from 2e80b3d to 9dfd058 Compare June 26, 2019 04:05
Copy link
Copy Markdown
Contributor

@SoManyHs SoManyHs left a comment

Choose a reason for hiding this comment

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

🎉

@efekarakus efekarakus merged commit 9dfd058 into aws:ecs-local Jun 26, 2019
@efekarakus efekarakus deleted the local/up-with-secrets branch June 26, 2019 16:39
@SoManyHs SoManyHs mentioned this pull request Jul 2, 2019
7 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants