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 Agent tagging support #1618

Merged
merged 2 commits into from
Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## 1.22.0-dev
* Feature - Add support for ECS Secrets integrating with AWS Systems Manager Parameter Store
* Feature - Introduce two environment variables `ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM` and `ECS_CONTAINER_INSTANCE_TAGS` to support ECS tagging [#1618](https://github.com/aws/amazon-ecs-agent/pull/1618)

## 1.21.0
* Feature - Add v3 task metadata support for awsvpc, host and bridge network mode
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ additional details on each available environment variable.
| `ECS_ENABLE_CPU_UNBOUNDED_WINDOWS_WORKAROUND` | `true` | When `true`, ECS will allow CPU unbounded(CPU=`0`) tasks to run along with CPU bounded tasks in Windows. | Not applicable | `false` |
| `ECS_TASK_METADATA_RPS_LIMIT` | `100,150` | Comma separated integer values for steady state and burst throttle limits for task metadata endpoint | `40,60` | `40,60` |
| `ECS_SHARED_VOLUME_MATCH_FULL_CONFIG` | `true` | When `true`, ECS Agent will compare name, driver options, and labels to make sure volumes are identical. When `false`, Agent will short circuit shared volume comparison if the names match. This is the default Docker behavior. If a volume is shared across instances, this should be set to `false`. | `false` | `false`|
| `ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM` | `"ec2_instance"` | If `ec2_instance` is specified, existing tags defined on the container instance will be registered to Amazon ECS and will be discoverable using the `ListTagsForResource` API. Using this requires that the IAM role associated with the container instance have the `ec2:DescribeTags` action allowed. | `"none"` | `"none"` |
| `ECS_CONTAINER_INSTANCE_TAGS` | `{"tag_key": "tag_val"}` | The metadata that you apply to the container instance to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. Tag keys can have a maximum character length of 128 characters, and tag values can have a maximum length of 256 characters. If tags also exist on your container instance that are propagated using the `ECS_CONTAINER_INSTANCE_PROPAGATE_TAGS_FROM` parameter, those tags will be overwritten by the tags specified using `ECS_CONTAINER_INSTANCE_TAGS`. | `{}` | `{}` |

### Persistence

Expand Down
5 changes: 4 additions & 1 deletion agent/Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions agent/api/ecsclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ func (client *APIECSClient) CreateCluster(clusterName string) (string, error) {
// ContainerInstanceARN if successful. Supplying a non-empty container
// instance ARN allows a container instance to update its registered
// resources.
func (client *APIECSClient) RegisterContainerInstance(containerInstanceArn string, attributes []*ecs.Attribute) (string, error) {
func (client *APIECSClient) RegisterContainerInstance(containerInstanceArn string,
attributes []*ecs.Attribute, tags []*ecs.Tag) (string, error) {
clusterRef := client.config.Cluster
// If our clusterRef is empty, we should try to create the default
if clusterRef == "" {
Expand All @@ -118,7 +119,7 @@ func (client *APIECSClient) RegisterContainerInstance(containerInstanceArn strin
}()
// Attempt to register without checking existence of the cluster so we don't require
// excess permissions in the case where the cluster already exists and is active
containerInstanceArn, err := client.registerContainerInstance(clusterRef, containerInstanceArn, attributes)
containerInstanceArn, err := client.registerContainerInstance(clusterRef, containerInstanceArn, attributes, tags)
if err == nil {
return containerInstanceArn, nil
}
Expand All @@ -129,10 +130,11 @@ func (client *APIECSClient) RegisterContainerInstance(containerInstanceArn strin
return "", err
}
}
return client.registerContainerInstance(clusterRef, containerInstanceArn, attributes)
return client.registerContainerInstance(clusterRef, containerInstanceArn, attributes, tags)
}

func (client *APIECSClient) registerContainerInstance(clusterRef string, containerInstanceArn string, attributes []*ecs.Attribute) (string, error) {
func (client *APIECSClient) registerContainerInstance(clusterRef string, containerInstanceArn string,
attributes []*ecs.Attribute, tags []*ecs.Tag) (string, error) {
registerRequest := ecs.RegisterContainerInstanceInput{Cluster: &clusterRef}
var registrationAttributes []*ecs.Attribute
if containerInstanceArn != "" {
Expand All @@ -155,6 +157,7 @@ func (client *APIECSClient) registerContainerInstance(clusterRef string, contain
// Add additional attributes such as the os type
registrationAttributes = append(registrationAttributes, client.getAdditionalAttributes()...)
registerRequest.Attributes = registrationAttributes
registerRequest.Tags = tags
registerRequest = client.setInstanceIdentity(registerRequest)

resources, err := client.getResources()
Expand Down
56 changes: 48 additions & 8 deletions agent/api/ecsclient/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,22 @@ const (
)

var (
iidResponse = []byte(iid)
iidSignatureResponse = []byte(iidSignature)
iidResponse = []byte(iid)
iidSignatureResponse = []byte(iidSignature)
containerInstanceTags = []*ecs.Tag{
{
Key: aws.String("my_key1"),
Value: aws.String("my_val1"),
},
{
Key: aws.String("my_key2"),
Value: aws.String("my_val2"),
},
}
containerInstanceTagsMap = map[string]string{
"my_key1": "my_val1",
"my_key2": "my_val2",
}
)

func NewMockClient(ctrl *gomock.Controller,
Expand Down Expand Up @@ -339,14 +353,20 @@ func TestReRegisterContainerInstance(t *testing.T) {
reqAttributes := func() map[string]string {
rv := make(map[string]string, len(req.Attributes))
for i := range req.Attributes {
rv[*req.Attributes[i].Name] = aws.StringValue(req.Attributes[i].Value)
rv[aws.StringValue(req.Attributes[i].Name)] = aws.StringValue(req.Attributes[i].Value)
}
return rv
}()
for k, v := range reqAttributes {
assert.Contains(t, expectedAttributes, k)
assert.Equal(t, expectedAttributes[k], v)
}
assert.Equal(t, len(containerInstanceTags), len(req.Tags), "Wrong number of tags")
reqTags := extractTagsMapFromRegisterContainerInstanceInput(req)
for k, v := range reqTags {
assert.Contains(t, containerInstanceTagsMap, k)
assert.Equal(t, containerInstanceTagsMap[k], v)
}
}).Return(&ecs.RegisterContainerInstanceOutput{
ContainerInstance: &ecs.ContainerInstance{
ContainerInstanceArn: aws.String("registerArn"),
Expand All @@ -355,7 +375,7 @@ func TestReRegisterContainerInstance(t *testing.T) {
nil),
)

arn, err := client.RegisterContainerInstance("arn:test", capabilities)
arn, err := client.RegisterContainerInstance("arn:test", capabilities, containerInstanceTags)
if err != nil {
t.Errorf("Should not be an error: %v", err)
}
Expand Down Expand Up @@ -402,14 +422,20 @@ func TestRegisterContainerInstance(t *testing.T) {
assert.Equal(t, expectedAttributes[*req.Attributes[i].Name], *req.Attributes[i].Value)
}
}
assert.Equal(t, len(containerInstanceTags), len(req.Tags), "Wrong number of tags")
reqTags := extractTagsMapFromRegisterContainerInstanceInput(req)
for k, v := range reqTags {
assert.Contains(t, containerInstanceTagsMap, k)
assert.Equal(t, containerInstanceTagsMap[k], v)
}
}).Return(&ecs.RegisterContainerInstanceOutput{
ContainerInstance: &ecs.ContainerInstance{
ContainerInstanceArn: aws.String("registerArn"),
Attributes: buildAttributeList(fakeCapabilities, expectedAttributes)}},
nil),
)

arn, err := client.RegisterContainerInstance("", capabilities)
arn, err := client.RegisterContainerInstance("", capabilities, containerInstanceTags)
assert.NoError(t, err)
assert.Equal(t, "registerArn", arn)
}
Expand Down Expand Up @@ -456,14 +482,20 @@ func TestRegisterContainerInstanceNoIID(t *testing.T) {
assert.Equal(t, expectedAttributes[*req.Attributes[i].Name], *req.Attributes[i].Value)
}
}
assert.Equal(t, len(containerInstanceTags), len(req.Tags), "Wrong number of tags")
reqTags := extractTagsMapFromRegisterContainerInstanceInput(req)
for k, v := range reqTags {
assert.Contains(t, containerInstanceTagsMap, k)
assert.Equal(t, containerInstanceTagsMap[k], v)
}
}).Return(&ecs.RegisterContainerInstanceOutput{
ContainerInstance: &ecs.ContainerInstance{
ContainerInstanceArn: aws.String("registerArn"),
Attributes: buildAttributeList(fakeCapabilities, expectedAttributes)}},
nil),
)

arn, err := client.RegisterContainerInstance("", capabilities)
arn, err := client.RegisterContainerInstance("", capabilities, containerInstanceTags)
assert.NoError(t, err)
assert.Equal(t, "registerArn", arn)
}
Expand All @@ -489,7 +521,7 @@ func TestRegisterContainerInstanceWithNegativeResource(t *testing.T) {
mockEC2Metadata.EXPECT().GetDynamicData(ec2.InstanceIdentityDocumentResource).Return("instanceIdentityDocument", nil),
mockEC2Metadata.EXPECT().GetDynamicData(ec2.InstanceIdentityDocumentSignatureResource).Return("signature", nil),
)
_, err := client.RegisterContainerInstance("", nil)
_, err := client.RegisterContainerInstance("", nil, nil)
assert.Error(t, err, "Register resource with negative value should cause registration fail")
}

Expand Down Expand Up @@ -563,7 +595,7 @@ func TestRegisterBlankCluster(t *testing.T) {
nil),
)

arn, err := client.RegisterContainerInstance("", nil)
arn, err := client.RegisterContainerInstance("", nil, nil)
if err != nil {
t.Errorf("Should not be an error: %v", err)
}
Expand Down Expand Up @@ -823,3 +855,11 @@ func TestSubmitContainerStateChangeWhileTaskInPending(t *testing.T) {
})
}
}

func extractTagsMapFromRegisterContainerInstanceInput(req *ecs.RegisterContainerInstanceInput) map[string]string {
tagsMap := make(map[string]string, len(req.Tags))
for i := range req.Tags {
tagsMap[aws.StringValue(req.Tags[i].Key)] = aws.StringValue(req.Tags[i].Value)
}
return tagsMap
}
5 changes: 3 additions & 2 deletions agent/api/interface.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2014-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright 2014-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
Expand Down Expand Up @@ -26,7 +26,8 @@ type ECSClient interface {
// ContainerInstanceARN if successful. Supplying a non-empty container
// instance ARN allows a container instance to update its registered
// resources.
RegisterContainerInstance(existingContainerInstanceArn string, attributes []*ecs.Attribute) (string, error)
RegisterContainerInstance(existingContainerInstanceArn string,
attributes []*ecs.Attribute, tags []*ecs.Tag) (string, error)
// SubmitTaskStateChange sends a state change and returns an error
// indicating if it was submitted
SubmitTaskStateChange(change TaskStateChange) error
Expand Down
8 changes: 4 additions & 4 deletions agent/api/mocks/api_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 52 additions & 12 deletions agent/app/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,6 @@ const (

vpcIDAttributeName = "ecs.vpc-id"
subnetIDAttributeName = "ecs.subnet-id"

// instanceIDMetadataPath is the metadata path from instance metadata service
// to retrieve the instance id.
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
// for details
instanceIDMetadataPath = "instance-id"
)

var (
Expand Down Expand Up @@ -96,6 +90,7 @@ type agent interface {
type ecsAgent struct {
ctx context.Context
ec2MetadataClient ec2.EC2MetadataClient
ec2Client ec2.Client
cfg *config.Config
dockerClient dockerapi.DockerClient
containerInstanceARN string
Expand Down Expand Up @@ -140,6 +135,8 @@ func newAgent(
seelog.Infof("Amazon ECS agent Version: %s, Commit: %s", version.Version, version.GitShortHash)
seelog.Debugf("Loaded config: %s", cfg.String())

ec2Client := ec2.NewClientImpl(cfg.AWSRegion)

dockerClient, err := dockerapi.NewDockerGoClient(
clientfactory.NewFactory(ctx, cfg.DockerEndpoint), cfg)
if err != nil {
Expand All @@ -159,6 +156,7 @@ func newAgent(
return &ecsAgent{
ctx: ctx,
ec2MetadataClient: ec2MetadataClient,
ec2Client: ec2Client,
cfg: cfg,
dockerClient: dockerClient,
// We instantiate our own credentialProvider for use in acs/tcs. This tries
Expand Down Expand Up @@ -399,7 +397,7 @@ func (agent *ecsAgent) setClusterInConfig(previousCluster string) error {

// getEC2InstanceID gets the EC2 instance ID from the metadata service
func (agent *ecsAgent) getEC2InstanceID() string {
instanceID, err := agent.ec2MetadataClient.GetMetadata(instanceIDMetadataPath)
instanceID, err := agent.ec2MetadataClient.InstanceID()
if err != nil {
seelog.Warnf(
"Unable to access EC2 Metadata service to determine EC2 ID: %v", err)
Expand Down Expand Up @@ -452,7 +450,6 @@ func (agent *ecsAgent) registerContainerInstance(
stateManager statemanager.StateManager,
client api.ECSClient,
additionalAttributes []*ecs.Attribute) error {

// Preflight request to make sure they're good
if preflightCreds, err := agent.credentialProvider.Get(); err != nil || preflightCreds.AccessKeyID == "" {
seelog.Warnf("Error getting valid credentials (AKID %s): %v", preflightCreds.AccessKeyID, err)
Expand All @@ -464,13 +461,27 @@ func (agent *ecsAgent) registerContainerInstance(
}
capabilities := append(agentCapabilities, additionalAttributes...)

// Get the tags of this container instance defined in config file
tags := utils.MapToTags(agent.cfg.ContainerInstanceTags)
if agent.cfg.ContainerInstancePropagateTagsFrom == config.ContainerInstancePropagateTagsFromEC2InstanceType {
ec2Tags, err := agent.getContainerInstanceTagsFromEC2API()
// If we are unable to call the API, we should not treat it as a transient error,
// because we've already retried several times, we may throttle the API if we
// keep retrying.
if err != nil {
return err
}
seelog.Infof("Retrieved Tags from EC2 DescribeTags API:\n%v", ec2Tags)
tags = mergeTags(tags, ec2Tags)
}

if agent.containerInstanceARN != "" {
seelog.Infof("Restored from checkpoint file. I am running as '%s' in cluster '%s'", agent.containerInstanceARN, agent.cfg.Cluster)
return agent.reregisterContainerInstance(client, capabilities)
return agent.reregisterContainerInstance(client, capabilities, tags)
}

seelog.Info("Registering Instance with ECS")
containerInstanceArn, err := client.RegisterContainerInstance("", capabilities)
containerInstanceArn, err := client.RegisterContainerInstance("", capabilities, tags)
if err != nil {
seelog.Errorf("Error registering: %v", err)
if retriable, ok := err.(apierrors.Retriable); ok && !retriable.Retry() {
Expand All @@ -496,8 +507,10 @@ func (agent *ecsAgent) registerContainerInstance(
// reregisterContainerInstance registers a container instance that has already been
// registered with ECS. This is for cases where the ECS Agent is being restored
// from a check point.
func (agent *ecsAgent) reregisterContainerInstance(client api.ECSClient, capabilities []*ecs.Attribute) error {
_, err := client.RegisterContainerInstance(agent.containerInstanceARN, capabilities)
func (agent *ecsAgent) reregisterContainerInstance(client api.ECSClient,
capabilities []*ecs.Attribute, tags []*ecs.Tag) error {
_, err := client.RegisterContainerInstance(agent.containerInstanceARN, capabilities, tags)

if err == nil {
return nil
}
Expand Down Expand Up @@ -614,3 +627,30 @@ func (agent *ecsAgent) verifyRequiredDockerVersion() (int, bool) {
dockerclient.Version_1_21)
return exitcodes.ExitTerminal, false
}

// getContainerInstanceTagsFromEC2API will retrieve the tags of this instance remotely.
func (agent *ecsAgent) getContainerInstanceTagsFromEC2API() ([]*ecs.Tag, error) {
// Get instance ID from ec2 metadata client.
instanceID, err := agent.ec2MetadataClient.InstanceID()
if err != nil {
return nil, err
}

return agent.ec2Client.DescribeECSTagsForInstance(instanceID)
}

// mergeTags will merge the local tags and ec2 tags, for the overlap part, ec2 tags
// will be overridden by local tags.
func mergeTags(localTags []*ecs.Tag, ec2Tags []*ecs.Tag) []*ecs.Tag {
tagsMap := make(map[string]string)

for _, ec2Tag := range ec2Tags {
tagsMap[aws.StringValue(ec2Tag.Key)] = aws.StringValue(ec2Tag.Value)
}

for _, localTag := range localTags {
tagsMap[aws.StringValue(localTag.Key)] = aws.StringValue(localTag.Value)
}

return utils.MapToTags(tagsMap)
}
Loading