Add AWS ECS provider
* add ecs provider * add ecs docs * fix test after rebase * add provider icon * add missing addProvider call * Fix for review * Fix documentation * Fix for review * Fix documentation * fix ctx usage * autoDiscoverClusters setDefaults false * Fix for review * review: doc. * Fix for review: add ctx in backoff retry * review: linter. Co-authored-by: Michael <michael.matur@gmail.com> Co-authored-by: romain <romain@containo.us> Co-authored-by: Fernandez Ludovic <ludovic@containo.us>
This commit is contained in:
parent
6e4f5821dc
commit
285ded6e49
19 changed files with 4348 additions and 0 deletions
194
docs/content/providers/ecs.md
Normal file
194
docs/content/providers/ecs.md
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
# Traefik & AWS ECS
|
||||||
|
|
||||||
|
A Story of Labels & Elastic Containers
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
Attach labels to your ECS containers and let Traefik do the rest!
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
??? example "Configuring ECS provider"
|
||||||
|
|
||||||
|
Enabling the ECS provider:
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
clusters = ["default"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
clusters:
|
||||||
|
- default
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.clusters=default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
Traefik needs the following policy to read ECS information:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "TraefikECSReadAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"ecs:ListClusters",
|
||||||
|
"ecs:DescribeClusters",
|
||||||
|
"ecs:ListTasks",
|
||||||
|
"ecs:DescribeTasks",
|
||||||
|
"ecs:DescribeContainerInstances",
|
||||||
|
"ecs:DescribeTaskDefinition",
|
||||||
|
"ec2:DescribeInstances"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provider configuration
|
||||||
|
|
||||||
|
### `autoDiscoverClusters`
|
||||||
|
|
||||||
|
_Optional, Default=false_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
autoDiscoverClusters = true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
autoDiscoverClusters: true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.autoDiscoverClusters=true
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Search for services in all clusters.
|
||||||
|
If set to true the configured clusters will be ignored and the clusters will be discovered.
|
||||||
|
If set to false the services will be discovered only in configured clusters.
|
||||||
|
|
||||||
|
### `exposedByDefault`
|
||||||
|
|
||||||
|
_Optional, Default=true_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
exposedByDefault = false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
exposedByDefault: false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.exposedByDefault=false
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Expose ECS services by default in Traefik.
|
||||||
|
If set to false, services that don't have a `traefik.enable=true` label will be ignored from the resulting routing configuration.
|
||||||
|
|
||||||
|
### `defaultRule`
|
||||||
|
|
||||||
|
_Optional, Default=```Host(`{{ normalize .Name }}`)```_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
defaultRule = "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
defaultRule: "Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.defaultRule=Host(`{{ .Name }}.{{ index .Labels \"customLabel\"}}`)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
For a given container if no routing rule was defined by a label, it is defined by this defaultRule instead.
|
||||||
|
It must be a valid [Go template](https://golang.org/pkg/text/template/),
|
||||||
|
augmented with the [sprig template functions](http://masterminds.github.io/sprig/).
|
||||||
|
The service name can be accessed as the `Name` identifier,
|
||||||
|
and the template has access to all the labels defined on this container.
|
||||||
|
|
||||||
|
### `refreshSeconds`
|
||||||
|
|
||||||
|
_Optional, Default=15_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
refreshSeconds = 15
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
refreshSeconds: 15
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.refreshSeconds=15
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Polling interval (in seconds).
|
||||||
|
|
||||||
|
### Credentials
|
||||||
|
|
||||||
|
_Optional_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
region = "us-east-1"
|
||||||
|
accessKeyID = "abc"
|
||||||
|
secretAccessKey = "123"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
region: us-east-1
|
||||||
|
accessKeyID: "abc"
|
||||||
|
secretAccessKey: "123"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.region="us-east-1"
|
||||||
|
--providers.ecs.accessKeyID="abc"
|
||||||
|
--providers.ecs.secretAccessKey="123"
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
If `accessKeyID` / `secretAccessKey` is not provided credentials will be resolved in the following order:
|
||||||
|
|
||||||
|
- From environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
|
||||||
|
- Shared credentials, determined by `AWS_PROFILE` and `AWS_SHARED_CREDENTIALS_FILE`, defaults to default and `~/.aws/credentials`.
|
||||||
|
- EC2 instance role or ECS task role
|
11
docs/content/reference/dynamic-configuration/ecs.md
Normal file
11
docs/content/reference/dynamic-configuration/ecs.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# ECS Configuration Reference
|
||||||
|
|
||||||
|
Dynamic configuration with ECS provider
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
The labels are case insensitive.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
--8<-- "content/reference/dynamic-configuration/ecs.yml"
|
||||||
|
--8<-- "content/reference/dynamic-configuration/docker-labels.yml"
|
||||||
|
```
|
1
docs/content/reference/dynamic-configuration/ecs.yml
Normal file
1
docs/content/reference/dynamic-configuration/ecs.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
- "traefik.enable=true"
|
|
@ -417,6 +417,33 @@ Use the ip address from the bound port, rather than from the inner network. (Def
|
||||||
`--providers.docker.watch`:
|
`--providers.docker.watch`:
|
||||||
Watch Docker Swarm events. (Default: ```true```)
|
Watch Docker Swarm events. (Default: ```true```)
|
||||||
|
|
||||||
|
`--providers.ecs.accesskeyid`:
|
||||||
|
The AWS credentials access key to use for making requests
|
||||||
|
|
||||||
|
`--providers.ecs.autodiscoverclusters`:
|
||||||
|
Auto discover cluster (Default: ```false```)
|
||||||
|
|
||||||
|
`--providers.ecs.clusters`:
|
||||||
|
ECS Clusters name (Default: ```default```)
|
||||||
|
|
||||||
|
`--providers.ecs.constraints`:
|
||||||
|
Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.
|
||||||
|
|
||||||
|
`--providers.ecs.defaultrule`:
|
||||||
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`--providers.ecs.exposedbydefault`:
|
||||||
|
Expose services by default (Default: ```true```)
|
||||||
|
|
||||||
|
`--providers.ecs.refreshseconds`:
|
||||||
|
Polling interval (in seconds) (Default: ```15```)
|
||||||
|
|
||||||
|
`--providers.ecs.region`:
|
||||||
|
The AWS region to use for requests
|
||||||
|
|
||||||
|
`--providers.ecs.secretaccesskey`:
|
||||||
|
The AWS credentials access key to use for making requests
|
||||||
|
|
||||||
`--providers.etcd`:
|
`--providers.etcd`:
|
||||||
Enable Etcd backend with default settings. (Default: ```false```)
|
Enable Etcd backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -417,6 +417,33 @@ Use the ip address from the bound port, rather than from the inner network. (Def
|
||||||
`TRAEFIK_PROVIDERS_DOCKER_WATCH`:
|
`TRAEFIK_PROVIDERS_DOCKER_WATCH`:
|
||||||
Watch Docker Swarm events. (Default: ```true```)
|
Watch Docker Swarm events. (Default: ```true```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_ACCESSKEYID`:
|
||||||
|
The AWS credentials access key to use for making requests
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_AUTODISCOVERCLUSTERS`:
|
||||||
|
Auto discover cluster (Default: ```false```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_CLUSTERS`:
|
||||||
|
ECS Clusters name (Default: ```default```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_CONSTRAINTS`:
|
||||||
|
Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container.
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_DEFAULTRULE`:
|
||||||
|
Default rule. (Default: ```Host(`{{ normalize .Name }}`)```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT`:
|
||||||
|
Expose services by default (Default: ```true```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_REFRESHSECONDS`:
|
||||||
|
Polling interval (in seconds) (Default: ```15```)
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_REGION`:
|
||||||
|
The AWS region to use for requests
|
||||||
|
|
||||||
|
`TRAEFIK_PROVIDERS_ECS_SECRETACCESSKEY`:
|
||||||
|
The AWS credentials access key to use for making requests
|
||||||
|
|
||||||
`TRAEFIK_PROVIDERS_ETCD`:
|
`TRAEFIK_PROVIDERS_ETCD`:
|
||||||
Enable Etcd backend with default settings. (Default: ```false```)
|
Enable Etcd backend with default settings. (Default: ```false```)
|
||||||
|
|
||||||
|
|
|
@ -151,6 +151,16 @@
|
||||||
[providers.consulCatalog.endpoint.httpAuth]
|
[providers.consulCatalog.endpoint.httpAuth]
|
||||||
username = "foobar"
|
username = "foobar"
|
||||||
password = "foobar"
|
password = "foobar"
|
||||||
|
[providers.ecs]
|
||||||
|
constraints = "foobar"
|
||||||
|
exposedByDefault = true
|
||||||
|
refreshSeconds = 42
|
||||||
|
defaultRule = "foobar"
|
||||||
|
clusters = ["foobar", "foobar"]
|
||||||
|
autoDiscoverClusters = true
|
||||||
|
region = "foobar"
|
||||||
|
accessKeyID = "foobar"
|
||||||
|
secretAccessKey = "foobar"
|
||||||
[providers.consul]
|
[providers.consul]
|
||||||
rootKey = "foobar"
|
rootKey = "foobar"
|
||||||
endpoints = ["foobar", "foobar"]
|
endpoints = ["foobar", "foobar"]
|
||||||
|
@ -233,6 +243,7 @@
|
||||||
[ping]
|
[ping]
|
||||||
entryPoint = "foobar"
|
entryPoint = "foobar"
|
||||||
manualRouting = true
|
manualRouting = true
|
||||||
|
terminatingStatusCode = 42
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "foobar"
|
level = "foobar"
|
||||||
|
|
|
@ -161,6 +161,18 @@ providers:
|
||||||
httpAuth:
|
httpAuth:
|
||||||
username: foobar
|
username: foobar
|
||||||
password: foobar
|
password: foobar
|
||||||
|
ecs:
|
||||||
|
constraints: foobar
|
||||||
|
exposedByDefault: true
|
||||||
|
refreshSeconds: 42
|
||||||
|
defaultRule: foobar
|
||||||
|
clusters:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
autoDiscoverClusters: true
|
||||||
|
region: foobar
|
||||||
|
accessKeyID: foobar
|
||||||
|
secretAccessKey: foobar
|
||||||
consul:
|
consul:
|
||||||
rootKey: foobar
|
rootKey: foobar
|
||||||
endpoints:
|
endpoints:
|
||||||
|
@ -250,6 +262,7 @@ metrics:
|
||||||
ping:
|
ping:
|
||||||
entryPoint: foobar
|
entryPoint: foobar
|
||||||
manualRouting: true
|
manualRouting: true
|
||||||
|
terminatingStatusCode: 42
|
||||||
log:
|
log:
|
||||||
level: foobar
|
level: foobar
|
||||||
filePath: foobar
|
filePath: foobar
|
||||||
|
|
445
docs/content/routing/providers/ecs.md
Normal file
445
docs/content/routing/providers/ecs.md
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
# Traefik & ECS
|
||||||
|
|
||||||
|
A Story of Labels & Elastic Containers
|
||||||
|
{: .subtitle }
|
||||||
|
|
||||||
|
Attach labels to your containers and let Traefik do the rest!
|
||||||
|
|
||||||
|
## Routing Configuration
|
||||||
|
|
||||||
|
!!! info "labels"
|
||||||
|
|
||||||
|
- labels are case insensitive.
|
||||||
|
- The complete list of labels can be found [the reference page](../../reference/dynamic-configuration/ecs.md)
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
Traefik creates, for each elastic service, a corresponding [service](../services/index.md) and [router](../routers/index.md).
|
||||||
|
|
||||||
|
The Service automatically gets a server per elastic container, and the router gets a default rule attached to it, based on the service name.
|
||||||
|
|
||||||
|
### Routers
|
||||||
|
|
||||||
|
To update the configuration of the Router automatically attached to the service, add labels starting with `traefik.routers.{name-of-your-choice}.` and followed by the option you want to change.
|
||||||
|
|
||||||
|
For example, to change the rule, you could add the label ```traefik.http.routers.my-service.rule=Host(`example.com`)```.
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the router name `<router_name>`."
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.rule`"
|
||||||
|
|
||||||
|
See [rule](../routers/index.md#rule) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.rule=Host(`example.com`)
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.entrypoints=web,websecure
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.middlewares`"
|
||||||
|
|
||||||
|
See [middlewares](../routers/index.md#middlewares) and [middlewares overview](../../middlewares/overview.md) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.middlewares=auth,prefix,cb
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [rule](../routers/index.md#service) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.service=myservice
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls`"
|
||||||
|
|
||||||
|
See [tls](../routers/index.md#tls) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter>.tls=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.certresolver`"
|
||||||
|
|
||||||
|
See [certResolver](../routers/index.md#certresolver) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.tls.certresolver=myresolver
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.domains[n].main`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.tls.domains[0].main=example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.domains[n].sans`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.tls.domains[0].sans=test.example.org,dev.example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.tls.options`"
|
||||||
|
|
||||||
|
See [options](../routers/index.md#options) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.tls.options=foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.routers.<router_name>.priority`"
|
||||||
|
|
||||||
|
See [priority](../routers/index.md#priority) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.routers.myrouter.priority=42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
|
To update the configuration of the Service automatically attached to the service,
|
||||||
|
add labels starting with `traefik.http.services.{name-of-your-choice}.`, followed by the option you want to change.
|
||||||
|
|
||||||
|
For example, to change the `passHostHeader` behavior,
|
||||||
|
you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.passhostheader=false`.
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the service name `<service_name>`."
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port.
|
||||||
|
Useful when the service exposes multiples ports.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.server.port=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.server.scheme`"
|
||||||
|
|
||||||
|
Overrides the default scheme.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.server.scheme=http
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.passhostheader`"
|
||||||
|
|
||||||
|
See [pass Host header](../services/index.md#pass-host-header) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.passhostheader=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.headers.<header_name>`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.headers.X-Foo=foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.hostname`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.hostname=example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.interval`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.interval=10
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.path`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.path=/foo
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.port`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.port=42
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.scheme`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.scheme=http
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.timeout`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.timeout=10
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.healthcheck.followredirects`"
|
||||||
|
|
||||||
|
See [health check](../services/index.md#health-check) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.healthcheck.followredirects=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.sticky=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.httponly`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.sticky.cookie.httponly=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.name`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.secure`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.sticky.cookie.secure=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.sticky.cookie.samesite`"
|
||||||
|
|
||||||
|
See [sticky sessions](../services/index.md#sticky-sessions) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.sticky.cookie.samesite=none
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.http.services.<service_name>.loadbalancer.responseforwarding.flushinterval`"
|
||||||
|
|
||||||
|
See [response forwarding](../services/index.md#response-forwarding) for more information.
|
||||||
|
|
||||||
|
FlushInterval specifies the flush interval to flush to the client while copying the response body.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.http.services.myservice.loadbalancer.responseforwarding.flushinterval=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
You can declare pieces of middleware using labels starting with `traefik.http.middlewares.{name-of-your-choice}.`, followed by the middleware type/options.
|
||||||
|
|
||||||
|
For example, to declare a middleware [`redirectscheme`](../../middlewares/redirectscheme.md) named `my-redirect`, you'd write `traefik.http.middlewares.my-redirect.redirectscheme.scheme: https`.
|
||||||
|
|
||||||
|
More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md).
|
||||||
|
|
||||||
|
!!! warning "The character `@` is not authorized in the middleware name."
|
||||||
|
|
||||||
|
??? example "Declaring and Referencing a Middleware"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ...
|
||||||
|
# Declaring a middleware
|
||||||
|
traefik.http.middlewares.my-redirect.redirectscheme.scheme=https
|
||||||
|
# Referencing a middleware
|
||||||
|
traefik.http.routers.my-service.middlewares=my-redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Conflicts in Declaration"
|
||||||
|
|
||||||
|
If you declare multiple middleware with the same name but with different parameters, the middleware fails to be declared.
|
||||||
|
|
||||||
|
### TCP
|
||||||
|
|
||||||
|
You can declare TCP Routers and/or Services using labels.
|
||||||
|
|
||||||
|
??? example "Declaring TCP Routers and Services"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.my-router.rule=HostSNI(`example.com`)
|
||||||
|
traefik.tcp.routers.my-router.tls=true
|
||||||
|
traefik.tcp.services.my-service.loadbalancer.server.port=4123
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "TCP and HTTP"
|
||||||
|
|
||||||
|
If you declare a TCP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no TCP Router/Service is defined).
|
||||||
|
You can declare both a TCP Router/Service and an HTTP Router/Service for the same elastic service (but you have to do so manually).
|
||||||
|
|
||||||
|
#### TCP Routers
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.entrypoints=ep1,ep2
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.rule`"
|
||||||
|
|
||||||
|
See [rule](../routers/index.md#rule_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.rule=HostSNI(`example.com`)
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [service](../routers/index.md#services) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.service=myservice
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls`"
|
||||||
|
|
||||||
|
See [TLS](../routers/index.md#tls_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls=true
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.certresolver`"
|
||||||
|
|
||||||
|
See [certResolver](../routers/index.md#certresolver_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls.certresolver=myresolver
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.domains[n].main`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls.domains[0].main=example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.domains[n].sans`"
|
||||||
|
|
||||||
|
See [domains](../routers/index.md#domains_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls.domains[0].sans=test.example.org,dev.example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.options`"
|
||||||
|
|
||||||
|
See [options](../routers/index.md#options_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls.options=mysoptions
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.routers.<router_name>.tls.passthrough`"
|
||||||
|
|
||||||
|
See [TLS](../routers/index.md#tls_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.routers.mytcprouter.tls.passthrough=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TCP Services
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port of the application.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.services.mytcpservice.loadbalancer.server.port=423
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.tcp.services.<service_name>.loadbalancer.terminationdelay`"
|
||||||
|
|
||||||
|
See [termination delay](../services/index.md#termination-delay) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.tcp.services.mytcpservice.loadbalancer.terminationdelay=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### UDP
|
||||||
|
|
||||||
|
You can declare UDP Routers and/or Services using tags.
|
||||||
|
|
||||||
|
??? example "Declaring UDP Routers and Services"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.udp.routers.my-router.entrypoints=udp
|
||||||
|
traefik.udp.services.my-service.loadbalancer.server.port=4123
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "UDP and HTTP"
|
||||||
|
|
||||||
|
If you declare a UDP Router/Service, it will prevent Traefik from automatically creating an HTTP Router/Service (like it does by default if no UDP Router/Service is defined).
|
||||||
|
You can declare both a UDP Router/Service and an HTTP Router/Service for the same elastic service (but you have to do so manually).
|
||||||
|
|
||||||
|
#### UDP Routers
|
||||||
|
|
||||||
|
??? info "`traefik.udp.routers.<router_name>.entrypoints`"
|
||||||
|
|
||||||
|
See [entry points](../routers/index.md#entrypoints_2) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.udp.routers.myudprouter.entrypoints=ep1,ep2
|
||||||
|
```
|
||||||
|
|
||||||
|
??? info "`traefik.udp.routers.<router_name>.service`"
|
||||||
|
|
||||||
|
See [service](../routers/index.md#services_1) for more information.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.udp.routers.myudprouter.service=myservice
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UDP Services
|
||||||
|
|
||||||
|
??? info "`traefik.udp.services.<service_name>.loadbalancer.server.port`"
|
||||||
|
|
||||||
|
Registers a port of the application.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.udp.services.myudpservice.loadbalancer.server.port=423
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Provider Options
|
||||||
|
|
||||||
|
#### `traefik.enable`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
traefik.enable=true
|
||||||
|
```
|
||||||
|
|
||||||
|
You can tell Traefik to consider (or not) the ECS service by setting `traefik.enable` to true or false.
|
||||||
|
|
||||||
|
This option overrides the value of `exposedByDefault`.
|
|
@ -77,6 +77,7 @@ nav:
|
||||||
- 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md'
|
- 'Kubernetes IngressRoute': 'providers/kubernetes-crd.md'
|
||||||
- 'Kubernetes Ingress': 'providers/kubernetes-ingress.md'
|
- 'Kubernetes Ingress': 'providers/kubernetes-ingress.md'
|
||||||
- 'Consul Catalog': 'providers/consul-catalog.md'
|
- 'Consul Catalog': 'providers/consul-catalog.md'
|
||||||
|
- 'ECS': 'providers/ecs.md'
|
||||||
- 'Marathon': 'providers/marathon.md'
|
- 'Marathon': 'providers/marathon.md'
|
||||||
- 'Rancher': 'providers/rancher.md'
|
- 'Rancher': 'providers/rancher.md'
|
||||||
- 'File': 'providers/file.md'
|
- 'File': 'providers/file.md'
|
||||||
|
@ -94,6 +95,7 @@ nav:
|
||||||
- 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md'
|
- 'Kubernetes IngressRoute': 'routing/providers/kubernetes-crd.md'
|
||||||
- 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md'
|
- 'Kubernetes Ingress': 'routing/providers/kubernetes-ingress.md'
|
||||||
- 'Consul Catalog': 'routing/providers/consul-catalog.md'
|
- 'Consul Catalog': 'routing/providers/consul-catalog.md'
|
||||||
|
- 'ECS': 'routing/providers/ecs.md'
|
||||||
- 'Marathon': 'routing/providers/marathon.md'
|
- 'Marathon': 'routing/providers/marathon.md'
|
||||||
- 'Rancher': 'routing/providers/rancher.md'
|
- 'Rancher': 'routing/providers/rancher.md'
|
||||||
- 'KV': 'routing/providers/kv.md'
|
- 'KV': 'routing/providers/kv.md'
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
||||||
github.com/VividCortex/gohistogram v1.0.0 // indirect
|
github.com/VividCortex/gohistogram v1.0.0 // indirect
|
||||||
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000
|
github.com/abbot/go-http-auth v0.0.0-00010101000000-000000000000
|
||||||
github.com/abronan/valkeyrie v0.0.0-20200127174252-ef4277a138cd
|
github.com/abronan/valkeyrie v0.0.0-20200127174252-ef4277a138cd
|
||||||
|
github.com/aws/aws-sdk-go v1.30.20
|
||||||
github.com/c0va23/go-proxyprotocol v0.9.1
|
github.com/c0va23/go-proxyprotocol v0.9.1
|
||||||
github.com/cenkalti/backoff/v4 v4.0.0
|
github.com/cenkalti/backoff/v4 v4.0.0
|
||||||
github.com/containerd/containerd v1.3.2 // indirect
|
github.com/containerd/containerd v1.3.2 // indirect
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
acmeprovider "github.com/containous/traefik/v2/pkg/provider/acme"
|
acmeprovider "github.com/containous/traefik/v2/pkg/provider/acme"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/consulcatalog"
|
"github.com/containous/traefik/v2/pkg/provider/consulcatalog"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/docker"
|
"github.com/containous/traefik/v2/pkg/provider/docker"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider/ecs"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/file"
|
"github.com/containous/traefik/v2/pkg/provider/file"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/kubernetes/crd"
|
"github.com/containous/traefik/v2/pkg/provider/kubernetes/crd"
|
||||||
"github.com/containous/traefik/v2/pkg/provider/kubernetes/ingress"
|
"github.com/containous/traefik/v2/pkg/provider/kubernetes/ingress"
|
||||||
|
@ -170,6 +171,7 @@ type Providers struct {
|
||||||
Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
||||||
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
||||||
ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"`
|
ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"`
|
||||||
|
Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty"`
|
||||||
|
|
||||||
Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
||||||
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
|
||||||
|
|
|
@ -49,6 +49,10 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator {
|
||||||
p.quietAddProvider(conf.Rancher)
|
p.quietAddProvider(conf.Rancher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.Ecs != nil {
|
||||||
|
p.quietAddProvider(conf.Ecs)
|
||||||
|
}
|
||||||
|
|
||||||
if conf.ConsulCatalog != nil {
|
if conf.ConsulCatalog != nil {
|
||||||
p.quietAddProvider(conf.ConsulCatalog)
|
p.quietAddProvider(conf.ConsulCatalog)
|
||||||
}
|
}
|
||||||
|
|
79
pkg/provider/ecs/builder_test.go
Normal file
79
pkg/provider/ecs/builder_test.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import "github.com/aws/aws-sdk-go/service/ecs"
|
||||||
|
|
||||||
|
func instance(ops ...func(*ecsInstance)) ecsInstance {
|
||||||
|
e := &ecsInstance{
|
||||||
|
containerDefinition: &ecs.ContainerDefinition{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
op(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *e
|
||||||
|
}
|
||||||
|
|
||||||
|
func name(name string) func(*ecsInstance) {
|
||||||
|
return func(e *ecsInstance) {
|
||||||
|
e.Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func id(id string) func(*ecsInstance) {
|
||||||
|
return func(e *ecsInstance) {
|
||||||
|
e.ID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iMachine(opts ...func(*machine)) func(*ecsInstance) {
|
||||||
|
return func(e *ecsInstance) {
|
||||||
|
e.machine = &machine{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(e.machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mState(state string) func(*machine) {
|
||||||
|
return func(m *machine) {
|
||||||
|
m.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mPrivateIP(ip string) func(*machine) {
|
||||||
|
return func(m *machine) {
|
||||||
|
m.privateIP = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mHealthStatus(status string) func(*machine) {
|
||||||
|
return func(m *machine) {
|
||||||
|
m.healthStatus = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mPorts(opts ...func(*portMapping)) func(*machine) {
|
||||||
|
return func(m *machine) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
p := &portMapping{}
|
||||||
|
opt(p)
|
||||||
|
m.ports = append(m.ports, *p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mPort(containerPort, hostPort int32, protocol string) func(*portMapping) {
|
||||||
|
return func(pm *portMapping) {
|
||||||
|
pm.containerPort = int64(containerPort)
|
||||||
|
pm.hostPort = int64(hostPort)
|
||||||
|
pm.protocol = protocol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func labels(labels map[string]string) func(*ecsInstance) {
|
||||||
|
return func(c *ecsInstance) {
|
||||||
|
c.Labels = labels
|
||||||
|
}
|
||||||
|
}
|
339
pkg/provider/ecs/config.go
Normal file
339
pkg/provider/ecs/config.go
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/label"
|
||||||
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider/constraints"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Provider) buildConfiguration(ctx context.Context, instances []ecsInstance) *dynamic.Configuration {
|
||||||
|
configurations := make(map[string]*dynamic.Configuration)
|
||||||
|
|
||||||
|
for _, instance := range instances {
|
||||||
|
instanceName := getServiceName(instance) + "-" + instance.ID
|
||||||
|
ctxContainer := log.With(ctx, log.Str("ecs-instance", instanceName))
|
||||||
|
|
||||||
|
if !p.filterInstance(ctxContainer, instance) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.FromContext(ctxContainer)
|
||||||
|
|
||||||
|
confFromLabel, err := label.DecodeConfiguration(instance.Labels)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcpOrUDP bool
|
||||||
|
if len(confFromLabel.TCP.Routers) > 0 || len(confFromLabel.TCP.Services) > 0 {
|
||||||
|
tcpOrUDP = true
|
||||||
|
|
||||||
|
err := p.buildTCPServiceConfiguration(instance, confFromLabel.TCP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider.BuildTCPRouterConfiguration(ctxContainer, confFromLabel.TCP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(confFromLabel.UDP.Routers) > 0 || len(confFromLabel.UDP.Services) > 0 {
|
||||||
|
tcpOrUDP = true
|
||||||
|
|
||||||
|
err := p.buildUDPServiceConfiguration(instance, confFromLabel.UDP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider.BuildUDPRouterConfiguration(ctxContainer, confFromLabel.UDP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpOrUDP && len(confFromLabel.HTTP.Routers) == 0 &&
|
||||||
|
len(confFromLabel.HTTP.Middlewares) == 0 &&
|
||||||
|
len(confFromLabel.HTTP.Services) == 0 {
|
||||||
|
configurations[instanceName] = confFromLabel
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.buildServiceConfiguration(ctxContainer, instance, confFromLabel.HTTP)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := getServiceName(instance)
|
||||||
|
|
||||||
|
model := struct {
|
||||||
|
Name string
|
||||||
|
Labels map[string]string
|
||||||
|
}{
|
||||||
|
Name: serviceName,
|
||||||
|
Labels: instance.Labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.BuildRouterConfiguration(ctx, confFromLabel.HTTP, serviceName, p.defaultRuleTpl, model)
|
||||||
|
|
||||||
|
configurations[instanceName] = confFromLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.Merge(ctx, configurations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) buildTCPServiceConfiguration(instance ecsInstance, configuration *dynamic.TCPConfiguration) error {
|
||||||
|
serviceName := getServiceName(instance)
|
||||||
|
|
||||||
|
if len(configuration.Services) == 0 {
|
||||||
|
configuration.Services = make(map[string]*dynamic.TCPService)
|
||||||
|
lb := &dynamic.TCPServersLoadBalancer{}
|
||||||
|
lb.SetDefaults()
|
||||||
|
configuration.Services[serviceName] = &dynamic.TCPService{
|
||||||
|
LoadBalancer: lb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, service := range configuration.Services {
|
||||||
|
err := p.addServerTCP(instance, service.LoadBalancer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %q error: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) buildUDPServiceConfiguration(instance ecsInstance, configuration *dynamic.UDPConfiguration) error {
|
||||||
|
serviceName := getServiceName(instance)
|
||||||
|
|
||||||
|
if len(configuration.Services) == 0 {
|
||||||
|
configuration.Services = make(map[string]*dynamic.UDPService)
|
||||||
|
lb := &dynamic.UDPServersLoadBalancer{}
|
||||||
|
configuration.Services[serviceName] = &dynamic.UDPService{
|
||||||
|
LoadBalancer: lb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, service := range configuration.Services {
|
||||||
|
err := p.addServerUDP(instance, service.LoadBalancer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %q error: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) buildServiceConfiguration(_ context.Context, instance ecsInstance, configuration *dynamic.HTTPConfiguration) error {
|
||||||
|
serviceName := getServiceName(instance)
|
||||||
|
|
||||||
|
if len(configuration.Services) == 0 {
|
||||||
|
configuration.Services = make(map[string]*dynamic.Service)
|
||||||
|
lb := &dynamic.ServersLoadBalancer{}
|
||||||
|
lb.SetDefaults()
|
||||||
|
configuration.Services[serviceName] = &dynamic.Service{
|
||||||
|
LoadBalancer: lb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, service := range configuration.Services {
|
||||||
|
err := p.addServer(instance, service.LoadBalancer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("service %q error: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) filterInstance(ctx context.Context, instance ecsInstance) bool {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
if instance.machine == nil {
|
||||||
|
logger.Debug("Filtering ecs instance with nil machine")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(instance.machine.state) != ec2.InstanceStateNameRunning {
|
||||||
|
logger.Debugf("Filtering ecs instance with an incorrect state %s (%s) (state = %s)", instance.Name, instance.ID, instance.machine.state)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if instance.machine.healthStatus == "UNHEALTHY" {
|
||||||
|
logger.Debugf("Filtering unhealthy ecs instance %s (%s)", instance.Name, instance.ID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instance.machine.privateIP) == 0 {
|
||||||
|
logger.Debugf("Filtering ecs instance without an ip address %s (%s)", instance.Name, instance.ID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !instance.ExtraConf.Enable {
|
||||||
|
logger.Debugf("Filtering disabled ecs instance %s (%s)", instance.Name, instance.ID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := constraints.MatchLabels(instance.Labels, p.Constraints)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error matching constraints expression: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !matches {
|
||||||
|
logger.Debugf("Container pruned by constraint expression: %q", p.Constraints)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) addServerTCP(instance ecsInstance, loadBalancer *dynamic.TCPServersLoadBalancer) error {
|
||||||
|
if loadBalancer == nil {
|
||||||
|
return errors.New("load-balancer is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverPort string
|
||||||
|
if len(loadBalancer.Servers) > 0 {
|
||||||
|
serverPort = loadBalancer.Servers[0].Port
|
||||||
|
loadBalancer.Servers[0].Port = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, port, err := p.getIPPort(instance, serverPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loadBalancer.Servers) == 0 {
|
||||||
|
server := dynamic.TCPServer{}
|
||||||
|
|
||||||
|
loadBalancer.Servers = []dynamic.TCPServer{server}
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == "" {
|
||||||
|
return errors.New("port is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBalancer.Servers[0].Address = net.JoinHostPort(ip, port)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) addServerUDP(instance ecsInstance, loadBalancer *dynamic.UDPServersLoadBalancer) error {
|
||||||
|
if loadBalancer == nil {
|
||||||
|
return errors.New("load-balancer is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverPort string
|
||||||
|
if len(loadBalancer.Servers) > 0 {
|
||||||
|
serverPort = loadBalancer.Servers[0].Port
|
||||||
|
loadBalancer.Servers[0].Port = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, port, err := p.getIPPort(instance, serverPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loadBalancer.Servers) == 0 {
|
||||||
|
server := dynamic.UDPServer{}
|
||||||
|
|
||||||
|
loadBalancer.Servers = []dynamic.UDPServer{server}
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == "" {
|
||||||
|
return errors.New("port is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBalancer.Servers[0].Address = net.JoinHostPort(ip, port)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) addServer(instance ecsInstance, loadBalancer *dynamic.ServersLoadBalancer) error {
|
||||||
|
if loadBalancer == nil {
|
||||||
|
return errors.New("load-balancer is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverPort string
|
||||||
|
if len(loadBalancer.Servers) > 0 {
|
||||||
|
serverPort = loadBalancer.Servers[0].Port
|
||||||
|
loadBalancer.Servers[0].Port = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, port, err := p.getIPPort(instance, serverPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loadBalancer.Servers) == 0 {
|
||||||
|
server := dynamic.Server{}
|
||||||
|
server.SetDefaults()
|
||||||
|
|
||||||
|
loadBalancer.Servers = []dynamic.Server{server}
|
||||||
|
}
|
||||||
|
|
||||||
|
if port == "" {
|
||||||
|
return errors.New("port is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBalancer.Servers[0].URL = fmt.Sprintf("%s://%s", loadBalancer.Servers[0].Scheme, net.JoinHostPort(ip, port))
|
||||||
|
loadBalancer.Servers[0].Scheme = ""
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) getIPPort(instance ecsInstance, serverPort string) (string, string, error) {
|
||||||
|
var ip, port string
|
||||||
|
|
||||||
|
ip = p.getIPAddress(instance)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports []nat.Port
|
||||||
|
for _, port := range instance.machine.ports {
|
||||||
|
natPort, err := nat.NewPort(port.protocol, strconv.FormatInt(port.hostPort, 10))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ports = append(ports, natPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
less := func(i, j nat.Port) bool {
|
||||||
|
return i.Int() < j.Int()
|
||||||
|
}
|
||||||
|
nat.Sort(ports, less)
|
||||||
|
|
||||||
|
if len(ports) > 0 {
|
||||||
|
min := ports[0]
|
||||||
|
return min.Port()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServiceName(instance ecsInstance) string {
|
||||||
|
return provider.Normalize(instance.Name)
|
||||||
|
}
|
2576
pkg/provider/ecs/config_test.go
Normal file
2576
pkg/provider/ecs/config_test.go
Normal file
File diff suppressed because it is too large
Load diff
469
pkg/provider/ecs/ecs.go
Normal file
469
pkg/provider/ecs/ecs.go
Normal file
|
@ -0,0 +1,469 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ec2"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ecs"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/dynamic"
|
||||||
|
"github.com/containous/traefik/v2/pkg/job"
|
||||||
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/provider"
|
||||||
|
"github.com/containous/traefik/v2/pkg/safe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider holds configurations of the provider.
|
||||||
|
type Provider struct {
|
||||||
|
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
|
||||||
|
ExposedByDefault bool `description:"Expose services by default" json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"`
|
||||||
|
RefreshSeconds int `description:"Polling interval (in seconds)" json:"refreshSeconds,omitempty" toml:"refreshSeconds,omitempty" yaml:"refreshSeconds,omitempty" export:"true"`
|
||||||
|
DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
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"`
|
||||||
|
SecretAccessKey string `description:"The AWS credentials access key to use for making requests" json:"secretAccessKey,omitempty" toml:"secretAccessKey,omitempty" yaml:"secretAccessKey,omitempty"`
|
||||||
|
defaultRuleTpl *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecsInstance struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
containerDefinition *ecs.ContainerDefinition
|
||||||
|
machine *machine
|
||||||
|
Labels map[string]string
|
||||||
|
ExtraConf configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
type portMapping struct {
|
||||||
|
containerPort int64
|
||||||
|
hostPort int64
|
||||||
|
protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
type machine struct {
|
||||||
|
state string
|
||||||
|
privateIP string
|
||||||
|
ports []portMapping
|
||||||
|
healthStatus string
|
||||||
|
}
|
||||||
|
|
||||||
|
type awsClient struct {
|
||||||
|
ecs *ecs.ECS
|
||||||
|
ec2 *ec2.EC2
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTemplateRule The default template for the default rule.
|
||||||
|
const DefaultTemplateRule = "Host(`{{ normalize .Name }}`)"
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ provider.Provider = (*Provider)(nil)
|
||||||
|
existingTaskDefCache = cache.New(30*time.Minute, 5*time.Minute)
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDefaults sets the default values.
|
||||||
|
func (p *Provider) SetDefaults() {
|
||||||
|
p.Clusters = []string{"default"}
|
||||||
|
p.AutoDiscoverClusters = false
|
||||||
|
p.ExposedByDefault = true
|
||||||
|
p.RefreshSeconds = 15
|
||||||
|
p.DefaultRule = DefaultTemplateRule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the provider.
|
||||||
|
func (p *Provider) Init() error {
|
||||||
|
defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while parsing default rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.defaultRuleTpl = defaultRuleTpl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) createClient(logger log.Logger) (*awsClient, error) {
|
||||||
|
sess, err := session.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ec2meta := ec2metadata.New(sess)
|
||||||
|
if p.Region == "" {
|
||||||
|
logger.Infoln("No EC2 region provided, querying instance metadata endpoint...")
|
||||||
|
identity, err := ec2meta.GetInstanceIdentityDocument()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.Region = identity.Region
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &aws.Config{
|
||||||
|
Region: &p.Region,
|
||||||
|
Credentials: credentials.NewChainCredentials(
|
||||||
|
[]credentials.Provider{
|
||||||
|
&credentials.StaticProvider{
|
||||||
|
Value: credentials.Value{
|
||||||
|
AccessKeyID: p.AccessKeyID,
|
||||||
|
SecretAccessKey: p.SecretAccessKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&credentials.EnvProvider{},
|
||||||
|
&credentials.SharedCredentialsProvider{},
|
||||||
|
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.WithLogger(aws.LoggerFunc(func(args ...interface{}) {
|
||||||
|
logger.Debug(args...)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return &awsClient{
|
||||||
|
ecs.New(sess, cfg),
|
||||||
|
ec2.New(sess, cfg),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide configuration to traefik from ECS.
|
||||||
|
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)
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
awsClient, err := p.createClient(logger)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration, err := p.loadECSConfig(ctxLog, awsClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: "ecs",
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
reload := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds))
|
||||||
|
defer reload.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-reload.C:
|
||||||
|
configuration, err := p.loadECSConfig(ctxLog, awsClient)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to load ECS configuration, error %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationChan <- dynamic.Message{
|
||||||
|
ProviderName: "ecs",
|
||||||
|
Configuration: configuration,
|
||||||
|
}
|
||||||
|
case <-routineCtx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := func(err error, time time.Duration) {
|
||||||
|
logger.Errorf("Provider connection error %+v, retrying in %s", err, time)
|
||||||
|
}
|
||||||
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backoff.WithContext(job.NewBackOff(backoff.NewExponentialBackOff()), routineCtx), notify)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Cannot connect to Provider api %+v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all running Provider tasks in a cluster, also collect the task definitions (for docker labels)
|
||||||
|
// and the EC2 instance data.
|
||||||
|
func (p *Provider) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var clustersArn []*string
|
||||||
|
var clusters []string
|
||||||
|
|
||||||
|
if p.AutoDiscoverClusters {
|
||||||
|
input := &ecs.ListClustersInput{}
|
||||||
|
for {
|
||||||
|
result, err := client.ecs.ListClusters(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result != nil {
|
||||||
|
clustersArn = append(clustersArn, result.ClusterArns...)
|
||||||
|
input.NextToken = result.NextToken
|
||||||
|
if result.NextToken == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, cArn := range clustersArn {
|
||||||
|
clusters = append(clusters, *cArn)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clusters = p.Clusters
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances []ecsInstance
|
||||||
|
|
||||||
|
logger.Debugf("ECS Clusters: %s", clusters)
|
||||||
|
for _, c := range clusters {
|
||||||
|
input := &ecs.ListTasksInput{
|
||||||
|
Cluster: &c,
|
||||||
|
DesiredStatus: aws.String(ecs.DesiredStatusRunning),
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := make(map[string]*ecs.Task)
|
||||||
|
err := client.ecs.ListTasksPagesWithContext(ctx, input, func(page *ecs.ListTasksOutput, lastPage bool) bool {
|
||||||
|
if len(page.TaskArns) > 0 {
|
||||||
|
resp, err := client.ecs.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
|
||||||
|
Tasks: page.TaskArns,
|
||||||
|
Cluster: &c,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to describe tasks for %v", page.TaskArns)
|
||||||
|
} else {
|
||||||
|
for _, t := range resp.Tasks {
|
||||||
|
if aws.StringValue(t.LastStatus) == ecs.DesiredStatusRunning {
|
||||||
|
tasks[aws.StringValue(t.TaskArn)] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !lastPage
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unable to list tasks")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to the next cluster if there are no tasks found on
|
||||||
|
// this cluster.
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ec2Instances, err := p.lookupEc2Instances(ctx, client, &c, tasks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDefinitions, err := p.lookupTaskDefinitions(ctx, client, tasks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, task := range tasks {
|
||||||
|
containerInstance := ec2Instances[aws.StringValue(task.ContainerInstanceArn)]
|
||||||
|
taskDef := taskDefinitions[key]
|
||||||
|
|
||||||
|
for _, container := range task.Containers {
|
||||||
|
var containerDefinition *ecs.ContainerDefinition
|
||||||
|
for _, def := range taskDef.ContainerDefinitions {
|
||||||
|
if aws.StringValue(container.Name) == aws.StringValue(def.Name) {
|
||||||
|
containerDefinition = def
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerDefinition == nil {
|
||||||
|
logger.Debugf("Unable to find container definition for %s", aws.StringValue(container.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var mach *machine
|
||||||
|
if len(task.Attachments) != 0 {
|
||||||
|
var ports []portMapping
|
||||||
|
for _, mapping := range containerDefinition.PortMappings {
|
||||||
|
if mapping != nil {
|
||||||
|
protocol := "TCP"
|
||||||
|
if aws.StringValue(mapping.Protocol) == "udp" {
|
||||||
|
protocol = "UDP"
|
||||||
|
}
|
||||||
|
|
||||||
|
ports = append(ports, portMapping{
|
||||||
|
hostPort: aws.Int64Value(mapping.HostPort),
|
||||||
|
containerPort: aws.Int64Value(mapping.ContainerPort),
|
||||||
|
protocol: protocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mach = &machine{
|
||||||
|
privateIP: aws.StringValue(container.NetworkInterfaces[0].PrivateIpv4Address),
|
||||||
|
ports: ports,
|
||||||
|
state: aws.StringValue(task.LastStatus),
|
||||||
|
healthStatus: aws.StringValue(task.HealthStatus),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if containerInstance == nil {
|
||||||
|
logger.Errorf("Unable to find container instance information for %s", aws.StringValue(container.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var ports []portMapping
|
||||||
|
for _, mapping := range container.NetworkBindings {
|
||||||
|
if mapping != nil {
|
||||||
|
ports = append(ports, portMapping{
|
||||||
|
hostPort: aws.Int64Value(mapping.HostPort),
|
||||||
|
containerPort: aws.Int64Value(mapping.ContainerPort),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mach = &machine{
|
||||||
|
privateIP: aws.StringValue(containerInstance.PrivateIpAddress),
|
||||||
|
ports: ports,
|
||||||
|
state: aws.StringValue(containerInstance.State.Name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := ecsInstance{
|
||||||
|
Name: fmt.Sprintf("%s-%s", strings.Replace(aws.StringValue(task.Group), ":", "-", 1), *container.Name),
|
||||||
|
ID: key[len(key)-12:],
|
||||||
|
containerDefinition: containerDefinition,
|
||||||
|
machine: mach,
|
||||||
|
Labels: aws.StringValueMap(containerDefinition.DockerLabels),
|
||||||
|
}
|
||||||
|
|
||||||
|
extraConf, err := p.getConfiguration(instance)
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(ctx).Errorf("Skip container %s: %w", getServiceName(instance), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instance.ExtraConf = extraConf
|
||||||
|
|
||||||
|
instances = append(instances, instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) loadECSConfig(ctx context.Context, client *awsClient) (*dynamic.Configuration, error) {
|
||||||
|
instances, err := p.listInstances(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.buildConfiguration(ctx, instances), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) lookupEc2Instances(ctx context.Context, client *awsClient, clusterName *string, ecsDatas map[string]*ecs.Task) (map[string]*ec2.Instance, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
instanceIds := make(map[string]string)
|
||||||
|
ec2Instances := make(map[string]*ec2.Instance)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
logger.Errorf("Unable to describe container instances: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range resp.ContainerInstances {
|
||||||
|
instanceIds[aws.StringValue(container.Ec2InstanceId)] = aws.StringValue(container.ContainerInstanceArn)
|
||||||
|
instanceArns = append(instanceArns, container.Ec2InstanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instanceArns) > 0 {
|
||||||
|
for _, ids := range p.chunkIDs(instanceArns) {
|
||||||
|
input := &ec2.DescribeInstancesInput{
|
||||||
|
InstanceIds: ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.ec2.DescribeInstancesPagesWithContext(ctx, input, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||||
|
if len(page.Reservations) > 0 {
|
||||||
|
for _, r := range page.Reservations {
|
||||||
|
for _, i := range r.Instances {
|
||||||
|
if i.InstanceId != nil {
|
||||||
|
ec2Instances[instanceIds[aws.StringValue(i.InstanceId)]] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !lastPage
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to describe instances: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ec2Instances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns map[string]*ecs.Task) (map[string]*ecs.TaskDefinition, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
taskDef := make(map[string]*ecs.TaskDefinition)
|
||||||
|
|
||||||
|
for arn, task := range taskDefArns {
|
||||||
|
if definition, ok := existingTaskDefCache.Get(arn); ok {
|
||||||
|
taskDef[arn] = definition.(*ecs.TaskDefinition)
|
||||||
|
logger.Debugf("Found cached task definition for %s. Skipping the call", arn)
|
||||||
|
} else {
|
||||||
|
resp, err := client.ecs.DescribeTaskDefinitionWithContext(ctx, &ecs.DescribeTaskDefinitionInput{
|
||||||
|
TaskDefinition: task.TaskDefinitionArn,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Unable to describe task definition: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
taskDef[arn] = resp.TaskDefinition
|
||||||
|
existingTaskDefCache.Set(arn, resp.TaskDefinition, cache.DefaultExpiration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return taskDef, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunkIDs ECS expects no more than 100 parameters be passed to a API call;
|
||||||
|
// thus, pack each string into an array capped at 100 elements.
|
||||||
|
func (p *Provider) chunkIDs(ids []*string) [][]*string {
|
||||||
|
var chuncked [][]*string
|
||||||
|
for i := 0; i < len(ids); i += 100 {
|
||||||
|
var sliceEnd int
|
||||||
|
if i+100 < len(ids) {
|
||||||
|
sliceEnd = i + 100
|
||||||
|
} else {
|
||||||
|
sliceEnd = len(ids)
|
||||||
|
}
|
||||||
|
chuncked = append(chuncked, ids[i:sliceEnd])
|
||||||
|
}
|
||||||
|
return chuncked
|
||||||
|
}
|
88
pkg/provider/ecs/ecs_test.go
Normal file
88
pkg/provider/ecs/ecs_test.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChunkIDs(t *testing.T) {
|
||||||
|
provider := &Provider{}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
count int
|
||||||
|
expected []int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "0 element",
|
||||||
|
count: 0,
|
||||||
|
expected: []int(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "1 element",
|
||||||
|
count: 1,
|
||||||
|
expected: []int{1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "99 elements, 1 chunk",
|
||||||
|
count: 99,
|
||||||
|
expected: []int{99},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "100 elements, 1 chunk",
|
||||||
|
count: 100,
|
||||||
|
expected: []int{100},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "101 elements, 2 chunks",
|
||||||
|
count: 101,
|
||||||
|
expected: []int{100, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "199 elements, 2 chunks",
|
||||||
|
count: 199,
|
||||||
|
expected: []int{100, 99},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "200 elements, 2 chunks",
|
||||||
|
count: 200,
|
||||||
|
expected: []int{100, 100},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "201 elements, 3 chunks",
|
||||||
|
count: 201,
|
||||||
|
expected: []int{100, 100, 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "555 elements, 5 chunks",
|
||||||
|
count: 555,
|
||||||
|
expected: []int{100, 100, 100, 100, 100, 55},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "1001 elements, 11 chunks",
|
||||||
|
count: 1001,
|
||||||
|
expected: []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var IDs []*string
|
||||||
|
for v := 0; v < test.count; v++ {
|
||||||
|
IDs = append(IDs, aws.String("a"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var outCount []int
|
||||||
|
for _, el := range provider.chunkIDs(IDs) {
|
||||||
|
outCount = append(outCount, len(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, outCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
23
pkg/provider/ecs/label.go
Normal file
23
pkg/provider/ecs/label.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containous/traefik/v2/pkg/config/label"
|
||||||
|
)
|
||||||
|
|
||||||
|
// configuration Contains information from the labels that are globals (not related to the dynamic configuration) or specific to the provider.
|
||||||
|
type configuration struct {
|
||||||
|
Enable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) getConfiguration(instance ecsInstance) (configuration, error) {
|
||||||
|
conf := configuration{
|
||||||
|
Enable: p.ExposedByDefault,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := label.Decode(instance.Labels, &conf, "traefik.ecs.", "traefik.enable")
|
||||||
|
if err != nil {
|
||||||
|
return configuration{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf, nil
|
||||||
|
}
|
36
webui/src/statics/providers/ecs.svg
Normal file
36
webui/src/statics/providers/ecs.svg
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="#fff"
|
||||||
|
fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<style><![CDATA[.B{stroke:none}.C{fill:#9d5025}.D{fill:#f58536}]]></style>
|
||||||
|
<circle cx="60" cy="60" r="60" fill="#FFFFFF" stroke="none"/>
|
||||||
|
<use xlink:href="#A" x="20" y="20"/>
|
||||||
|
<symbol id="A" overflow="visible">
|
||||||
|
<path
|
||||||
|
d="M4.282 5.345L0 7.627v64.746l4.282 2.282 16.87-33.306L4.282 5.345zM17.318 17.03l6.306-9.568 28.26 13.464-6.588 1.122L17.32 17.03zm-4.894 46.632l6.94 10.224 32.518-15.04L45.6 57.8l-33.176 5.862z"
|
||||||
|
class="B C"/>
|
||||||
|
<path d="M14.258 72.146l-9.976 2.51v-69.3l9.976 2.433v64.368z" class="B D"/>
|
||||||
|
<path
|
||||||
|
d="M9.012 2.8L14.26 0l9.882 44.488L14.26 80l-5.247-2.8V2.8zm36.235 54.94l6.635 1.097 6.07-18.115-6.07-19.805-6.635 1.122v35.7z"
|
||||||
|
class="B C"/>
|
||||||
|
<g class="D">
|
||||||
|
<path d="M24.47 76.912L14.26 80V0L24.47 3.1v73.8z" class="B"/>
|
||||||
|
<path d="M58.836 57.447L14.26 66.322V80l44.577-13.5v-9.064zm.082-35.097l-44.66-9.1V0l44.66 13.577v8.774z"
|
||||||
|
class="B"/>
|
||||||
|
<path d="M51.882 11.434l7.165 2.118v52.96l-7.165 2.105V11.434z" class="B"/>
|
||||||
|
</g>
|
||||||
|
<path d="M80 33.003l-19.177.97-6.776-.492 19.388-16.74L80 33.003z" fill="#6b3a19" class="B"/>
|
||||||
|
<path d="M54.047 33.482L73.435 32.3V16.74l-19.388 3.366v13.375z" class="B C"/>
|
||||||
|
<path d="M35.8 32.172l19.647-21.217 9.988 20.498L45.07 32.84l-9.27-.668z" fill="#6b3a19" class="B"/>
|
||||||
|
<g class="C">
|
||||||
|
<path
|
||||||
|
d="M35.8 32.172l19.647-1.8V10.955L35.8 15.884V32.17zm18.247 14.623l25.953.48-6.565 16.262-19.388-3.366V46.795z"
|
||||||
|
class="B"/>
|
||||||
|
<path d="M35.8 48.106l29.635.73-9.988 20.485L35.8 64.406v-16.3z" class="B"/>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M35.8 48.106l19.647 1.803 9.988-1.072-20.365-1.4-9.27.668zm44.2-.832l-19.177-.958-6.776.48 19.388 1.185L80 47.274z"
|
||||||
|
class="B" fill="#fbbf93"/>
|
||||||
|
<path
|
||||||
|
d="M73.435 32.3l6.565.693V18.846l-6.565-2.105V32.3zm-8-.847l-9.988-1.072V10.955l9.988 3.215v17.283zm8 16.527L80 47.274V61.43l-6.565 2.105V47.98zm-8 .857l-9.988 1.072v19.414l9.988-3.202V48.837z"
|
||||||
|
class="B D"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
Reference in a new issue