Add support for ECS Anywhere

This commit is contained in:
José Gaspar 2022-09-14 15:22:08 +01:00 committed by GitHub
parent fd95560c66
commit b351266b2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 11 deletions

View file

@ -47,7 +47,8 @@ Traefik needs the following policy to read ECS information:
"ecs:DescribeTasks",
"ecs:DescribeContainerInstances",
"ecs:DescribeTaskDefinition",
"ec2:DescribeInstances"
"ec2:DescribeInstances",
"ssm:DescribeInstanceInformation"
],
"Resource": [
"*"
@ -57,6 +58,10 @@ Traefik needs the following policy to read ECS information:
}
```
!!! info "ECS Anywhere"
Please note that the `ssm:DescribeInstanceInformation` action is required for ECS anywhere instances discovery.
## Provider Configuration
### `autoDiscoverClusters`
@ -86,6 +91,33 @@ providers:
# ...
```
### `ecsAnywhere`
_Optional, Default=false_
Enable ECS Anywhere support.
- If set to `true` service discovery is enabled for ECS Anywhere instances.
- If set to `false` service discovery is disabled for ECS Anywhere instances.
```yaml tab="File (YAML)"
providers:
ecs:
ecsAnywhere: true
# ...
```
```toml tab="File (TOML)"
[providers.ecs]
ecsAnywhere = true
# ...
```
```bash tab="CLI"
--providers.ecs.ecsAnywhere=true
# ...
```
### `clusters`
_Optional, Default=["default"]_

View file

@ -573,6 +573,9 @@ Constraints is an expression that Traefik matches against the container's labels
`--providers.ecs.defaultrule`:
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
`--providers.ecs.ecsanywhere`:
Enable ECS Anywhere support (Default: ```false```)
`--providers.ecs.exposedbydefault`:
Expose services by default (Default: ```true```)

View file

@ -573,6 +573,9 @@ Constraints is an expression that Traefik matches against the container's labels
`TRAEFIK_PROVIDERS_ECS_DEFAULTRULE`:
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
`TRAEFIK_PROVIDERS_ECS_ECSANYWHERE`:
Enable ECS Anywhere support (Default: ```false```)
`TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT`:
Expose services by default (Default: ```true```)

View file

@ -204,6 +204,7 @@
region = "foobar"
accessKeyID = "foobar"
secretAccessKey = "foobar"
ecsAnywhere = true
[providers.consul]
rootKey = "foobar"
endpoints = ["foobar", "foobar"]

View file

@ -220,6 +220,7 @@ providers:
region: foobar
accessKeyID: foobar
secretAccessKey: foobar
ecsAnywhere: true
consul:
rootKey: foobar
endpoints:

View file

@ -292,7 +292,7 @@ func (p *Provider) addServer(instance ecsInstance, loadBalancer *dynamic.Servers
func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, string, error) {
var ip, port string
ip = p.getIPAddress(instance)
ip = instance.machine.privateIP
port = getPort(instance, serverPort)
if len(ip) == 0 {
return "", "", fmt.Errorf("unable to find the IP address for the instance %q: the server is ignored", instance.Name)
@ -301,10 +301,6 @@ func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, s
return ip, port, nil
}
func (p Provider) getIPAddress(instance ecsInstance) string {
return instance.machine.privateIP
}
func getPort(instance ecsInstance, serverPort string) string {
if len(serverPort) > 0 {
for _, port := range instance.machine.ports {

View file

@ -14,6 +14,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/cenkalti/backoff/v4"
"github.com/patrickmn/go-cache"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
@ -33,6 +34,7 @@ type Provider struct {
// Provider lookup parameters.
Clusters []string `description:"ECS Clusters name" json:"clusters,omitempty" toml:"clusters,omitempty" yaml:"clusters,omitempty" export:"true"`
AutoDiscoverClusters bool `description:"Auto discover cluster" json:"autoDiscoverClusters,omitempty" toml:"autoDiscoverClusters,omitempty" yaml:"autoDiscoverClusters,omitempty" export:"true"`
ECSAnywhere bool `description:"Enable ECS Anywhere support" json:"ecsAnywhere,omitempty" toml:"ecsAnywhere,omitempty" yaml:"ecsAnywhere,omitempty" export:"true"`
Region string `description:"The AWS region to use for requests" json:"region,omitempty" toml:"region,omitempty" yaml:"region,omitempty" export:"true"`
AccessKeyID string `description:"The AWS credentials access key to use for making requests" json:"accessKeyID,omitempty" toml:"accessKeyID,omitempty" yaml:"accessKeyID,omitempty" loggable:"false"`
SecretAccessKey string `description:"The AWS credentials access key to use for making requests" json:"secretAccessKey,omitempty" toml:"secretAccessKey,omitempty" yaml:"secretAccessKey,omitempty" loggable:"false"`
@ -64,6 +66,7 @@ type machine struct {
type awsClient struct {
ecs *ecs.ECS
ec2 *ec2.EC2
ssm *ssm.SSM
}
// DefaultTemplateRule The default template for the default rule.
@ -139,11 +142,12 @@ func (p *Provider) createClient(logger log.Logger) (*awsClient, error) {
return &awsClient{
ecs.New(sess, cfg),
ec2.New(sess, cfg),
ssm.New(sess, cfg),
}, nil
}
// Provide configuration to traefik from ECS.
func (p Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error {
pool.GoCtx(func(routineCtx context.Context) {
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "ecs"))
logger := log.FromContext(ctxLog)
@ -277,6 +281,15 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
return nil, err
}
miInstances := make(map[string]*ssm.InstanceInformation)
if p.ECSAnywhere {
// Try looking up for instances on ECS Anywhere
miInstances, err = p.lookupMiInstances(ctx, client, &c, tasks)
if err != nil {
return nil, err
}
}
taskDefinitions, err := p.lookupTaskDefinitions(ctx, client, tasks)
if err != nil {
return nil, err
@ -324,7 +337,8 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
healthStatus: aws.StringValue(task.HealthStatus),
}
} else {
if containerInstance == nil {
miContainerInstance := miInstances[aws.StringValue(task.ContainerInstanceArn)]
if containerInstance == nil && miContainerInstance == nil {
logger.Errorf("Unable to find container instance information for %s", aws.StringValue(container.Name))
continue
}
@ -338,10 +352,19 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
})
}
}
var privateIPAddress, stateName string
if containerInstance != nil {
privateIPAddress = aws.StringValue(containerInstance.PrivateIpAddress)
stateName = aws.StringValue(containerInstance.State.Name)
} else if miContainerInstance != nil {
privateIPAddress = aws.StringValue(miContainerInstance.IPAddress)
stateName = aws.StringValue(task.LastStatus)
}
mach = &machine{
privateIP: aws.StringValue(containerInstance.PrivateIpAddress),
privateIP: privateIPAddress,
ports: ports,
state: aws.StringValue(containerInstance.State.Name),
state: stateName,
}
}
@ -368,6 +391,72 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
return instances, nil
}
func (p *Provider) lookupMiInstances(ctx context.Context, client *awsClient, clusterName *string, ecsDatas map[string]*ecs.Task) (map[string]*ssm.InstanceInformation, error) {
instanceIds := make(map[string]string)
miInstances := make(map[string]*ssm.InstanceInformation)
var containerInstancesArns []*string
var instanceArns []*string
for _, task := range ecsDatas {
if task.ContainerInstanceArn != nil {
containerInstancesArns = append(containerInstancesArns, task.ContainerInstanceArn)
}
}
for _, arns := range p.chunkIDs(containerInstancesArns) {
resp, err := client.ecs.DescribeContainerInstancesWithContext(ctx, &ecs.DescribeContainerInstancesInput{
ContainerInstances: arns,
Cluster: clusterName,
})
if err != nil {
return nil, fmt.Errorf("describing container instances: %w", err)
}
for _, container := range resp.ContainerInstances {
instanceIds[aws.StringValue(container.Ec2InstanceId)] = aws.StringValue(container.ContainerInstanceArn)
// Disallow EC2 Instance IDs
// This prevents considering EC2 instances in ECS
// and getting InvalidInstanceID.Malformed error when calling the describe-instances endpoint.
if !strings.HasPrefix(aws.StringValue(container.Ec2InstanceId), "mi-") {
continue
}
instanceArns = append(instanceArns, container.Ec2InstanceId)
}
}
if len(instanceArns) > 0 {
for _, ids := range p.chunkIDs(instanceArns) {
input := &ssm.DescribeInstanceInformationInput{
Filters: []*ssm.InstanceInformationStringFilter{
{
Key: aws.String("InstanceIds"),
Values: ids,
},
},
}
err := client.ssm.DescribeInstanceInformationPagesWithContext(ctx, input, func(page *ssm.DescribeInstanceInformationOutput, lastPage bool) bool {
if len(page.InstanceInformationList) > 0 {
for _, i := range page.InstanceInformationList {
if i.InstanceId != nil {
miInstances[instanceIds[aws.StringValue(i.InstanceId)]] = i
}
}
}
return !lastPage
})
if err != nil {
return nil, fmt.Errorf("describing instances: %w", err)
}
}
}
return miInstances, nil
}
func (p *Provider) lookupEc2Instances(ctx context.Context, client *awsClient, clusterName *string, ecsDatas map[string]*ecs.Task) (map[string]*ec2.Instance, error) {
instanceIds := make(map[string]string)
ec2Instances := make(map[string]*ec2.Instance)

View file

@ -721,6 +721,7 @@ func TestDo_staticConfiguration(t *testing.T) {
DefaultRule: "PathPrefix(`/`)",
Clusters: []string{"Cluster1", "Cluster2"},
AutoDiscoverClusters: true,
ECSAnywhere: true,
Region: "Awsregion",
AccessKeyID: "AwsAccessKeyID",
SecretAccessKey: "AwsSecretAccessKey",

View file

@ -223,6 +223,7 @@
"Cluster2"
],
"autoDiscoverClusters": true,
"ecsAnywhere": true,
"region": "Awsregion",
"accessKeyID": "xxxx",
"secretAccessKey": "xxxx"
@ -465,4 +466,4 @@
}
}
}
}
}