Understanding Traefik

Learning the ins and outs of Traefik, The Cloud Native Edge Router

Posted by Krystian Wojcicki on Tuesday, August 18, 2020 Tags: Tutorial   23 minute read

Introduction

Traefik is a cloud native reverse proxy, meaning it’s both a bodyguard and a guide to your backend. It intercepts incoming requests and routes them to the intended services according to rules set by you, potentially even modifying the requests. There are a multitude of benefits from using a reverse proxy:

  • Load Balancing: Traefik can distribute incoming traffic to maximize speed and utilization of your services
  • Request Acceleration: Traefik can compress incoming and outgoing data; in the future, Traefik may also support caching
  • Security: Traefik protects your backend service’s identity and acts as another layer of defence by acting as the gateway

Core definitions

Traefik revolves around three key concepts:

  • Entrypoints are essentially synonyms for a port and protocol tuple (i.e. 5000/udp), with [host]:port[/tcp|/udp] being its exact definition
  • Services forward the incoming request to your actual services
  • Routers serve as an intermediary between Entrypoints and Services. Routers have user defined rules that decide which Traefik service the incoming request should go to

Let’s return to our bodyguard and guide metaphor. Entrypoints are the heavily forfeited doors to our backend. Once a request has gone through the door it is handled by our bodyguards (Routers) which ensure the request should be here and find out where it wishes to go. The bodyguards then escort the request to our guides (Services) which handle delivery to our backend service.

Here’s a high level architecture diagram of Traefik:

Traefik architecture

An incoming request talks to an Entrypoint. Routers corresponding to that Entrypoint examine the request (Host, Path, Headers, Method) to determine which Service this request should be handed off to. The Service that receives the request is responsible for forwarding the request to its end destination.

Having these key components divided and independent of one another allows Traefik to be customized to every use case.

Simple Example

Let’s take a look at a simple example to better understand Traefik’s key concepts. Let’s imagine we have a simple service called whoami that we are looking to expose to the outer world. whoami simply returns information about the machine it is deployed on.

For this tutorial we will be using Docker and Docker-compose. We’ll create a docker-compose.yml file defining our Traefik and whoami service.

version: "3"

services:
  reverse-proxy:
    # The official v2 Traefik docker image
    image: traefik:v2.3
    ports:
      # The HTTP port
      - "80:80"
      # The Web UI (enabled by api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
      # So that we can configure Traefik
      - "./config.toml:/etc/traefik/traefik.toml"
  whoami:
    # A container that exposes an API to show its host IP address
    image: containous/whoami

Next, create a file in the same location as the docker-compose.yml called config.toml

[global]
  checkNewVersion = false
  sendAnonymousUsage = false

[log]
  level = "DEBUG"

[entryPoints] # Creating an entrypoint listening on port 80 with the default protocol of TCP
  [entryPoints.server]
    address = ":80"

[http.routers] # Creating a router which routes all requests matching Host == whoami.docker.localhost to the whoami-service
  [http.routers.my-router]
    rule = "Host(`whoami.docker.localhost`)"
    service = "whoami-service"

[http.services]  # Defining a service called whoami-service with its accompanying url
  [http.services.whoami-service.loadBalancer]
    [[http.services.whoami-service.loadBalancer.servers]]
      url = "http://whoami:80"

[api]
  insecure = true # Enables the web UI

[providers] # Providers will be looked at in the next section
  [providers.file]
    filename = "/etc/traefik/traefik.toml"

Start the containers using docker-compose up. Then in another terminal let’s curl our whoami service.

curl -H Host:whoami.docker.localhost http://127.0.0.1 returning

Hostname: 10a2a065a5a7
IP: 127.0.0.1
...

So what we’ve done is defined a TCP entrypoint on port 80. We’ve also created a http router that will examine the incoming request coming in on port 80 (specifically the Host header) and any requests with a Host header of whoami.docker.localhost will be routed to the whoami-service service. Next, we define our http service whoami-service which simply defines the URL of our docker container.

Basic Traefik example

All relatively simple and straightforward, albeit not particularly useful.

Providers: A deeper look into Traefik

Traefik’s configuration has two separate forms: static and dynamic. The static portion can be configured as part of a file, CLI arguments, or environment variables. The dynamic section can be fed to Traefik in a multitude of formats. The most time consuming is in the file alongside the static portion, but it’s also possible to use your favorite orchestrator (Docker, Kubernetes, Consul Catalog, …) or key value store (Redis, Zookeeper, etcd) to feed Traefik its dynamic configuration. Entrypoints and Providers are defined in the static portion, while Routers and Services are defined in the dynamic portion. Providers are existing infrastructure components (Docker, Kubernetes, Redis, Zookeeper, …) which can be queried by Traefik to automatically discover new Routers and Services.

Providers give Traefik lots of flexibility as Routers and Services can be defined after Traefik has already started which can be useful as your services go offline or get put on new machines.

We’ll be using the Docker provider for the rest of the tutorial.

Real Use Case

Now, let’s imagine a more realistic use case for Traefik where we have several microservices our clients wish to fetch data from. Our system represents a simplistic shopping website with services called order (with a URI of /order), account (with a URI of /api/v1/account) and inventory (with a URI of /api/v1/inventory). All services performing actions related to their names; We’ll also have an auth service.

# handles all requests related to orders. Requires authentication
order:
  image: containous/whoami

# handles all requests related to inventory
inventory:
  image: containous/whoami

# handles all requests related to accounts. Requires authentication
account:
  image: containous/whoami

# our auth service which all calls to account/order must go through
auth:
  image: lthummus/auththingie

Since any user is free to browse our inventory, those routes can be accessed without authorization. But, for our all order and account requests the user must be authenticated. We can accomplish this using Traefik’s middleware pattern.

Middlewares attach to a router and enable us to modify the request using Traefik’s pre-created middlewares.

The ForwardAuth middleware delegates user authentication to an external service (for us we will delegate to our auth service).

reverse-proxy:
  image: traefik:v2.3
  ...
  labels:
    - "traefik.enable=true"
    # defining a ForwardAuth middleware called test-auth, which forwards all authentication to the auth service
    - "traefik.http.middlewares.test-auth.forwardauth.address=http://reverse-proxy/auth"

Defining all middlewares under reverse-proxy is not necessary, and similar to variable definition the middlewares definition should correspond to its usage.

Now, let’s have the order service utilize our test-auth middleware

# handles all requests related to orders. Requires authentication
order:
  image: containous/whoami
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.order.entrypoints=server"
    - "traefik.http.routers.order.middlewares=test-auth" # specifying what middlewares to use
    - "traefik.http.routers.order.rule=Path(`/order`)"

Putting that all together we have a docker-compose.yml file:

version: "3"

services:
  reverse-proxy:
    image: traefik:v2.3
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - "./config.toml:/etc/traefik/traefik.toml"
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.test-auth.forwardauth.address=http://reverse-proxy/auth"

  # handles all requests related to orders. Requires authentication
  order:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.order.entrypoints=server"
      - "traefik.http.routers.order.middlewares=test-auth"
      - "traefik.http.routers.order.rule=Path(`/order`)" # the order service handles all requests with a path of /order

  # handles all requests related to inventory
  inventory:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.inventory.entrypoints=server"
      - "traefik.http.routers.inventory.rule=Path(`/api/v1/inventory`)" # the inventory service handles all requests with a path of /api/v1/inventory

  # handles all requests related to accounts. Requires authentication
  account:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.account.entrypoints=server"
      - "traefik.http.routers.account.middlewares=test-auth"
      - "traefik.http.routers.account.rule=Path(`/api/v1/account`)" # the account service handles all requests with a path of /api/v1/account

  # our auth service which all calls to account/order must go through
  auth:
    image: lthummus/auththingie
    ports:
      - "9000:9000"
    volumes:
      - ./authconfig.conf:/authconfig.conf
    environment:
      - AUTHTHINGIE_CONFIG_FILE_PATH=/authconfig.conf
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.auth.entrypoints=server"
      - "traefik.http.routers.auth.rule=Path(`/auth`)"

And a new updated config.toml file:

[global]
  checkNewVersion = false
  sendAnonymousUsage = false

[log]
  level = "DEBUG"

[entryPoints]
  [entryPoints.server]
    address = ":80"

    [entryPoints.server.forwardedHeaders]
      insecure = true # necessary for the auth service we are using. Not to be used in production

[api]
  insecure = true # Enables the web UI

[providers.docker]
  exposedByDefault = false

As well as a authconfig.conf for configuring our auth service.

auththingie {
  domain: reverse-proxy
  secretKey: "test"
  rules: [
    {
      "name": "/*",
      "pathPattern": "/*",
      "hostPattern": "reverse-proxy",
      "public": true,
      "permittedRoles": []
    }
  ]

  users: [
    {
      "htpasswdLine": "test:$2y$05$WgJrjE0ao3gbysOcE1F6.utXniEudCDEQ5EABjkBOVHze5iM/rFu2",
      "admin": true,
      "roles": [],
    }
  ]

  authSiteUrl = "http://127.0.0.1:9000"
}

Now, let’s restart our solution with Ctrl + C and docker-compose up.

Next, let’s get some orders with curl http://127.0.0.1/order which will return an empty respond. Why? Because we are not authenticated and Traefik did not forward our request to the backend. Let’s now authenticate by passing an Authorization header (corresponding to the htpasswdLine we set in the authconfig.conf but that’s not too important).

curl http://127.0.0.1/order -H "Authorization: Basic dGVzdDp0ZXN0"

Hostname: a9b412305755
IP: 127.0.0.1
...

Our auth service verifies the Authorization token we sent it (you can verify the authentication process by sending a different Authorization token and seeing the request fail) then proceeds to forward the request to the actual order service.

We can test out our account service in a similar fashion

~/workspace# curl http://127.0.0.1/api/v1/account -H "Authorization: Basic dGVzdDp0ZXN0"
Hostname: 8c8415d0c974
IP: 127.0.0.1
...

~/workspace# curl http://127.0.0.1/api/v1/account -H "Authorization: Basic dGVzdDp0ZXM=" -v
...
>
< HTTP/1.1 401 Unauthorized

But we can access our inventory without any need for authentication.

~/workspace# curl http://127.0.0.1/api/v1/inventory
Hostname: 570d161310c2
IP: 127.0.0.1
...

Real Use Case Traefik

Advanced Middleware Usage

You may have noticed in the previous example the order service responded to /order requests. While account and inventory had a prefix of /api/v1/ in their paths. Let’s pretend that the team reworked the order service to have two APIs: the old one is available at /api/v1/order, and a new one is available at /api/v2/order. We would like to remedy this situation without having to discontinue any clients that continue to use the /order endpoint.

We’ll utilize the Chain and AddPrefix middlewares to accomplish this.

# handles all requests related to orders. Requires authentication
order:
  image: containous/whoami
  labels:
    - "traefik.enable=true"
    - "traefik.http.routers.order_deprecated.entrypoints=server"
      # our order service has gone through a complete rehaul, resulting in an /api/v1/order and /api/v2/order. However originally all clients simply used /order so we can add /api/v1 path prefix to any request that requests /order.
    - "traefik.http.routers.order_deprecated.middlewares=deprecated-clients"
    - "traefik.http.routers.order_deprecated.rule=Path(`/order`)"
    - "traefik.http.routers.order.entrypoints=server"
    - "traefik.http.routers.order.middlewares=test-auth"
    - "traefik.http.routers.order.rule=Path(`/api/v2/order`) || Path(`/api/v1/order`)"
    - "traefik.http.middlewares.deprecated-clients.chain.middlewares=add-version,test-auth"
    - "traefik.http.middlewares.add-version.addprefix.prefix=/api/v1"

We’ll utilize the same authconfig.conf and config.toml as before.

authconfig.conf
auththingie {
  domain: reverse-proxy
  secretKey: "test"
  rules: [
    {
      "name": "/*",
      "pathPattern": "/*",
      "hostPattern": "reverse-proxy",
      "public": true,
      "permittedRoles": []
    }
  ]

users: [
{
"htpasswdLine": "test:$2y$05\$WgJrjE0ao3gbysOcE1F6.utXniEudCDEQ5EABjkBOVHze5iM/rFu2",
"admin": true,
"roles": [],
}
]

authSiteUrl = "http://127.0.0.1:9000"
}

config.toml
[global]
checkNewVersion = false
sendAnonymousUsage = false

[log]
level = "DEBUG"

[entryPoints]
[entryPoints.server]
  address = ":80"

  [entryPoints.server.forwardedHeaders]
    insecure = true # necessary for the auth service we are using. Not to be used in production

[api]
insecure = true # Enables the web UI

[providers.docker]
exposedByDefault = false

With an updated docker-compose.yml.

docker-compose.yml
version: "3"

services:
  reverse-proxy:
    image: traefik:v2.3
    ports:
      - "80:80"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - "./config.toml:/etc/traefik/traefik.toml"
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.test-auth.forwardauth.address=http://reverse-proxy/auth"

  # handles all requests related to orders. Requires authentication
  order:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.order_deprecated.entrypoints=server"
        # our order service has gone through a complete rehaul, resulting in an /api/v1/order and /api/v2/order. However originally all clients simply used /order so we can add /api/v1 path prefix to any request that requests /order.
      - "traefik.http.routers.order_deprecated.middlewares=deprecated-clients"
      - "traefik.http.routers.order_deprecated.rule=Path(`/order`)"
      - "traefik.http.routers.order.entrypoints=server"
      - "traefik.http.routers.order.middlewares=test-auth"
      - "traefik.http.routers.order.rule=Path(`/api/v2/order`) || Path(`/api/v1/order`)"
      - "traefik.http.middlewares.deprecated-clients.chain.middlewares=add-version,test-auth"
      - "traefik.http.middlewares.add-version.addprefix.prefix=/api/v1"

  # handles all requests related to inventory
  inventory:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.inventory.entrypoints=server"
      - "traefik.http.routers.inventory.rule=Path(`/api/v1/inventory`)" # the inventory service handles all requests with a path of /api/v1/inventory

  # handles all requests related to accounts. Requires authentication
  account:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.account.entrypoints=server"
      - "traefik.http.routers.account.middlewares=test-auth"
      - "traefik.http.routers.account.rule=Path(`/api/v1/account`)" # the account service handles all requests with a path of /api/v1/account

  # our auth service which all calls to account/order must go through
  auth:
    image: lthummus/auththingie
    ports:
      - "9000:9000"
    volumes:
      - ./authconfig.conf:/authconfig.conf
    environment:
      - AUTHTHINGIE_CONFIG_FILE_PATH=/authconfig.conf
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.auth.entrypoints=server"
      - "traefik.http.routers.auth.rule=Path(`/auth`)"

After restaritng our solution with Ctrl + C and docker-compose up. Clients can now request /api/v1/order, /api/v2/order and /order.


~/workspace# curl http://127.0.0.1/order -H "Authorization: Basic dGVzdDp0ZXN0"
Hostname: 570d161310c2
IP: 127.0.0.1
...
~/workspace# curl http://127.0.0.1/api/v1/order -H "Authorization: Basic dGVzdDp0ZXN0"
Hostname: 570d161310c2
IP: 127.0.0.1
...
~/workspace# curl http://127.0.0.1/api/v2/order -H "Authorization: Basic dGVzdDp0ZXN0"
Hostname: 570d161310c2
IP: 127.0.0.1
...

Advanced Use Case Traefik

UDP

Let’s also expand our services to include a track service which sends minute by minute GPS data of ones purchase. Since this is minutely data it’s okay if a packet gets lost here and there, so we’ll use UDP for this service.

# handles sending minutely data about a purchase
track:
  image: containous/whoamiudp
  labels:
    - "traefik.enable=true"
    - "traefik.udp.routers.track.entrypoints=server-udp"

We’ll also have to create a new entrypoint that listens for UDP.

[entryPoints]
  ...
  [entryPoints.server-udp]
  address = ":81/udp"

And expose port 81 on Traefik’s container

reverse-proxy:
  image: traefik
  ports:
    - "80:80"
    - "81:81/udp"
    - "8080:8080"
  ...
docker-compose.yml
version: "3"

services:
  reverse-proxy:
    image: traefik:v2.3
    ports:
      - "80:80"
      - "81:81/udp"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - "./config.toml:/etc/traefik/traefik.toml"
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.test-auth.forwardauth.address=http://reverse-proxy/auth"

  # handles all requests related to orders. Requires authentication
  order:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.order_deprecated.entrypoints=server"
        # our order service has gone through a complete rehaul, resulting in an /api/v1/order and /api/v2/order. However originally all clients simply used /order so we can add /api/v1 path prefix to any request that requests /order.
      - "traefik.http.routers.order_deprecated.middlewares=deprecated-clients"
      - "traefik.http.routers.order_deprecated.rule=Path(`/order`)"
      - "traefik.http.routers.order.entrypoints=server"
      - "traefik.http.routers.order.middlewares=test-auth"
      - "traefik.http.routers.order.rule=Path(`/api/v2/order`) || Path(`/api/v1/order`)"
      - "traefik.http.middlewares.deprecated-clients.chain.middlewares=add-version,test-auth"
      - "traefik.http.middlewares.add-version.addprefix.prefix=/api/v1"

  # handles all requests related to inventory
  inventory:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.inventory.entrypoints=server"
      - "traefik.http.routers.inventory.rule=Path(`/api/v1/inventory`)" # the inventory service handles all requests with a path of /api/v1/inventory

  # handles all requests related to accounts. Requires authentication
  account:
    image: containous/whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.account.entrypoints=server"
      - "traefik.http.routers.account.middlewares=test-auth"
      - "traefik.http.routers.account.rule=Path(`/api/v1/account`)" # the account service handles all requests with a path of /api/v1/account

  # our auth service which all calls to account/order must go through
  auth:
    image: lthummus/auththingie
    ports:
      - "9000:9000"
    volumes:
      - ./authconfig.conf:/authconfig.conf
    environment:
      - AUTHTHINGIE_CONFIG_FILE_PATH=/authconfig.conf
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.auth.entrypoints=server"
      - "traefik.http.routers.auth.rule=Path(`/auth`)"

  # handles sending minutely data about a purchase
  track:
    image: containous/whoamiudp
    labels:
      - "traefik.enable=true"
      - "traefik.udp.routers.track.entrypoints=server-udp"
config.toml
[global]
  checkNewVersion = false
  sendAnonymousUsage = false

[log]
  level = "DEBUG"

[entryPoints]
  [entryPoints.server]
    address = ":80"

    [entryPoints.server.forwardedHeaders]
      insecure = true # necessary for the auth service we are using. Not to be used in production

  [entryPoints.server-udp]
    address = ":81/udp"

[api]
  insecure = true # Enables the web UI

[providers.docker]
  exposedByDefault = false
authconfig.conf
auththingie {
  domain: reverse-proxy
  secretKey: "test"
  rules: [
    {
      "name": "/*",
      "pathPattern": "/*",
      "hostPattern": "reverse-proxy",
      "public": true,
      "permittedRoles": []
    }
  ]

  users: [
    {
      "htpasswdLine": "test:$2y$05$WgJrjE0ao3gbysOcE1F6.utXniEudCDEQ5EABjkBOVHze5iM/rFu2",
      "admin": true,
      "roles": [],
    }
  ]

  authSiteUrl = "http://127.0.0.1:9000"
}

First let’s restart our solution with Ctrl + C and docker-compose up. Next, using nc (Netcat) we can confirm the ability to connect to our new track service.

~/workspace# nc 127.0.0.1 81 -v -u
WHO
Connection to 127.0.0.1 81 port [udp/*] succeeded!
Received: XReceived: XReceived: XReceived: XReceived: XHostname: 784d3e419126
IP: 127.0.0.1
IP: 172.29.0.3

UDP Traefik Usecase


With this you’ve covered the majority there is to know on Traefik. The key is to fully understand Providers, Entrypoints, Routers and Services then the rest is just fluff for specific use cases.