Support secrets with "local up"#808
Conversation
Manual TestsGivenMy 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"
}
}WhenRan 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 ... doneThen✅ 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 |
e7426f5 to
2e80b3d
Compare
| secretARN := aws.StringValue(cs.secret.ValueFrom) | ||
| parsedARN, err := arn.Parse(secretARN) | ||
| if err != nil { | ||
| if strings.Contains(err.Error(), "arn: invalid prefix") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
Is that kind of too much overlap compared to createCommand? Except for running it.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Comment here a tad confusing, since the task definition file can also be JSON.
There was a problem hiding this comment.
👍 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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
|
||
| type secretsManagerDecrypter struct { | ||
| client *secretsmanager.SecretsManager | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Ah, got it. Any reason not to put the interface in the same place as the implementation? (non-blocking)
There was a problem hiding this comment.
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
|
|
||
| decrypted := "" | ||
| err = nil | ||
| if service == secretsmanager.ServiceName { |
There was a problem hiding this comment.
Nit: You could use a switch case statement here instead.
This test is broken until we implement the full cycle with local create -> local up -> local down. Removing it for now.
2e80b3d to
9dfd058
Compare
Issue #, if available: #797
Description of changes:
Enter
[N/A]in the box, if an item is not applicable to your change.Testing
Documentation
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.