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.
 

2 pensées sur “Traefik comme reverse proxy sur GKE

  • 16 juin 2018 à 13 h 57 min
    Permalink

    Bonjour,
    J’ai toujours eu du mal avec la programmation…
    Merci pour ce travail.

    Répondre
  • 25 juin 2018 à 12 h 15 min
    Permalink

    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

    Répondre

Répondre à Joël Séguillon Annuler la réponse.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :