Traefik comme reverse proxy sur GKE
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.
Le projet
Je vais déployer une application composée de deux conteneurs. Un front en HTML / Javascript et un back en NodeJS.
Le back incrémente un compteur et nous renvoie le nom du host sur lequel il est hébergé.
server.js :
const express = require('express') const app = express() const router = express.Router() const os = require('os') var COUNTER = 0 router.get('/', function (req, res) { res.send('Hello World ' + COUNTER++ + ' from ' + os.hostname()) }) app.use('/api', router) app.listen(3000, function () { console.log('Example app listening on port 3000!') })
Le front appelle l’api back et affiche la réponse :
index.html :
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Hello world</title> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script> $( document ).ready(function() { $("#btn-hello").click(function() { $.get("/api", function( data ) { $( "#result" ).text( data ) }) }) }); </script> </head> <body> <button id="btn-hello">Say hello</button> <div id="result"></div> </body> </html>
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
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.9.6 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 demo \ --disk-size 10 \ --machine-type n1-highcpu-2 \ --num-nodes 3 \ --cluster-version 1.9.6-gke.0 \ --preemptible
On peut vérifier que GKE a créé un cluster ainsi que trois machines virtuelles :
$ gcloud container clusters list NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS demo europe-west1-c 1.9.6-gke.0 xxx.xxx.xxx.xxx n1-highcpu-2 1.9.6-gke.0 3 RUNNING $ gcloud compute instances list NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS gke-demo-default-pool-e28cb79c-73bd europe-west1-c n1-highcpu-2 true 10.132.0.2 xxx.xxx.xxx.xxx RUNNING gke-demo-default-pool-e28cb79c-b8zc europe-west1-c n1-highcpu-2 true 10.132.0.4 xxx.xxx.xxx.xxx RUNNING gke-demo-default-pool-e28cb79c-c39d europe-west1-c n1-highcpu-2 true 10.132.0.3 xxx.xxx.xxx.xxx 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 :
$ gcloud container clusters get-credentials demo Fetching cluster endpoint and auth data. kubeconfig entry generated for demo. $ kubectl cluster-info Kubernetes master is running at https://xxx.xxx.xxx.xxx [...]
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.zenika.com 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 = "vincent.poilvert@zenika.com" storage = "/etc/traefik/acme/acme.json" entryPoint = "https" [acme.httpChallenge] entryPoint = "http" [[acme.domains]] main = "traefik-gke-demo.zenika.com"
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.zenika.com) 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.zenika.com 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.zenika.com 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.zenika.com/. 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.
Bonjour,
J’ai toujours eu du mal avec la programmation…
Merci pour ce travail.
Et pour ne pas trop vous embêtez vous pouvez aussi déployer traefik avec Helm 🙂
helm install –name traefik-release –namespace kube-system \
–set dashboard.enabled=true,dashboard.domain=traefik.domain.com \
stable/traefik