Merge pull request #1088 from lpetre/amazon_ecs_provider

Add an ECS provider
This commit is contained in:
Emile Vauge 2017-02-05 21:01:17 +01:00 committed by GitHub
commit fce32ea5c7
10 changed files with 820 additions and 4 deletions

View file

@ -50,6 +50,7 @@ type GlobalConfiguration struct {
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
Mesos *provider.Mesos `description:"Enable Mesos backend"`
Eureka *provider.Eureka `description:"Enable Eureka backend"`
ECS *provider.ECS `description:"Enable ECS backend"`
}
// DefaultEntryPoints holds default entry points
@ -391,6 +392,14 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultMesos.ExposedByDefault = true
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{
Docker: &defaultDocker,
File: &defaultFile,
@ -403,6 +412,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
Boltdb: &defaultBoltDb,
Kubernetes: &defaultKubernetes,
Mesos: &defaultMesos,
ECS: &defaultECS,
Retry: &Retry{},
}
return &TraefikConfiguration{

View file

@ -11,7 +11,7 @@
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

View file

@ -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.
## 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
View file

@ -1,5 +1,5 @@
hash: 2a2f73984ec9cccd62220c0eba5ddc82dec96b0044ffed48b25d964e2f3eee05
updated: 2017-02-05T17:22:02.550162979+01:00
hash: a0b0abed2162e490cbe75a6a36ebaaf39e748ee80e419e879e7253679a0bc134
updated: 2017-02-05T18:09:09.856588042Z
imports:
- name: bitbucket.org/ww/goautoneg
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
@ -35,12 +35,17 @@ imports:
- aws/session
- aws/signer/v4
- private/protocol
- private/protocol/ec2query
- private/protocol/json/jsonutil
- private/protocol/jsonrpc
- private/protocol/query
- private/protocol/query/queryutil
- private/protocol/rest
- private/protocol/restxml
- private/protocol/xml/xmlutil
- private/waiter
- service/ec2
- service/ecs
- service/route53
- service/sts
- name: github.com/Azure/azure-sdk-for-go
@ -735,7 +740,7 @@ testImports:
subpackages:
- specs-go
- name: github.com/pkg/errors
version: 248dadf4e9068a0b3e79f02ed0a610d935de5302
version: 01fa4104b9c248c8945d14d9f128454d5b28d595
- name: github.com/vbatts/tar-split
version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df
subpackages:

View file

@ -119,7 +119,15 @@ import:
- package: github.com/aws/aws-sdk-go
version: v1.6.18
subpackages:
- aws
- aws/credentials
- aws/defaults
- aws/ec2metadata
- aws/endpoints
- aws/request
- aws/session
- service/ec2
- service/ecs
- package: cloud.google.com/go
version: v0.6.0
subpackages:

414
provider/ecs.go Normal file
View 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
View 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)
}
}
}

View file

@ -373,6 +373,9 @@ func (server *Server) configureProviders() {
if server.globalConfiguration.Eureka != nil {
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() {

17
templates/ecs.tmpl Normal file
View 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}}

View file

@ -868,6 +868,67 @@
# 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"
################################################################