Merge pull request #1088 from lpetre/amazon_ecs_provider
Add an ECS provider
This commit is contained in:
commit
fce32ea5c7
10 changed files with 820 additions and 4 deletions
|
@ -50,6 +50,7 @@ type GlobalConfiguration struct {
|
||||||
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
|
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
|
||||||
Mesos *provider.Mesos `description:"Enable Mesos backend"`
|
Mesos *provider.Mesos `description:"Enable Mesos backend"`
|
||||||
Eureka *provider.Eureka `description:"Enable Eureka backend"`
|
Eureka *provider.Eureka `description:"Enable Eureka backend"`
|
||||||
|
ECS *provider.ECS `description:"Enable ECS backend"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultEntryPoints holds default entry points
|
// DefaultEntryPoints holds default entry points
|
||||||
|
@ -391,6 +392,14 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||||
defaultMesos.ExposedByDefault = true
|
defaultMesos.ExposedByDefault = true
|
||||||
defaultMesos.Constraints = types.Constraints{}
|
defaultMesos.Constraints = types.Constraints{}
|
||||||
|
|
||||||
|
//default ECS
|
||||||
|
var defaultECS provider.ECS
|
||||||
|
defaultECS.Watch = true
|
||||||
|
defaultECS.ExposedByDefault = true
|
||||||
|
defaultECS.RefreshSeconds = 15
|
||||||
|
defaultECS.Cluster = "default"
|
||||||
|
defaultECS.Constraints = types.Constraints{}
|
||||||
|
|
||||||
defaultConfiguration := GlobalConfiguration{
|
defaultConfiguration := GlobalConfiguration{
|
||||||
Docker: &defaultDocker,
|
Docker: &defaultDocker,
|
||||||
File: &defaultFile,
|
File: &defaultFile,
|
||||||
|
@ -403,6 +412,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||||
Boltdb: &defaultBoltDb,
|
Boltdb: &defaultBoltDb,
|
||||||
Kubernetes: &defaultKubernetes,
|
Kubernetes: &defaultKubernetes,
|
||||||
Mesos: &defaultMesos,
|
Mesos: &defaultMesos,
|
||||||
|
ECS: &defaultECS,
|
||||||
Retry: &Retry{},
|
Retry: &Retry{},
|
||||||
}
|
}
|
||||||
return &TraefikConfiguration{
|
return &TraefikConfiguration{
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
|
|
||||||
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically.
|
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Amazon ECS](https://aws.amazon.com/ecs/), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|
75
docs/toml.md
75
docs/toml.md
|
@ -1347,3 +1347,78 @@ delay = "1m"
|
||||||
```
|
```
|
||||||
|
|
||||||
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on traefik KV structure.
|
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on traefik KV structure.
|
||||||
|
|
||||||
|
|
||||||
|
## ECS backend
|
||||||
|
|
||||||
|
Træfɪk can be configured to use Amazon ECS as a backend configuration:
|
||||||
|
|
||||||
|
|
||||||
|
```toml
|
||||||
|
################################################################
|
||||||
|
# ECS configuration backend
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable ECS configuration backend
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
[ecs]
|
||||||
|
|
||||||
|
# ECS Cluster Name
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "default"
|
||||||
|
#
|
||||||
|
Cluster = "default"
|
||||||
|
|
||||||
|
# Enable watch ECS changes
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
Watch = true
|
||||||
|
|
||||||
|
# Polling interval (in seconds)
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: 15
|
||||||
|
#
|
||||||
|
RefreshSeconds = 15
|
||||||
|
|
||||||
|
# Expose ECS services by default in traefik
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
ExposedByDefault = false
|
||||||
|
|
||||||
|
# Region to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# Region = "us-east-1"
|
||||||
|
|
||||||
|
# AccessKeyID to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# AccessKeyID = "abc"
|
||||||
|
|
||||||
|
# SecretAccessKey to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# SecretAccessKey = "123"
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Labels can be used on task containers to override default behaviour:
|
||||||
|
|
||||||
|
- `traefik.protocol=https`: override the default `http` protocol
|
||||||
|
- `traefik.weight=10`: assign this weight to the container
|
||||||
|
- `traefik.enable=false`: disable this container in Træfɪk
|
||||||
|
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||||
|
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||||
|
- `traefik.frontend.priority=10`: override default frontend priority
|
||||||
|
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||||
|
|
11
glide.lock
generated
11
glide.lock
generated
|
@ -1,5 +1,5 @@
|
||||||
hash: 2a2f73984ec9cccd62220c0eba5ddc82dec96b0044ffed48b25d964e2f3eee05
|
hash: a0b0abed2162e490cbe75a6a36ebaaf39e748ee80e419e879e7253679a0bc134
|
||||||
updated: 2017-02-05T17:22:02.550162979+01:00
|
updated: 2017-02-05T18:09:09.856588042Z
|
||||||
imports:
|
imports:
|
||||||
- name: bitbucket.org/ww/goautoneg
|
- name: bitbucket.org/ww/goautoneg
|
||||||
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
|
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
|
||||||
|
@ -35,12 +35,17 @@ imports:
|
||||||
- aws/session
|
- aws/session
|
||||||
- aws/signer/v4
|
- aws/signer/v4
|
||||||
- private/protocol
|
- private/protocol
|
||||||
|
- private/protocol/ec2query
|
||||||
|
- private/protocol/json/jsonutil
|
||||||
|
- private/protocol/jsonrpc
|
||||||
- private/protocol/query
|
- private/protocol/query
|
||||||
- private/protocol/query/queryutil
|
- private/protocol/query/queryutil
|
||||||
- private/protocol/rest
|
- private/protocol/rest
|
||||||
- private/protocol/restxml
|
- private/protocol/restxml
|
||||||
- private/protocol/xml/xmlutil
|
- private/protocol/xml/xmlutil
|
||||||
- private/waiter
|
- private/waiter
|
||||||
|
- service/ec2
|
||||||
|
- service/ecs
|
||||||
- service/route53
|
- service/route53
|
||||||
- service/sts
|
- service/sts
|
||||||
- name: github.com/Azure/azure-sdk-for-go
|
- name: github.com/Azure/azure-sdk-for-go
|
||||||
|
@ -735,7 +740,7 @@ testImports:
|
||||||
subpackages:
|
subpackages:
|
||||||
- specs-go
|
- specs-go
|
||||||
- name: github.com/pkg/errors
|
- name: github.com/pkg/errors
|
||||||
version: 248dadf4e9068a0b3e79f02ed0a610d935de5302
|
version: 01fa4104b9c248c8945d14d9f128454d5b28d595
|
||||||
- name: github.com/vbatts/tar-split
|
- name: github.com/vbatts/tar-split
|
||||||
version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df
|
version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df
|
||||||
subpackages:
|
subpackages:
|
||||||
|
|
|
@ -119,7 +119,15 @@ import:
|
||||||
- package: github.com/aws/aws-sdk-go
|
- package: github.com/aws/aws-sdk-go
|
||||||
version: v1.6.18
|
version: v1.6.18
|
||||||
subpackages:
|
subpackages:
|
||||||
|
- aws
|
||||||
|
- aws/credentials
|
||||||
|
- aws/defaults
|
||||||
|
- aws/ec2metadata
|
||||||
- aws/endpoints
|
- aws/endpoints
|
||||||
|
- aws/request
|
||||||
|
- aws/session
|
||||||
|
- service/ec2
|
||||||
|
- service/ecs
|
||||||
- package: cloud.google.com/go
|
- package: cloud.google.com/go
|
||||||
version: v0.6.0
|
version: v0.6.0
|
||||||
subpackages:
|
subpackages:
|
||||||
|
|
414
provider/ecs.go
Normal file
414
provider/ecs.go
Normal file
|
@ -0,0 +1,414 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/ty/fun"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/request"
|
||||||
|
"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/cenk/backoff"
|
||||||
|
"github.com/containous/traefik/job"
|
||||||
|
"github.com/containous/traefik/log"
|
||||||
|
"github.com/containous/traefik/safe"
|
||||||
|
"github.com/containous/traefik/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Provider = (*ECS)(nil)
|
||||||
|
|
||||||
|
// ECS holds configurations of the ECS provider.
|
||||||
|
type ECS struct {
|
||||||
|
BaseProvider `mapstructure:",squash"`
|
||||||
|
|
||||||
|
Domain string `description:"Default domain used"`
|
||||||
|
ExposedByDefault bool `description:"Expose containers by default"`
|
||||||
|
RefreshSeconds int `description:"Polling interval (in seconds)"`
|
||||||
|
|
||||||
|
// ECS lookup parameters
|
||||||
|
Cluster string `description:"ECS Cluster Name"`
|
||||||
|
Region string `description:"The AWS region to use for requests"`
|
||||||
|
AccessKeyID string `description:"The AWS credentials access key to use for making requests"`
|
||||||
|
SecretAccessKey string `description:"The AWS credentials access key to use for making requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecsInstance struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
task *ecs.Task
|
||||||
|
taskDefinition *ecs.TaskDefinition
|
||||||
|
container *ecs.Container
|
||||||
|
containerDefinition *ecs.ContainerDefinition
|
||||||
|
machine *ec2.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
type awsClient struct {
|
||||||
|
ecs *ecs.ECS
|
||||||
|
ec2 *ec2.EC2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) createClient() (*awsClient, error) {
|
||||||
|
sess := session.New()
|
||||||
|
ec2meta := ec2metadata.New(sess)
|
||||||
|
if provider.Region == "" {
|
||||||
|
log.Infoln("No EC2 region provided, querying instance metadata endpoint...")
|
||||||
|
identity, err := ec2meta.GetInstanceIdentityDocument()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
provider.Region = identity.Region
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &aws.Config{
|
||||||
|
Region: &provider.Region,
|
||||||
|
Credentials: credentials.NewChainCredentials(
|
||||||
|
[]credentials.Provider{
|
||||||
|
&credentials.StaticProvider{
|
||||||
|
Value: credentials.Value{
|
||||||
|
AccessKeyID: provider.AccessKeyID,
|
||||||
|
SecretAccessKey: provider.SecretAccessKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&credentials.EnvProvider{},
|
||||||
|
&credentials.SharedCredentialsProvider{},
|
||||||
|
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &awsClient{
|
||||||
|
ecs.New(sess, cfg),
|
||||||
|
ec2.New(sess, cfg),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide allows the provider to provide configurations to traefik
|
||||||
|
// using the given configuration channel.
|
||||||
|
func (provider *ECS) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
||||||
|
|
||||||
|
provider.Constraints = append(provider.Constraints, constraints...)
|
||||||
|
|
||||||
|
handleCanceled := func(ctx context.Context, err error) error {
|
||||||
|
if ctx.Err() == context.Canceled || err == context.Canceled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Go(func(stop chan bool) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
aws, err := provider.createClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||||
|
if err != nil {
|
||||||
|
return handleCanceled(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationChan <- types.ConfigMessage{
|
||||||
|
ProviderName: "ecs",
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.Watch {
|
||||||
|
reload := time.NewTicker(time.Second * time.Duration(provider.RefreshSeconds))
|
||||||
|
defer reload.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-reload.C:
|
||||||
|
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||||
|
if err != nil {
|
||||||
|
return handleCanceled(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationChan <- types.ConfigMessage{
|
||||||
|
ProviderName: "ecs",
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return handleCanceled(ctx, ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
log.Errorf("ECS connection error %+v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Cannot connect to ECS api %+v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapAws(ctx context.Context, req *request.Request) error {
|
||||||
|
req.HTTPRequest = req.HTTPRequest.WithContext(ctx)
|
||||||
|
return req.Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) {
|
||||||
|
var ecsFuncMap = template.FuncMap{
|
||||||
|
"filterFrontends": provider.filterFrontends,
|
||||||
|
"getFrontendRule": provider.getFrontendRule,
|
||||||
|
}
|
||||||
|
|
||||||
|
instances, err := provider.listInstances(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
instances = fun.Filter(provider.filterInstance, instances).([]ecsInstance)
|
||||||
|
|
||||||
|
return provider.getConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
|
||||||
|
Instances []ecsInstance
|
||||||
|
}{
|
||||||
|
instances,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all running ECS tasks in a cluster, also collect the task definitions (for docker labels)
|
||||||
|
// and the EC2 instance data
|
||||||
|
func (provider *ECS) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) {
|
||||||
|
var taskArns []*string
|
||||||
|
req, _ := client.ecs.ListTasksRequest(&ecs.ListTasksInput{
|
||||||
|
Cluster: &provider.Cluster,
|
||||||
|
DesiredStatus: aws.String(ecs.DesiredStatusRunning),
|
||||||
|
})
|
||||||
|
|
||||||
|
for ; req != nil; req = req.NextPage() {
|
||||||
|
if err := wrapAws(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskArns = append(taskArns, req.Data.(*ecs.ListTasksOutput).TaskArns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, taskResp := client.ecs.DescribeTasksRequest(&ecs.DescribeTasksInput{
|
||||||
|
Tasks: taskArns,
|
||||||
|
Cluster: &provider.Cluster,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wrapAws(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
containerInstanceArns := make([]*string, 0)
|
||||||
|
byContainerInstance := make(map[string]int)
|
||||||
|
|
||||||
|
taskDefinitionArns := make([]*string, 0)
|
||||||
|
byTaskDefinition := make(map[string]int)
|
||||||
|
|
||||||
|
for _, task := range taskResp.Tasks {
|
||||||
|
if _, found := byContainerInstance[*task.ContainerInstanceArn]; !found {
|
||||||
|
byContainerInstance[*task.ContainerInstanceArn] = len(containerInstanceArns)
|
||||||
|
containerInstanceArns = append(containerInstanceArns, task.ContainerInstanceArn)
|
||||||
|
}
|
||||||
|
if _, found := byTaskDefinition[*task.TaskDefinitionArn]; !found {
|
||||||
|
byTaskDefinition[*task.TaskDefinitionArn] = len(taskDefinitionArns)
|
||||||
|
taskDefinitionArns = append(taskDefinitionArns, task.TaskDefinitionArn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
machines, err := provider.lookupEc2Instances(ctx, client, containerInstanceArns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDefinitions, err := provider.lookupTaskDefinitions(ctx, client, taskDefinitionArns)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances []ecsInstance
|
||||||
|
for _, task := range taskResp.Tasks {
|
||||||
|
|
||||||
|
machineIdx := byContainerInstance[*task.ContainerInstanceArn]
|
||||||
|
taskDefIdx := byTaskDefinition[*task.TaskDefinitionArn]
|
||||||
|
|
||||||
|
for _, container := range task.Containers {
|
||||||
|
|
||||||
|
taskDefinition := taskDefinitions[taskDefIdx]
|
||||||
|
var containerDefinition *ecs.ContainerDefinition
|
||||||
|
for _, def := range taskDefinition.ContainerDefinitions {
|
||||||
|
if *container.Name == *def.Name {
|
||||||
|
containerDefinition = def
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instances = append(instances, ecsInstance{
|
||||||
|
fmt.Sprintf("%s-%s", strings.Replace(*task.Group, ":", "-", 1), *container.Name),
|
||||||
|
(*task.TaskArn)[len(*task.TaskArn)-12:],
|
||||||
|
task,
|
||||||
|
taskDefinition,
|
||||||
|
container,
|
||||||
|
containerDefinition,
|
||||||
|
machines[machineIdx],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) lookupEc2Instances(ctx context.Context, client *awsClient, containerArns []*string) ([]*ec2.Instance, error) {
|
||||||
|
req, containerResp := client.ecs.DescribeContainerInstancesRequest(&ecs.DescribeContainerInstancesInput{
|
||||||
|
ContainerInstances: containerArns,
|
||||||
|
Cluster: &provider.Cluster,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wrapAws(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
order := make(map[string]int)
|
||||||
|
for i, arn := range containerArns {
|
||||||
|
order[*arn] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIds := make([]*string, len(containerArns))
|
||||||
|
for i, container := range containerResp.ContainerInstances {
|
||||||
|
order[*container.Ec2InstanceId] = order[*container.ContainerInstanceArn]
|
||||||
|
instanceIds[i] = container.Ec2InstanceId
|
||||||
|
}
|
||||||
|
|
||||||
|
req, instancesResp := client.ec2.DescribeInstancesRequest(&ec2.DescribeInstancesInput{
|
||||||
|
InstanceIds: instanceIds,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wrapAws(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
instances := make([]*ec2.Instance, len(containerArns))
|
||||||
|
for _, r := range instancesResp.Reservations {
|
||||||
|
instances[order[*r.Instances[0].InstanceId]] = r.Instances[0]
|
||||||
|
}
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns []*string) ([]*ecs.TaskDefinition, error) {
|
||||||
|
taskDefinitions := make([]*ecs.TaskDefinition, len(taskDefArns))
|
||||||
|
for i, arn := range taskDefArns {
|
||||||
|
|
||||||
|
req, resp := client.ecs.DescribeTaskDefinitionRequest(&ecs.DescribeTaskDefinitionInput{
|
||||||
|
TaskDefinition: arn,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wrapAws(ctx, req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDefinitions[i] = resp.TaskDefinition
|
||||||
|
}
|
||||||
|
return taskDefinitions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) label(k string) string {
|
||||||
|
if v, found := i.containerDefinition.DockerLabels[k]; found {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) filterInstance(i ecsInstance) bool {
|
||||||
|
if len(i.container.NetworkBindings) == 0 {
|
||||||
|
log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
label := i.label("traefik.enable")
|
||||||
|
enabled := provider.ExposedByDefault && label != "false" || label == "true"
|
||||||
|
if !enabled {
|
||||||
|
log.Debugf("Filtering disabled ecs instance %s (%s) (traefik.enabled = '%s')", i.Name, i.ID, label)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) filterFrontends(instances []ecsInstance) []ecsInstance {
|
||||||
|
byName := make(map[string]bool)
|
||||||
|
|
||||||
|
return fun.Filter(func(i ecsInstance) bool {
|
||||||
|
if _, found := byName[i.Name]; !found {
|
||||||
|
byName[i.Name] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}, instances).([]ecsInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *ECS) getFrontendRule(i ecsInstance) string {
|
||||||
|
if label := i.label("traefik.frontend.rule"); label != "" {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + provider.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) Protocol() string {
|
||||||
|
if label := i.label("traefik.protocol"); label != "" {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) Host() string {
|
||||||
|
return *i.machine.PrivateIpAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) Port() string {
|
||||||
|
return strconv.FormatInt(*i.container.NetworkBindings[0].HostPort, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) Weight() string {
|
||||||
|
if label := i.label("traefik.weight"); label != "" {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) PassHostHeader() string {
|
||||||
|
if label := i.label("traefik.frontend.passHostHeader"); label != "" {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) Priority() string {
|
||||||
|
if label := i.label("traefik.frontend.priority"); label != "" {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ecsInstance) EntryPoints() []string {
|
||||||
|
if label := i.label("traefik.frontend.entryPoints"); label != "" {
|
||||||
|
return strings.Split(label, ",")
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
223
provider/ecs_test.go
Normal file
223
provider/ecs_test.go
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ecs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance {
|
||||||
|
container := &ecs.Container{
|
||||||
|
Name: containerDef.Name,
|
||||||
|
NetworkBindings: make([]*ecs.NetworkBinding, len(containerDef.PortMappings)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, pm := range containerDef.PortMappings {
|
||||||
|
container.NetworkBindings[i] = &ecs.NetworkBinding{
|
||||||
|
HostPort: pm.HostPort,
|
||||||
|
ContainerPort: pm.ContainerPort,
|
||||||
|
Protocol: pm.Protocol,
|
||||||
|
BindIP: aws.String("0.0.0.0"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ecsInstance{
|
||||||
|
Name: "foo-http",
|
||||||
|
ID: "123456789abc",
|
||||||
|
task: &ecs.Task{
|
||||||
|
Containers: []*ecs.Container{container},
|
||||||
|
},
|
||||||
|
taskDefinition: &ecs.TaskDefinition{
|
||||||
|
ContainerDefinitions: []*ecs.ContainerDefinition{containerDef},
|
||||||
|
},
|
||||||
|
container: container,
|
||||||
|
containerDefinition: containerDef,
|
||||||
|
machine: &ec2.Instance{
|
||||||
|
PrivateIpAddress: aws.String("10.0.0.0"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleEcsInstance(labels map[string]*string) ecsInstance {
|
||||||
|
return makeEcsInstance(&ecs.ContainerDefinition{
|
||||||
|
Name: aws.String("http"),
|
||||||
|
PortMappings: []*ecs.PortMapping{{
|
||||||
|
HostPort: aws.Int64(80),
|
||||||
|
ContainerPort: aws.Int64(80),
|
||||||
|
Protocol: aws.String("tcp"),
|
||||||
|
}},
|
||||||
|
DockerLabels: labels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsProtocol(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "http",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: "https",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.protocol": aws.String("https"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.Protocol()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsHost(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "10.0.0.0",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.Host()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsPort(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "80",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.Port()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsWeight(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "0",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: "10",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.weight": aws.String("10"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.Weight()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsPassHostHeader(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "true",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: "false",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.frontend.passHostHeader": aws.String("false"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.PassHostHeader()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsPriority(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: "0",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: "10",
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.frontend.priority": aws.String("10"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.Priority()
|
||||||
|
if value != c.expected {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEcsEntryPoints(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
expected []string
|
||||||
|
instanceInfo ecsInstance
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expected: []string{},
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: []string{"http"},
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.frontend.entryPoints": aws.String("http"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expected: []string{"http", "https"},
|
||||||
|
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||||
|
"traefik.frontend.entryPoints": aws.String("http,https"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
value := c.instanceInfo.EntryPoints()
|
||||||
|
if !reflect.DeepEqual(value, c.expected) {
|
||||||
|
t.Fatalf("Should have been %s, got %s", c.expected, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -373,6 +373,9 @@ func (server *Server) configureProviders() {
|
||||||
if server.globalConfiguration.Eureka != nil {
|
if server.globalConfiguration.Eureka != nil {
|
||||||
server.providers = append(server.providers, server.globalConfiguration.Eureka)
|
server.providers = append(server.providers, server.globalConfiguration.Eureka)
|
||||||
}
|
}
|
||||||
|
if server.globalConfiguration.ECS != nil {
|
||||||
|
server.providers = append(server.providers, server.globalConfiguration.ECS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) startProviders() {
|
func (server *Server) startProviders() {
|
||||||
|
|
17
templates/ecs.tmpl
Normal file
17
templates/ecs.tmpl
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[backends]{{range .Instances}}
|
||||||
|
[backends.backend-{{ .Name }}.servers.server-{{ .Name }}{{ .ID }}]
|
||||||
|
url = "{{ .Protocol }}://{{ .Host }}:{{ .Port }}"
|
||||||
|
weight = {{ .Weight }}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
[frontends]{{range filterFrontends .Instances}}
|
||||||
|
[frontends.frontend-{{ .Name }}]
|
||||||
|
backend = "backend-{{ .Name }}"
|
||||||
|
passHostHeader = {{ .PassHostHeader }}
|
||||||
|
priority = {{ .Priority }}
|
||||||
|
entryPoints = [{{range .EntryPoints }}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
[frontends.frontend-{{ .Name }}.routes.route-frontend-{{ .Name }}]
|
||||||
|
rule = "{{getFrontendRule .}}"
|
||||||
|
{{end}}
|
|
@ -868,6 +868,67 @@
|
||||||
# filename = "boltdb.tmpl"
|
# filename = "boltdb.tmpl"
|
||||||
|
|
||||||
|
|
||||||
|
################################################################
|
||||||
|
# ECS configuration backend
|
||||||
|
################################################################
|
||||||
|
|
||||||
|
# Enable ECS configuration backend
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# [ecs]
|
||||||
|
|
||||||
|
# ECS Cluster Name
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: "default"
|
||||||
|
#
|
||||||
|
# Cluster = "default"
|
||||||
|
|
||||||
|
# Enable watch ECS changes
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
# Watch = true
|
||||||
|
|
||||||
|
# Polling interval (in seconds)
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: 15
|
||||||
|
#
|
||||||
|
# RefreshSeconds = 15
|
||||||
|
|
||||||
|
# Expose ECS services by default in traefik
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
# Default: true
|
||||||
|
#
|
||||||
|
# ExposedByDefault = false
|
||||||
|
|
||||||
|
# Region to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# Region = "us-east-1"
|
||||||
|
|
||||||
|
# AccessKeyID to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# AccessKeyID = "abc"
|
||||||
|
|
||||||
|
# SecretAccessKey to use when connecting to AWS
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# SecretAccessKey = "123"
|
||||||
|
|
||||||
|
# Override default configuration template. For advanced users :)
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
# filename = "ecs.tmpl"
|
||||||
|
|
||||||
|
|
||||||
################################################################
|
################################################################
|
||||||
|
|
Loading…
Reference in a new issue