Gradual Migration from Traefik 1.x to 2.x
Guest post by Juan Carlos Mejías, Traefik Ambassador
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:
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.
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:
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:
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.