Traefik est un reverse proxy / load balancer qui supporte de nombreux backends (Docker, Swarm mode, Kubernetes, Marathon, et plus). GKE (Google Kubernetes Engine)  est le service Kubernetes managé par Google.

Dans cet article je vais vous montrer comment utiliser Traefik comme reverse proxy pour vos applications hébergées sur GKE. Nous l’utiliserons également pour répartir la charge entre les pods et pour sécuriser le trafic en https grâce à des certificats TLS générés avec Let’s Encrypt.

Cet article est un copié/collé assumé de l'article paru sur le blog de Zenika.

Les images des conteneurs sont disponibles sur Docker Hub :

Les sources sont sur GitHub : https://github.com/Zenika/traefik-gke-demo/tree/1.0.0

Architecture type d'un reverse proxy ou ingress chez Kubernetes

Préparation

Pour commencer, je prépare mon environnement pour créer le cluster dans la zone europe-west-1-c sur mon projet :

gcloud config set project [votre projet]
gcloud config set compute/region europe-west1
gcloud config set compute/zone europe-west1-c

Les fichiers de configuration présentés dans cet article sont appliqués sur le cluster via la commande suivante :

kubectl apply -f nom_du_fichier.yml

Création du cluster kubernetes

Je vais créer un cluster Kubernetes en version 1.10 et composé de trois noeuds. J’utilise des instances préemptibles pour faire diminuer la facture (pour plus de détails, je vous invite à consulter cet article).

Création du cluster :

gcloud container clusters create traefik-cluster --disk-size 10 --machine-type n1-highcpu-2 --num-nodes 3 --preemptible

On peut vérifier que GKE a créé un cluster ainsi que trois machines virtuelles :

MBP-de-admin:~ admin$ gcloud compute instances list
NAME                                            ZONE            MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
gke-traefik-cluster-default-pool-ccb967df-5cph  europe-west1-c  n1-highcpu-2  true         10.132.0.2   35.205.202.228  RUNNING
gke-traefik-cluster-default-pool-ccb967df-5vvj  europe-west1-c  n1-highcpu-2  true         10.132.0.4   35.190.222.236  RUNNING
gke-traefik-cluster-default-pool-ccb967df-dx6m  europe-west1-c  n1-highcpu-2  true         10.132.0.3   35.205.42.174   RUNNING

Ensuite je configure la commande kubectl pour qu’elle se connecte au cluster et je vérifie que l’accès au cluster fonctionne :

MBP-de-admin:~ admin$ gcloud container clusters get-credentials traefik-cluster
Fetching cluster endpoint and auth data.
kubeconfig entry generated for traefik-cluster.
MBP-de-admin:~ admin$ kubectl cluster-info
Kubernetes master is running at https://35.241.156.152
GLBCDefaultBackend is running at https://35.241.156.152/api/v1/namespaces/kube-system/services/default-http-backend:http/proxy
Heapster is running at https://35.241.156.152/api/v1/namespaces/kube-system/services/heapster/proxy
KubeDNS is running at https://35.241.156.152/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://35.241.156.152/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy

Création du service back

J’ajoute un déploiement pour le service back. Deux instances de ce dernier seront déployées, elles écoutent sur le port 3000.

01_back_deployment.yml :

kind: Deployment
apiVersion: apps/v1beta1
metadata:
  name: back-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: traefik-gke-demo
      tier: backend
  template:
    metadata:
      labels:
        app: traefik-gke-demo
        tier: backend
    spec:
      containers:
        - name: back
          image: "vpoilvert/traefik-gke-demo-back:1.0.0"
          ports:
            - containerPort: 3000

Ensuite j’expose les pods créés avec un service. Ce dernier est de type NodePort, c’est nécessaire pour fonctionner avec Traefik.

02_back_service.yml :

kind: Service
apiVersion: v1
metadata:
  name: back-service
spec:
  selector:
    app: traefik-gke-demo
    tier: backend
  ports:
  - port: 3000
  type: NodePort

Création du service front

Maintenant je vais déployer le service front sur le cluster. Cette fois je déploie une seule copie du conteneur qui répond sur le port 80.

03_front_deployment.yml :

kind: Deployment
apiVersion: apps/v1beta1
metadata:
  name: front-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traefik-gke-demo
      tier: frontend
  template:
    metadata:
      labels:
        app: traefik-gke-demo
        tier: frontend
    spec:
      containers:
        - name: front
          image: "vpoilvert/traefik-gke-demo-front:1.0.0"

Je déclare le service associé, encore une fois de type NodePort.

04_front_service.yml :

kind: Service
apiVersion: v1
metadata:
  name: front-service
spec:
  selector:
    app: traefik-gke-demo
    tier: frontend
  ports:
  - port: 80
    targetPort: 80
  type: NodePort

Création des autorisations pour Traefik

Depuis la version 1.6 de Kubernetes, RBAC (Role-Based Access Control) est activé par défaut sur GKE. Lorsque RBAC est activé, il faut donner l’autorisation à Traefik d’accéder aux API Kubernetes pour mettre à jour dynamiquement les règles de routage.

Pour commencer, je dois me donner le droit de créer des nouveaux rôles dans le cluster :

kubectl create clusterrolebinding cluster-admin-binding \
--clusterrole cluster-admin \
--user $(gcloud config get-value account)

Ensuite, je crée un nouveau rôle avec les droits dont à besoin Traefik et j’affecte ce rôle à un compte de service.

05_traefik_cluster_role.yml :

# create Traefik cluster role
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-cluster-role
rules:
  - apiGroups:
      - ""
    resources:
      - services
      - endpoints
      - secrets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - extensions
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
---
# create Traefik service account
kind: ServiceAccount
apiVersion: v1
metadata:
  name: traefik-service-account
  namespace: default
---
# bind role with service account
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: traefik-cluster-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: traefik-cluster-role
subjects:
- kind: ServiceAccount
  name: traefik-service-account
  namespace: default

Configuration de Traefik

Je vais déployer le fichier de configuration Traefik en utilisant une ConfigMap qui sera montée en lecture seule via un volume. Ce fichier de configuration active le https pour le domaine traefik-gke-demo.gabrielsagnard.fr grâce à Let’s Encrypt,  ainsi que la redirection http vers https.

06_traefik_config.yml :

kind: ConfigMap
apiVersion: v1
metadata:
  name: traefik-config
data:
  traefik.toml: |
    # traefik.toml
    defaultEntryPoints = ["http","https"]
    [entryPoints]
      [entryPoints.http]
      address = ":80"
        [entryPoints.http.redirect]
        entryPoint = "https"
      [entryPoints.https]
      address = ":443"
        [entryPoints.https.tls]
    [acme]
    email = "votreadressemail"
    storage = "/etc/traefik/acme/acme.json"
    entryPoint = "https"
    [acme.httpChallenge]
      entryPoint = "http"
    [[acme.domains]]
    main = "traefik-gke-demo.gabrielsagnard.fr"

Remarque : Pour que la génération du certificat https fonctionne, le domaine listé doit exister et répondre sur le port 80 (l’obtention de l’adresse ip du service est faite plus bas).

Sauvegarde acme.json

La configuration ci-dessus spécifie que le fichier de gestion des certificats générés par Let’s Encrypt doit être enregistré dans /etc/traefik/acme/acme.json. Or je veux conserver ce fichier lorsque le pod est recréé, pour cela je vais le sauvegarder sur un disque persistent.

La création du disque se fait avec la commande suivante :

gcloud compute disks create demo-acme --size 10GB --type pd-ssd

Ensuite, je crée une machine virtuelle temporaire pour y attacher le disque afin de le formater et d’y créer le fichier acme.json vide avec les bonnes permissions (nécessaire pour le lancement de Traefik).

Création de la machine virtuelle et ajout du disque :

$ gcloud compute instances create demo-acme-inst
$ gcloud compute instances attach-disk demo-acme-inst --disk demo-acme

Connexion à la VM, formatage du disque, création du fichier acme.json :

$ gcloud compute ssh demo-acme-inst
$ sudo mkdir /mnt/demo-acme
$ sudo mkfs.ext4 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/disk/by-id/scsi-0Google_PersistentDisk_persistent-disk-1
$ sudo mount -o discard,defaults /dev/disk/by-id/scsi-0Google_PersistentDisk_persistent-disk-1 /mnt/demo-acme
$ sudo touch /mnt/demo-acme/acme.json
$ sudo chmod 600 /mnt/demo-acme/acme.json
$ sudo umount /mnt/demo-acme
$ exit

Une fois ces opérations terminées je détache le disque et supprime la machine virtuelle :

$ gcloud compute instances detach-disk demo-acme-inst --disk demo-acme
$ gcloud compute instances delete demo-acme-inst

Remarque : Le disque créé ne peut être monté que par une machine à la fois en écriture. Si vous voulez utiliser plus d’une instance de Traefik pour faire de la haute disponibilité, les certificats doivent être stockés dans un Key Value Store  comme Consul : https://docs.traefik.io/user-guide/cluster/.

Création d’une ip statique

Avant de déployer Traefik je vais créer une adresse ip statique à lui assigner. La seconde commande affiche l’ip qui a été générée et qui devra être mise dans le fichier de service.

$ gcloud compute addresses create test-ip --region europe-west1
$ gcloud compute addresses describe test-ip --region europe-west1

Déploiement de Traefik

Je crée un déploiement pour Traefik qui utilise le compte de service créé précédemment. Le fichier de configuration traefik.toml et le disque persistent sont montés via des volumes.

07_traefik_deployment.yml :

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: traefik-deployment
  labels:
    app: traefik-gke-demo
    tier: reverse-proxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: traefik-gke-demo
      tier: reverse-proxy
  template:
    metadata:
      labels:
        app: traefik-gke-demo
        tier: reverse-proxy
    spec:
      serviceAccountName: traefik-service-account
      terminationGracePeriodSeconds: 60
      volumes:
        - name: config
          configMap:
            name: traefik-config
        - name: demo-acme
          gcePersistentDisk:
            pdName: demo-acme
            fsType: ext4
      containers:
      - name: traefik
        image: "traefik:1.6"
        volumeMounts:
          - mountPath: "/etc/traefik/config"
            name: config
          - mountPath: /etc/traefik/acme
            name: demo-acme
        args:
        - --configfile=/etc/traefik/config/traefik.toml
        - --api
        - --kubernetes

Je crée également un service de type LoadBalancer pour exposer Traefik vers l’extérieur via l’ip statique que j’ai créé. Ce service expose également l’interface d’administration de Traefik sur le port 8080.

08_traefik_service.yml :

kind: Service
apiVersion: v1
metadata:
  name: traefik-service
spec:
  selector:
    app: traefik-gke-demo
    tier: reverse-proxy
  ports:
    - port: 80
      name: http
    - port: 443
      name: https
    - port: 8080
      name: admin
  type: LoadBalancer
  loadBalancerIP: "your static ip"

Attention : Si le nom de domaine (ici traefik-gke-demo.gabrielsagnard.fr) n’existe pas lors du déploiement de Traefik ou qu’il ne pointe pas sur la bonne ip, la génération du certificat https échouera. Si cela arrive, il faut supprimer le pod et une nouvelle génération sera effectuée lors de la création d’un nouveau pod par Kubernetes.

Définition des règles de routage

Maintenant que les services back et front sont déployés, ainsi que Traefik, je dois indiquer à ce dernier les règles de routage à appliquer. Je veux que toutes les requêtes qui arrivent sur /api soient envoyées sur le back et toutes les requêtes qui arrivent sur/partent sur le front.

Pour cela je déclare deux ressources ingress qui configurent les règles à appliquer. Je leur précise une priorité pour que le chemin /api soit traité avant /, sinon toutes les requêtes seront envoyées sur le front.

09_ingress_controller.yml :

kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: traefik-ingress-back
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.frontend.passHostHeader: "false"
    traefik.frontend.priority: "2"
spec:
  rules:
  - host: traefik-gke-demo.gabrielsagnard.fr
    http:
      paths:
      - path: /api
        backend:
          serviceName: back-service
          servicePort: 3000
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: traefik-ingress-front
  annotations:
    kubernetes.io/ingress.class: traefik
    traefik.frontend.passHostHeader: "false"
    traefik.frontend.priority: "1"
spec:
  rules:
  - host: traefik-gke-demo.gabrielsagnard.fr
    http:
      paths:
      - path: /
        backend:
          serviceName: front-service
          servicePort: 80

Test de l’application

J’accède à l’application via l’url https://traefik-gke-demo.gabrielsagnard.fr/. Chaque click sur le bouton “Say hello” incrémente le compteur sur un des services back et nous renvoie son nom.

Pour vérifier que Traefik met sa configuration à jour dynamiquement je peux détruire tous les pods et attendre que Kubernetes en crée de nouveaux :

kubectl delete pods -l app=traefik-gke-demo,tier=backend

Sans actualiser la page web je clique de nouveau sur le bouton “Say Hello” :

La requête a bien été redirigée vers un des nouveaux pods.

Conclusion

Vous savez désormais comment mettre en place Traefik pour gérer le trafic entrant sur un cluster Kubernetes sans vous préoccuper de la découverte des services ou du renouvellement des certificats Let’s Encrypt.