Blog

Gradual Migration from Traefik 1.x to 2.x

Guest post by Juan Carlos Mejías, Traefik Ambassador

Gradual Migration from Traefik 1.x to 2.x

Are you a happy Traefik user? Join the club! I use Traefik as a reverse proxy to manage the ingress of several dozen services in a Docker Swarm cluster, and couldn't be happier with it. Since its introduction in early 2015, Traefik has grown in maturity and popularity (don't take my word, look at the project's stargazers over time. When Traefik v2 was released I couldn't help but think about migrating, but I had one major concern: downtime.

Traefik's documentation explains how to migrate configurations from 1.x format to 2.x format however, as in any system with some degree of complexity, migrating is not just about changing configurations but managing them. You have to make sure everything keeps running smoothly and be prepared to rollback in case something goes wrong -have you heard of Murphy's Law? Also, you probably don't want to migrate the whole system at a time, or you could quickly find yourself trying to put out more fires than you can handle.

In this post, I will share a migration strategy that helped me move to Traefik 2 with very little downtime, one service at a time, with an easy way to rollback. For the sake of clarity and brevity, I will start from a single Traefik instance with two backend services and will keep everything in a single Docker Swarm stack. The same strategy could be used in a clustered Traefik deployment with many more backend services as well. In fact, this scenario is where Traefik shines the brightest.

Initial setup

Let's start from the following setup, with a Traefik 1 instance as a reverse proxy and two Nginx services, all running on Docker Swarm:

Initial setup. Traefik 1 handling all routing
Initial setup. Traefik 1 handling all routing

This configuration can be deployed to the swarm with the following stack definition:

# docker-compose.yaml

# Version >= 3.3 so configs are available
version: "3.4"

networks:
  traefik-public:
    external: true

configs:
  index1:
    # Contains string "1"
    file: ./index1.html
  index2:
    # Contains string "2"
    file: ./index2.html

services:
  traefik1:
    image: traefik:v1.7
    ports:
      - "80:80"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock
    command: >
      --docker
      --docker.swarmmode
      --entrypoints='Name:http Address::80'
    networks:
      - traefik-public

  web1:
    image: nginx:1-alpine
    deploy:
      labels:
        - traefik.enable=true
        - traefik.frontend.rule=Host:web1.docker.local
        - traefik.port=80
        - traefik.webservice.frontend.entryPoints=http
    configs:
      - source: index1
        target: /usr/share/nginx/html/index.html
    networks:
      - traefik-public

  web2:
    image: nginx:1-alpine
    deploy:
      labels:
        - traefik.enable=true
        - traefik.frontend.rule=Host:web2.docker.local
        - traefik.port=80
        - traefik.webservice.frontend.entryPoints=http
    configs:
      - source: index2
        target: /usr/share/nginx/html/index.html
    networks:
      - traefik-public

Files index1.html and index1.html contain strings 1 and 2 respectively:

echo 1 > index1.html
echo 2 > index2.html

With the above configuration, you can now create a Docker Swarm (if you don't already have one), an overlay network for Traefik and deploy the stack:

docker swarm init
docker network create --driver=overlay traefik-public
docker stack deploy -c docker-compose.yaml traefik

When the stack deployment finishes you will be able to query the defined Nginx services as web1.docker.local and web2.docker.local. In the example below I’m using curl:

curl -H Host:web1.docker.local http://127.0.0.1
# 1
curl -H Host:web1.docker.local http://127.0.0.1
# 2

It may take a few seconds to start the containers so if you get a 404 page not found response just wait and try again.

Traefik 2 with fallback to Traefik 1

You now have a working Traefik 1.x reverse proxy and two backend services. Let's migrate it to 2.x! Next you are going to add a Traefik 2 service which will run alongside and proxy requests to the existing one. Incoming requests will be routed to the Traefik 2 service and if no routes are matched they will then be routed to the Traefik 1 service.

Traefik 2 routing all requests to Traefik 1
Traefik 2 routing all requests to Traefik 1

Deploy these changes to the stack definition file:

 # docker-compose.yaml

 ...
 configs:
   ...
+  # Dynamic configuration for Traefik 2 (see below)
+  traefik2-providers:
+    file: ./traefik2-providers.yaml

 services:
   traefik1:
     image: traefik:v1.7
-    ports:
-      - "80:80"
   ...
+  traefik2:
+    image: traefik:v2.1
+    ports:
+      # The HTTP port
+      - "80:80"
+    volumes:
+      # So that Traefik can listen to the Docker events
+      - /var/run/docker.sock:/var/run/docker.sock
+    command: >
+      --providers.docker
+      --providers.docker.swarmMode
+      --providers.file.directory=/etc/traefik
+      --providers.file.filename=providers.yaml
+      --entryPoints.http.address=:80
+      --api.insecure
+    configs:
+      - source: traefik2-providers
+        target: /etc/traefik/providers.yaml
+    networks:
+      - traefik-public

The traefik2-providers.yaml file used in the traefik2-providers config directive for the traefik2 service defines a catch-all route that forwards unmatched requests to the traefik1 service:

# traefik2-providers.yaml

http:
  routers:
    # Define a catch-all router that forwards requests to legacy Traefik
    to-traefik1:
      # Catch all domains (regex matches all strings)
      # See https://github.com/google/re2/wiki/Syntax
      rule: "HostRegexp(`{domain:.+}`)"
      # If the rule matches, forward to the traefik1 service (see below)
      service: traefik1
      # Set the lowest priority, so this route is only used as a last resort
      priority: 1

  services:
    # Define how to reach legacy Traefik
    traefik1:
      loadBalancer:
        servers:
          # Legacy Traefik is part of the same stack so,
          # hostname defaults to service name
          - url: http://traefik1

Redeploy the stack and check everything is still working as expected:

docker stack deploy -c docker-compose.yaml traefik
# ...
curl -H Host:web1.docker.local http://127.0.0.1
# 1
curl -H Host:web2.docker.local http://127.0.0.1
# 2

Traefik 2 replacing Traefik 1

Next let's set up Traefik 2 to handle requests to web1, as in Image 3:

Traefik 2 handling web1 service's routing
Traefik 2 handling web1 service's routing

This setup can be achieved by updating web1 service labels to match Traefik 2 format as follows:

...
services:
  ...
  web1:
    ...
    deploy:
      labels:
        - traefik.enable=true
        - traefik.http.routers.web1.rule=Host(`web1.docker.local`)
        - traefik.http.services.web1.loadbalancer.server.port=80
    ...

Redeploy the stack and again check everything is still working as expected:

docker stack deploy -c docker-compose.yaml traefik
# ...
curl -H Host:web1.docker.local http://127.0.0.1
# 1
curl -H Host:web2.docker.local http://127.0.0.1
# 2

Now repeat the process for web2 service. If something goes wrong, you just need to revert to a previous working configuration for the affected service, redeploy, and start over. In a real-world scenario with lots of services, migration can take place one service at a time like this, reducing downtime.
When you finish migrating to Traefik 2, take down the Traefik 1 service. You will then end up with this scenario:

Traefik 2 handling all routing
Traefik 2 handling all routing

Wrapping Up

And that's it! You’ve successfully migrated from Traefik 1.x to 2.x one service at a time. This step by step migration strategy comes from the StranglerFigApplication pattern, as described by Martin Fowler. As a final note, I would highly recommend putting your configurations under version control as that would make it very easy to roll back changes when needed.

Author's Bio

Juan Carlos is a lecturer at the Informatics and Exact Sciences Faculty of the University of Camagüey, Cuba, and also DevOps engineer at the same institution. He has specialized on version control, continuous integration and deployment, Linux, and Docker containers. Since 2015 he has developed, deployed and monitored web applications for the University's IT infrastructure.