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: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"]_
|
||||
|
|
|
@ -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```)
|
||||
|
||||
|
|
|
@ -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```)
|
||||
|
||||
|
|
|
@ -204,6 +204,7 @@
|
|||
region = "foobar"
|
||||
accessKeyID = "foobar"
|
||||
secretAccessKey = "foobar"
|
||||
ecsAnywhere = true
|
||||
[providers.consul]
|
||||
rootKey = "foobar"
|
||||
endpoints = ["foobar", "foobar"]
|
||||
|
|
|
@ -220,6 +220,7 @@ providers:
|
|||
region: foobar
|
||||
accessKeyID: foobar
|
||||
secretAccessKey: foobar
|
||||
ecsAnywhere: true
|
||||
consul:
|
||||
rootKey: foobar
|
||||
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) {
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -223,6 +223,7 @@
|
|||
"Cluster2"
|
||||
],
|
||||
"autoDiscoverClusters": true,
|
||||
"ecsAnywhere": true,
|
||||
"region": "Awsregion",
|
||||
"accessKeyID": "xxxx",
|
||||
"secretAccessKey": "xxxx"
|
||||
|
@ -465,4 +466,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue