Add support for ECS Anywhere
This commit is contained in:
parent
fd95560c66
commit
b351266b2d
9 changed files with 138 additions and 11 deletions
|
@ -47,7 +47,8 @@ Traefik needs the following policy to read ECS information:
|
||||||
"ecs:DescribeTasks",
|
"ecs:DescribeTasks",
|
||||||
"ecs:DescribeContainerInstances",
|
"ecs:DescribeContainerInstances",
|
||||||
"ecs:DescribeTaskDefinition",
|
"ecs:DescribeTaskDefinition",
|
||||||
"ec2:DescribeInstances"
|
"ec2:DescribeInstances",
|
||||||
|
"ssm:DescribeInstanceInformation"
|
||||||
],
|
],
|
||||||
"Resource": [
|
"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
|
## Provider Configuration
|
||||||
|
|
||||||
### `autoDiscoverClusters`
|
### `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`
|
### `clusters`
|
||||||
|
|
||||||
_Optional, Default=["default"]_
|
_Optional, Default=["default"]_
|
||||||
|
|
|
@ -573,6 +573,9 @@ Constraints is an expression that Traefik matches against the container's labels
|
||||||
`--providers.ecs.defaultrule`:
|
`--providers.ecs.defaultrule`:
|
||||||
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`--providers.ecs.ecsanywhere`:
|
||||||
|
Enable ECS Anywhere support (Default: ```false```)
|
||||||
|
|
||||||
`--providers.ecs.exposedbydefault`:
|
`--providers.ecs.exposedbydefault`:
|
||||||
Expose services by default (Default: ```true```)
|
Expose services by default (Default: ```true```)
|
||||||
|
|
||||||
|
|
|
@ -573,6 +573,9 @@ Constraints is an expression that Traefik matches against the container's labels
|
||||||
`TRAEFIK_PROVIDERS_ECS_DEFAULTRULE`:
|
`TRAEFIK_PROVIDERS_ECS_DEFAULTRULE`:
|
||||||
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_ECSANYWHERE`:
|
||||||
|
Enable ECS Anywhere support (Default: ```false```)
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT`:
|
`TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT`:
|
||||||
Expose services by default (Default: ```true```)
|
Expose services by default (Default: ```true```)
|
||||||
|
|
||||||
|
|
|
@ -204,6 +204,7 @@
|
||||||
region = "foobar"
|
region = "foobar"
|
||||||
accessKeyID = "foobar"
|
accessKeyID = "foobar"
|
||||||
secretAccessKey = "foobar"
|
secretAccessKey = "foobar"
|
||||||
|
ecsAnywhere = true
|
||||||
[providers.consul]
|
[providers.consul]
|
||||||
rootKey = "foobar"
|
rootKey = "foobar"
|
||||||
endpoints = ["foobar", "foobar"]
|
endpoints = ["foobar", "foobar"]
|
||||||
|
|
|
@ -220,6 +220,7 @@ providers:
|
||||||
region: foobar
|
region: foobar
|
||||||
accessKeyID: foobar
|
accessKeyID: foobar
|
||||||
secretAccessKey: foobar
|
secretAccessKey: foobar
|
||||||
|
ecsAnywhere: true
|
||||||
consul:
|
consul:
|
||||||
rootKey: foobar
|
rootKey: foobar
|
||||||
endpoints:
|
endpoints:
|
||||||
|
|
|
@ -292,7 +292,7 @@ func (p *Provider) addServer(instance ecsInstance, loadBalancer *dynamic.Servers
|
||||||
func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, string, error) {
|
func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, string, error) {
|
||||||
var ip, port string
|
var ip, port string
|
||||||
|
|
||||||
ip = p.getIPAddress(instance)
|
ip = instance.machine.privateIP
|
||||||
port = getPort(instance, serverPort)
|
port = getPort(instance, serverPort)
|
||||||
if len(ip) == 0 {
|
if len(ip) == 0 {
|
||||||
return "", "", fmt.Errorf("unable to find the IP address for the instance %q: the server is ignored", instance.Name)
|
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
|
return ip, port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Provider) getIPAddress(instance ecsInstance) string {
|
|
||||||
return instance.machine.privateIP
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPort(instance ecsInstance, serverPort string) string {
|
func getPort(instance ecsInstance, serverPort string) string {
|
||||||
if len(serverPort) > 0 {
|
if len(serverPort) > 0 {
|
||||||
for _, port := range instance.machine.ports {
|
for _, port := range instance.machine.ports {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/ec2"
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
"github.com/aws/aws-sdk-go/service/ecs"
|
"github.com/aws/aws-sdk-go/service/ecs"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ssm"
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||||
|
@ -33,6 +34,7 @@ type Provider struct {
|
||||||
// Provider lookup parameters.
|
// Provider lookup parameters.
|
||||||
Clusters []string `description:"ECS Clusters name" json:"clusters,omitempty" toml:"clusters,omitempty" yaml:"clusters,omitempty" export:"true"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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 {
|
type awsClient struct {
|
||||||
ecs *ecs.ECS
|
ecs *ecs.ECS
|
||||||
ec2 *ec2.EC2
|
ec2 *ec2.EC2
|
||||||
|
ssm *ssm.SSM
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultTemplateRule The default template for the default rule.
|
// DefaultTemplateRule The default template for the default rule.
|
||||||
|
@ -139,11 +142,12 @@ func (p *Provider) createClient(logger log.Logger) (*awsClient, error) {
|
||||||
return &awsClient{
|
return &awsClient{
|
||||||
ecs.New(sess, cfg),
|
ecs.New(sess, cfg),
|
||||||
ec2.New(sess, cfg),
|
ec2.New(sess, cfg),
|
||||||
|
ssm.New(sess, cfg),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide configuration to traefik from ECS.
|
// 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) {
|
pool.GoCtx(func(routineCtx context.Context) {
|
||||||
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "ecs"))
|
ctxLog := log.With(routineCtx, log.Str(log.ProviderName, "ecs"))
|
||||||
logger := log.FromContext(ctxLog)
|
logger := log.FromContext(ctxLog)
|
||||||
|
@ -277,6 +281,15 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
|
||||||
return nil, err
|
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)
|
taskDefinitions, err := p.lookupTaskDefinitions(ctx, client, tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -324,7 +337,8 @@ func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsI
|
||||||
healthStatus: aws.StringValue(task.HealthStatus),
|
healthStatus: aws.StringValue(task.HealthStatus),
|
||||||
}
|
}
|
||||||
} else {
|
} 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))
|
logger.Errorf("Unable to find container instance information for %s", aws.StringValue(container.Name))
|
||||||
continue
|
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{
|
mach = &machine{
|
||||||
privateIP: aws.StringValue(containerInstance.PrivateIpAddress),
|
privateIP: privateIPAddress,
|
||||||
ports: ports,
|
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
|
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) {
|
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)
|
instanceIds := make(map[string]string)
|
||||||
ec2Instances := make(map[string]*ec2.Instance)
|
ec2Instances := make(map[string]*ec2.Instance)
|
||||||
|
|
|
@ -721,6 +721,7 @@ func TestDo_staticConfiguration(t *testing.T) {
|
||||||
DefaultRule: "PathPrefix(`/`)",
|
DefaultRule: "PathPrefix(`/`)",
|
||||||
Clusters: []string{"Cluster1", "Cluster2"},
|
Clusters: []string{"Cluster1", "Cluster2"},
|
||||||
AutoDiscoverClusters: true,
|
AutoDiscoverClusters: true,
|
||||||
|
ECSAnywhere: true,
|
||||||
Region: "Awsregion",
|
Region: "Awsregion",
|
||||||
AccessKeyID: "AwsAccessKeyID",
|
AccessKeyID: "AwsAccessKeyID",
|
||||||
SecretAccessKey: "AwsSecretAccessKey",
|
SecretAccessKey: "AwsSecretAccessKey",
|
||||||
|
|
|
@ -223,6 +223,7 @@
|
||||||
"Cluster2"
|
"Cluster2"
|
||||||
],
|
],
|
||||||
"autoDiscoverClusters": true,
|
"autoDiscoverClusters": true,
|
||||||
|
"ecsAnywhere": true,
|
||||||
"region": "Awsregion",
|
"region": "Awsregion",
|
||||||
"accessKeyID": "xxxx",
|
"accessKeyID": "xxxx",
|
||||||
"secretAccessKey": "xxxx"
|
"secretAccessKey": "xxxx"
|
||||||
|
|
Loading…
Reference in a new issue