RETEX : comment transformer un monolithe en NodeJS en une application résiliente et scalable ?
Sommaire
J’ai voulu partager les différentes étapes par lesquelles je suis passé et qui m’ont permises d’atteindre l’objectif fixé. Chacun pourra s’inspirer d’une étape sans forcément suivre les autres, c’est une réponse à un cas particulier et non un tutoriel à suivre dans tous les cas.
Dans la suite de cette article, pour une simplification des explications, cette application prendra le nom de “app”.
Dans cet article, j’ai choisi de ne mettre que les lignes me permettant d’illustrer certains points. Vous n’y retrouverez donc pas l’ensemble des configurations et du code m’ayant servi à refactoriser le projet.
Audit et constat
La première phase d’un projet est souvent la découverte de l’application. On doit se poser quelques questions pour pouvoir avancer par étapes.
Voilà quelques questions que je me suis posées dès le début en tant qu’opérationnel :
- Quelles technologies sont utilisées dans cette application ?
- Comment l’installer sur mon poste de travail ?
- Comment vais-je pouvoir développer et itérer facilement ?
- Quels sont les outils prévus par le développeur pour réaliser le déploiement ?
- Où l’a t-il déployé lui-même ?
- L’application est-elle stateless ?
- Combien y a t-il de composants à déployer et comment sont-ils versionnés ?
Quelques questions que je me suis posées en tant que développeur :
- La vue est elle séparée du modèle et du contrôleur ?
- L’api est-elle bien versionnée ?
- Quels et où sont les tests unitaires que je vais devoir lancer ?
- La documentation est-elle suffisante ?
- Comment la sécurité est-elle gérée ?
Bien sûr, il existe de nombreuses questions à se poser, mais celles-là me paraissent structurelles pour bien commencer mon audit.
Suite à ces questions, j’ai pu identifier quelques problèmes aussi bien fonctionnels qu’opérationnels :
- un seul conteneur pour les blocs fonctionnels suivant :
- le front, codé en ReactJS et dont le lancement est configuré par webpack
- le backend, codé en NodeJS
- le lancement des scripts de migration de base de données
- un déploiement via docker-compose, inutilisable en production pour une gestion sans douleur des conteneurs, mais également à refactoriser pour une utilisation en local après mes modifications,
- une gestion des mots de passe en fichier que je dois passer en variable d’environnement en particulier pour une utilisation en mode conteneur,
- pas de “root url” permettant de différencier les appels au backend et les appels au front. Et donc pas de versionning des contrats d’interface,
- pas de gestion de l’utilisateur / mot de passe par défaut mais des commandes à exécuter après connexion à la base de données,
- des rôles utilisateurs mal définis ne permettant pas complètement de répondre au besoin du client interne,
- quelques interfaces graphiques à revoir pour une appropriation de l’outils à notre besoin.
Je devais également pouvoir tester que le front et le backend étaient bien stateless et donc que je pourrais dimensionner ces services en fonction du besoin.
Bref, du boulot en perspective.
Nous verrons ces étapes dans cet article :
- audit
- choix d’architecture
- refactoring de la création des conteneurs
- refactoring du code
- infrastructure dans Google Cloud Platform
- architecture dans Kubernetes
- monitoring
Choix d’architecture
Pour une question de coût et de facilité de mise en place, j’ai choisi de mettre en production sur Google Cloud Platform (GCP) et en particulier pour l’application sur Google Kubernetes Engine (GKE).
Le choix de Kubernetes fut également déterminé par la volonté de pouvoir intégrer d’autres applications dans le futur au sein de la même infrastructure, tout en diminuant le coût au maximum. Il était donc nécessaire de pouvoir densifier notre système d’information dès que possible. L’utilisation des conteneurs était également présente au coeur de cette application dès la phase de développement et permet également ce choix.
J’allais pouvoir utiliser la grande intégration de GCP au sein de Kubernetes pour accélérer la création de mon infrastructure, en particulier la gestion des volumes persistants et des services de type LoadBalancer. C’est également un service managé, je n’ai donc presque pas besoin de me préoccuper de la santé de ce cluster mais juste de lui allouer suffisamment de ressources.
L’utilisation de Google Container Registry (GCR), le repository de conteneurs privé chez Google, tombait également sous le sens dans ce cas d’utilisation.
Une bonne pratique aurait été d’utiliser Cloud SQL pour la gestion de ma base de données plutôt d’un conteneur. J’aurais économisé du temps lors de mes opérations mais c’est également un coût supplémentaire. Il sera possible de migrer vers cette solution si notre application devient très utilisée.
Refactoring des conteneurs
Conformément aux conclusions de mon audit, je devais séparer le conteneur unique en trois. L’objectif étant de gérer au mieux la scalabilité et la montée de version.
Le front
Le front est un processus configuré par Webpack qui permet de servir des fichiers presque statiques. Il nécessite néanmoins un conteneur “node” pour faire tourner le serveur.
Quelques modifications sont nécessaires pour permettre un découplage du front et du backend et en particulier l’ajout d’une variable d’environnement pour définir la route vers le backend.
FROM node:6.2
RUN mkdir -p /app
ENV API_URL http://localhost/api/v1
ENV PORT 80
EXPOSE $PORT
WORKDIR /app
COPY package.json /app/package.json
RUN npm install
COPY build /app/
CMD [ "node", "server.js" ]
Le backend
Le backend est une application NodeJS, qui nécessite donc lui aussi le conteneur “node” en tant que parent.
Les modifications apportées sont principalement liées au développement.
Un point à noter est l’ajout de la configuration des éléments de connexion vers la BDD dans des variables d’environnements.
La migration de base de données
Sortir le script de migration de la base de données est essentiel dans le processus de mise à jour de mon application.
En effet, dans le cas où une mise à jour se ferait avec plusieurs instances du backend déployées, il y aurait un risque non nul de collision entre les différentes instances lançant en parallèle le script de migration.
Cela aurait pour conséquence au mieux un blocage de certaines instances et donc une dégradation du service, et au pire un état de base de données incohérent et donc une reprise d’activité depuis un backup.
Rien de complexe à réaliser dans ce cas, le développeur a bien fait son boulot et j’ai juste à lancer un conteneur héritant de “node” et qui appelle le script souhaité.
Un petit changement est nécessaire pour pouvoir faire passer les configurations de la BDD à l’instanciation du conteneur.
Refactoring du code
L’objectif étant de pouvoir récupérer les nouvelles versions du software au fur et à mesure de leur sortie, il était nécessaire de limiter les changements du code pour une simplification des unifications à venir, du moins tant que le changement fait n’est pas encore accepté par les développeurs de l’application.
Le front
Les modifications du front étant essentiellement esthétiques et fonctionnelles, il n’y a pas lieu de les énumérer ici.
Le seul point d’attention est de bien s’assurer que le lien vers le backend est défini via une variable d’environnement. Ici il s’agit de “API_URL“ que nous avons injecté dans le conteneur du front précédemment.
Pour cela nous pouvons utiliser le processus NodeJS qui récupère la variable d’environnement.
API_URL: process.env.API_URL
Le backend
Un des principaux problèmes rencontrés avec le backend était l’ajout d’un préfixe dans l’url pour pouvoir faire un routage HTTP et indiquer la version de mon backend.
Bonne surprise, cela se fait sans difficulté grâce à la librairie Express qui assure le routage dans cette application NodeJS.
var router = express.Router();
UserRouter.register(router);
DomainRouter.register(router);
...
app.use('/api/v1', router);
app.listen(process.env.PORT || 8080, () => {
…
}
Le problème fonctionnel qui venait du manque de droit pour un nouvel utilisateur créé a été résolu par l’ajout d’une commande SQL supplémentaire.
La migration de base de données
Dans ce cas d’utilisation, la migration de la base de données se fait par une librairie NodeJS qui exécute une série ordonnancée de scripts fournis en enregistrant à chaque étape si le script a été un succès.
Le développeur doit donc fournir un script permettant la montée de version et un autre permettant le retour en arrière pour chaque changement.
Un des soucis que j’avais noté lors de ma découverte était l’absence d’utilisateur par défaut lors de l’initialisation de l’application.
Il me suffit donc d’ajouter une étape de création de l’utilisateur et de lui ajouter tous les droits :
INSERT INTO User (name, email, password) VALUES ('ADMIN','admin@app.fr', 'sha1(‘fakepassword’)');
INSERT INTO UserRole(user_id,roles_id) VALUES ((SELECT id FROM User WHERE email LIKE 'admin@app.fr'), 1);
...
INSERT INTO UserRole(user_id,roles_id) VALUES ((SELECT id FROM User WHERE email LIKE 'admin@app.fr'), 6);
et le script de retour arrière :
DELETE FROM UserRole WHERE User_id=(SELECT id FROM User WHERE email LIKE 'admin@app.fr');
DELETE FROM User WHERE email LIKE 'admin@app.fr';
Docker-compose
Pour pouvoir tester mes modifications, j’ai du revoir le fichier docker-compose de l’application pour ajouter les variables d’environnement et un loadbalancer. J’ai choisi Traefik, un loadbalancer adapté aux conteneurs, qui les découvre en se basant sur leurs labels. De nombreux articles sur Traefik sont disponibles sur notre blog.
version: "3"
services:
loadbalancer:
image: traefik
command: --web --docker --logLevel=DEBUG
networks:
- backend
ports:
- 80:80
- 8080:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /dev/null:/traefik.toml
db:
image: mysql:5.7
environment:
- MYSQL_ROOT_PASSWORD=admin
- MYSQL_DATABASE=app
- MYSQL_USER=app
- MYSQL_PASSWORD=app
ports:
- 3306:3306
labels:
- "traefik.enable=false"
networks:
- backend
back:
image: app/back:latest
links:
- db
networks:
- backend
environment:
- RDS_HOST=db
- RDS_USER=app
- RDS_PORT=3306
- RDS_DATABASE=app
- RDS_PASSWORD=app
expose:
- 8080
labels:
- "traefik.backend=back"
- "traefik.frontend.rule=Host:app-back"
dbmigrate:
image: app/dbmigrate:latest
depends_on:
- db
links:
- db
networks:
- backend
environment:
- RDS_HOST=db
- RDS_USER=app
- RDS_PORT=3306
- RDS_DATABASE=app
- RDS_PASSWORD=app
web:
image: app/front:latest
networks:
- backend
environment:
- API_URL="http://app-back:80/api/v1"
- PORT=80
expose:
- 80
labels:
- "traefik.backend=front"
- "traefik.frontend.rule=Host:app-front"
networks:
backend:
Pour lancer et tester la scalabilité de mes instances, il me suffit de lancer la commande suivante :
docker-compose up -d --scale back=2 --scale web=2
Infrastructure dans GCP
Le choix de création d’éléments d’infrastructure s’est rapidement tourné sur Terraform, c’est une solution très complète et qui a bien intégré GCP. De nombreux articles sur Terraform sont disponibles sur le blog.
L’objectif est de gérer uniquement la partie réseau, sécurité, ressources et cluster Kubernetes avec Terraform. Tout ce qui relève de l’architecture sera géré par Kubernetes.
La première chose à faire est de configurer Terraform pour GCP. Dans GCP il est possible, via IAM, de générer un fichier json lié à un compte de service pour authentifier celle-ci. J’enregistre celui-ci en local sous le titre de "wescale-site-credential.json". Premier réflexe à avoir : pensez bien à ajouter ce fichier dans votre “gitignore” ;-)
Ma configuration de mon projet :
variable "MOD_JSON_PATH" {
default = "wescale-site-credential.json"
}
variable "MOD_PROJECT" {
default = "wescale-site"
}
variable "MOD_REGION" {
default = "europe-west1"
}
provider "google" {
credentials = "${file("${var.MOD_JSON_PATH}")}"
project = "${var.MOD_PROJECT}"
region = "${var.MOD_REGION}"
}
Vous pouvez également exporter le contenu du fichier dans la variable d’environnement “GOOGLE_CREDENTIALS” avec la commande suivante. Terraform va vérifier le contenu de cette variable si le champ credentials est vide.
GOOGLE_CREDENTIALS=$(cat wescale-site-credential.json)
Enfin si vous utilisez la CLI “gcloud”, Terraform est automatiquement configuré avec vos droits utilisateurs une fois la connexion faite avec la commande/
gcloud auth login
Ensuite je crée un réseau “custom” c’est-à-dire sans génération automatique des sous-réseaux : "app-net", et je crée un sous réseau particulier "app-subnet".
J’attribue ensuite des règles firewall à ce réseau et donc j’autorise en TCP les ports suivants :
- les ports 80 (http non sécurisé),
- 443 (pour le https)
- 8080 (pour mon admin) - seulement depuis certaines IPs
Enfin, je crée un Persistent Disk, c’est-à-dire un bloc de stockage géré par GCP, que j’utiliserai pour persister les données de ma base. Je crée un bloc de seulement 20 Go mais cette taille est suffisante pour le peu de données de l’application.
resource "google_compute_disk" "app-1" {
name = "app-1"
type = "pd-standard"
zone = "europe-west1-b"
size = "20"
}
On peut noter que Kubernetes dans GCP peut également créer son Persistent Disk automatiquement à la création du Persistent Volume.
Enfin le cluster Kubernetes se crée facilement :
resource "google_container_cluster" "app-cluster" {
name = "app-cluster"
zone = "${var.MOD_REGION}-b"
initial_node_count = 1
network = "${google_compute_network.app_net.name}"
subnetwork = "${google_compute_subnetwork.app_subnet.name}"
...
}
Une fois ce projet Terraform lancé, j’ai les ressources disponibles pour lancer l’architecture.
Pour récupérer mes credentials de connexion Kubernetes, GCP me fournit un outil simple :
gcloud container clusters get-credentials "app-cluster" --zone europe-west1-b
Architecture dans Kubernetes
L’objectif de ce chapitre est de regarder l’architecture nécessaire à cette application et définie dans Kubernetes.
Configuration de Kubernetes
Il faut déjà configurer quelques éléments dans Kubernetes pour les utiliser ensuite dans mon application. Ces éléments pourront également être utilisés par plusieurs autres applications de mon cluster.
Ajout de la ressource disque
Pour lier mon PersistentDisk créé avec Terraform dans GCP, je dois créer un PersistentVolume dans Kubernetes. Ce lien permettra à Kubernetes de créer ensuite des PersistentVolumeClaim dans cette ressource.
apiVersion: v1
kind: PersistentVolume
metadata:
name: app-pv-1
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 20Gi
gcePersistentDisk:
pdName: app-1
Ajout d’un IngressControler
J’ai choisi de déployer Traefik comme IngressControler. C’est un outil complet qui permettra de résoudre certains problèmes dont je parlerai après.
Dans un premier temps, je vous invite à suivre la documentation, très bien faite, disponible ici si vous souhaitez le déployer.
Dans le cas de ce tutoriel, le type de “Service” est défini à “NodePort” dans ce cas j’utilise plutôt un type “LoadBalancer” de manière à laisser GCP gérer la distribution du trafic entre les différents “Node”. Ce service de type “LoadBalancer” me permet également de lui lier une adresse IP persistante.
Cette adresse IP fixe me permettra de lier un nom de domaine à ce service. De plus, lors d’un redimensionnement de mon cluster il sera à la charge de GCP de modifier la configuration et ce sera donc transparent pour moi.
kind: Service
apiVersion: v1
metadata:
name: traefik-ingress-service
namespace: kube-system
spec:
selector:
k8s-app: traefik-ingress-lb
type: LoadBalancer
ports:
- port: 80
name: web
- port: 8080
name: admin
- port: 443
name: web-secure
J’ajoute également le port 443 me permettant de rendre accessible mon site en HTTPS.
Namespaces
Lors de la création de Kubernetes, le namespace kube-system est créé pour les applications relevant de l’administration du cluster. On y retrouve de nombreuses applications implémentées par GCP dans ce cas.
Je vais créer deux namespaces : un pour mon application et un pour le monitoring du cluster et de ses applications.
apiVersion: v1
kind: Namespace
metadata:
name: app-namespace
L’objet est identique au nom près pour le namespace “monitoring”.
La communication entre Pods
Par défaut dans Kubernetes toutes les communications réseaux sont permises, je commence donc par un DenyAll sur toutes les connexions.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: app-namespace
spec:
podSelector: {}
policyTypes:
- Ingress
Secrets
Enfin, j’ajoute le secret de ma base de données, pour cela je lance la commande suivante :
kubectl create secret generic app-db-mysql --from-file=password.txt
Dans le cas de la création d’un secret en utilisant cette méthode, une erreur fréquente est d’ajouter, sans le savoir, un saut de ligne. Certaines applications vont l'interpréter de façon différentes !
Déploiement du front
Pour déployer le front j’utilise un objet Deployment :
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app-front-deployment
namespace: app-namespace
spec:
template:
metadata:
labels:
app: app
tier: front
spec:
containers:
- env:
- name: API_URL
value: '''https://app.wescale.fr/api/v1'''
image: eu.gcr.io/wescale-site/app-front:0.0.1
name: app-front
ports:
- containerPort: 80
name: app-front
J’utilise aussi un objet HorizontalPodAutoscaler pour dimensionner mes pods. Pour l’instant je ne dimensionne mon service qu’en fonction de la charge CPU. C’est lors de l’utilisation que je saurai quand le produit doit vraiment se redimensionner. C’est une approche pragmatique que je choisis ici.
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: app-front-hpa
namespace: app-namespace
spec:
maxReplicas: 10
minReplicas: 1
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: app-front-deployment
targetCPUUtilizationPercentage: 80
Un service permet de rendre accessible mon front.
apiVersion: v1
kind: Service
metadata:
labels:
app: app
name: front-svc
namespace: app-namespace
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
tier: front
sessionAffinity: None
A noter que seul le port 80 est disponible dans l’application. La gestion du certificat SSL est donc reportée à l’IngressControler.
En terme de sécurité, j’applique une NetworkPolicy permettant à mon front d’être accessible depuis l’IngressController.
Pour l’instant les NetworkPolicy ne permettent pas de cibler un Pod dans un autre namespace. J’autorise donc toutes les connexions depuis le namespace kube-system. C’est une limitation qui devrait être levée dans les prochaines versions de Calico.
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: access-front
namespace: app-namespace
spec:
ingress:
- from:
- namespaceSelector:
matchLabels:
name: kube-system
podSelector:
matchLabels:
app: app
tier: front
policyTypes:
- Ingress
Déploiement du backend
Pour déployer le backend j’utilise un objet Deployment :
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app-back-deployment
namespace: app
spec:
replicas: 1
template:
metadata:
labels:
app: app
tier: back
spec:
containers:
- env:
- name: RDS_HOST
value: app-db-mysql
- name: RDS_USER
value: appdbuser
- name: RDS_PORT
value: "3306"
- name: RDS_DATABASE
value: app
- name: RDS_PASSWORD
valueFrom:
secretKeyRef:
key: mysql-password
name: app-db-mysql
image: eu.gcr.io/wescale-site/app-back:0.0.1
name: app-back
ports:
- containerPort: 8080
J’utilise le secret pour connecter mon backend à la base de données.
Le service est très semblable à celui du front ainsi que le HPA.
La NetworkPolicy doit permettre l’accessibilité depuis l’extérieur du cluster. Le front est exécuté côté client en Javascript et les requêtes ne viendront pas de mes pods front mais bien de mes IngressControllers. J’utilise donc un namespaceSelector et je rends accessible le backend depuis le namespace kube-system.
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: access-back
namespace: app-namespace
spec:
ingress:
- from:
- namespaceSelector:
matchLabels:
name: kube-system
podSelector:
matchLabels:
app: app
tier: back
policyTypes:
- Ingress
Déploiement de la BDD
Le déploiement de la BDD est différent sur plusieurs aspects par rapport aux autres applications vues précédemment.
Premièrement, je souhaite faire persister les données, je dois donc créer un PersistentVolumeClaim.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-db-mysql
namespace: app-namespace
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 8Gi
storageClassName: standard
Je dois lier ensuite le volume /var/lib/mysql, dans lequel mysql stocke ses données d’après la documentation.
La mise en place du “readinessProbe” et du “livenessProbe” permettra de corréler les états de Kubernetes avec les états de la base. En cas d’erreur fonctionnelle de la base, elle sera également redémarrée automatiquement.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app-db-mysql
namespace: app-namespace
spec:
replicas: 1
strategy:
rollingUpdate
template:
metadata:
labels:
app: app-db-mysql
spec:
containers:
- image: mysql:5.7
name: app-db-mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
key: mysql-root-password
name: app-db-mysql
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
key: mysql-password
name: app-db-mysql
- name: MYSQL_USER
value: dbuser
- name: MYSQL_DATABASE
value: app
ports:
- containerPort: 3306
name: mysql
livenessProbe:
exec:
command:
- sh
- -c
- mysqladmin ping -u root -p${MYSQL_ROOT_PASSWORD}
readinessProbe:
exec:
command:
- sh
- -c
- mysqladmin ping -u root -p${MYSQL_ROOT_PASSWORD}
resources:
requests:
cpu: 100m
memory: 256Mi
volumeMounts:
- mountPath: /var/lib/mysql
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-db-mysql
Le “Service” lié à ce déploiement est très simple également. La NetworkPolicy est définie pour permettre le lien entre le backend et la base de données.
apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
name: access-bdd
namespace: app-namespace
spec:
ingress:
- from:
- podSelector:
matchLabels:
app: app
tier: back
podSelector:
matchLabels:
app: app-db-mysql
policyTypes:
- Ingress
Déploiement de la “migration BDD”
Le cas de la migration BDD est différent des cas des autres conteneurs. Il faut le lancer une fois, soit à la création de la BDD soit au moment d’une migration, et ne pas le monitorer en continu.
Un objet Kubernetes permet de n’être exécuté qu’une seule fois : le Job. Le cas le plus courant d’utilisation étant un CronJob.
Ici je peux donc lancer ma commande une seule fois pour créer ou migrer la database. Attention à ne pas oublier la NetworkPolicy pour permettre la connexion à la base.
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate-job
namespace: app-namespace
spec:
template:
metadata:
name: db-migrate-job
spec:
containers:
- name: db-migrate
image: eu.gcr.io/wescale-site/app-dbmigrate:0.0.1
restartPolicy: Never
Rendre accessible depuis l’extérieur
Pour rendre mes services accessibles depuis internet je dois faire un lien entre l’IngressController et le loadbalancer extérieur géré par celui-ci.
La règle d’Ingress doit permettre d’accéder au front et au backend et donc router les paquets arrivant avec le nom de mon site vers le front pour toutes les requêtes sauf celles portant sur “/api/v1” qui doivent être routées au backend.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
name: app-ingress
namespace: app-namespace
spec:
rules:
- host: app.wescale.fr
http:
paths:
- backend:
serviceName: back-svc
servicePort: 8080
path: /api/*
- backend:
serviceName: back-svc
servicePort: 8080
path: /api
- backend:
serviceName: front-svc
servicePort: 80
path: /
- backend:
serviceName: front-svc
servicePort: 80
path: /*
Gestion du SSL
Traefik peut gérer une mise à jour des certificats SSL de manière automatique grâce à Let’sEncrypt et j’ajoute également à la configuration une redirection de http vers https.
Il est possible de gérer la redirection avec une règle ressemblant à une “rewrite url” sinon la redirection est par défaut sur “/”.
Pour déployer la configuration de Traefik, je crée une ConfigMap.
apiVersion: v1
kind: ConfigMap
metadata:
name: traefik-conf
namespace: kube-system
data:
traefik.toml: |
defaultEntryPoints = ["http","https"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[acme]
acmeLogging = true
email = "moi@wescale.fr"
storage = "acme.json"
entryPoint = "https"
onDemand = false
OnHostRule = false
[acme.httpChallenge]
entryPoint = "http"
[[acme.domains]]
main = "app.wescale.fr"
Puis je modifie le déploiement de Traefik, en ajoutant dans les arguments au lancement du conteneur la lecture du fichier de configuration.
- args:
- --api
- --kubernetes
- --logLevel=INFO
- --configfile=/config/traefik.toml
Puis j’insère la ConfigMap dans un volume et je le monte dans “/config” :
volumeMounts:
- mountPath: /config
name: config
volumes:
- configMap:
name: traefik-conf
name: config
L’application est maintenant accessible depuis internet sur HTTPS !
Monitoring et tests
Je dois maintenant monitorer l’application, pour cela j’ai déployé un Prometheus et un Grafana.
Le déploiement de ces deux produits n’est pas décrit ici pour ne pas alourdir l’article, mais vous trouverez un exemple ici.
Traefik
J’ajoute dans la configuration de Traefik, c’est à dire dans ma ConfigMap, les lignes ci-dessous.
[metrics]
[metrics.prometheus]
Pour m’assurer de la découverte de mon Pod par Prometheus, j’ajoute l’annotation suivante dans les metadatas de mon template dans mon Deployment de Traefik.
annotations:
prometheus.io/scrape: "true"
Après un ajout d’un dashboard dans Grafana.
Base de données
Pour la base de données, j’ajoute au déploiement de la base un nouveau conteneur qui contient un “mysqld_exporter”.
Ce conteneur Docker hérite de “prom/mysqld-exporter:master” mais ajoute un script permettant de définir chaque variable de la chaîne de connexion indépendamment. Le conteneur par défaut propose la forme suivante pour la chaîne de connexion : “DATA_SOURCE_NAME='user:password@(hostname:3306)/'“.
- image: eu.gcr.io/wescale-site/my-prom-mysql:master-v4
name: prom-mysql
env:
- name: MYSQL_USER
value: root
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
key: mysql-root-password
name: app-db-mysql
- name: MYSQL_HOST
value: localhost
- name: MYSQL_PORT
value: "3306"
- name: MYSQL_DB
value: app
ports:
- containerPort: 9104
Pour la découverte et le paramétrage de Prometheus, il faut ajouter les annotations suivantes dans le Deployment de ma base de données.
template:
metadata:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "9104"
prometheus.io/scrape: "true"
Après l’ajout d’un dashboard InnoDB trouvé dans le GitHub de Percona.
Conclusion
Cette application est maintenant en production depuis quelques semaines et je n’ai pas de problème pour le moment.
Voyons quels seraient les axes d’amélioration possibles :
- le front sous la forme de fichiers statiques : minifier le front de cette manière et utiliser Google Cloud Storage pour le mettre à disposition à l’extérieur permettra de supprimer un des déploiement qui n’existe que pour servir des fichiers. Le point bloquant devrait pouvoir être traité par une configuration webpack,
- intégration d’OpenTracing : déjà activable sur Traefik, cela me permettrait d’avoir une meilleure visibilité sur les mécanismes du backend pour pouvoir l’améliorer. Mais pour cela il faut faire de nouveaux ajouts dans le code donc diverger du repos que j’ai dupliqué. L’idée est d’attendre l’unification de nos versions pour continuer le développement,
- intégration de métriques Prometheus pour l’application : en effet, c’est le seul point sur lequel je n’ai aucune métrique. La problématique est la même que pour OpenTracing,
- gestion du RBAC sous Kubernetes : c’est un point que je n’ai pas pris le temps de gérer pour le moment,
- utilisation de “custom metrics” pour le dimensionnement : l’ajout de métriques Prometheus dans l’application permettra d’établir des conditions fonctionnelles pour la scalabilité de l’ensemble.
Ce que cette expérience nous montre c’est la nécessité de respecter dès le début certaines bonnes pratiques de développement qui m’ont permis de réaliser cette conversion.
L’ajout d’un architecte et d’un membre d’une équipe SRE/DevOps/Ops permettra de prévoir les besoins de production et d’accélérer le travail des équipes en amont comme en aval du projet.
L’utilisation de Kubernetes et GCP dans ce cas est un gain de temps, de stabilité mais également de facilité dans la mise en place.
J’espère que cet article vous a plu, qu’éventuellement vous avez appris des choses et encore mieux qu’il vous aura donné envie de nous rejoindre !