Le blog

Déployer API Platform sur Kubernetes quand on ne l'a jamais fait - partie 5

Publié le 18 juillet 2023

Version utilisée : 3.1

Attention : Ce ne sera pas facile.
Connaitre le fonctionnement de API Platform n'est pas un pré-requis. Si vous n'avez jamais fait de Docker, pas de souci, je récapitule quelques concepts et fondamentaux lorsque je trouve que c'est nécessaire. Je vais les précéder du symbole :

Dans la partie 4 nous avons utilisé Minikube pour tester localement, mais il arrive souvent que les projets soient trop gros et qu'il faille déployer sur Kubernetes en développement. Lançons-nous !

Déployer sur un vrai Cluster #

Moment crucial ! A-t-on bien appris notre leçon ? Cette étape est source de douleur, alors plutôt que d'écrire en pensant que tout va bien, je vais volontairement utiliser un fournisseur qui m'est inconnu et partager mes peines. Lorsque vous les rencontrerez, vous aurez une idée de la raison.

Je constate que je pouvais mettre à jour la version de Postgresql, alors je modifie le chart.yaml https://github.com/bitnami/charts/blob/master/bitnami/postgresql/Chart.yaml au profit de la 12.6.0 au moment de l'écriture de ce document.
Pour que ce soit pris en compte, il faut mettre à jour les dépendances de Helm (https://github.com/helm/helm/issues/8036).

helm repo add bitnami https://charts.bitnami.com/bitnami/
helm repo add stable https://charts.helm.sh/stable/   
helm dependency update ./helm/api-platform

Il est temps de trouver un fournisseur de gestion de conteneur en ligne.

Alors, parmi les différents fournisseurs (GCP, AWS, etc), j’ai choisi Digital Ocean (ce n'est pas du tout sponsorisé, j'aurais pu opter pour notre offre PtitKube) pour sa facilité de mise en œuvre. Je n'aurais pas à créer le cluster moi-même. Je crée un compte. Puisque chaque fournisseur a son jargon, il faut que je m'adapte à celui-ci que je ne connais pas encore.

Après avoir lu les différentes descriptions des offres (il y a de quoi faire tourner directement des conteneurs via docker-compose, ou des serveurs classiques, des espaces de stockages, et enfin un onglet kubernetes pour créer un cluster.

Je sélectionne et choisis le datacenter le plus proche. Francfort ou Londres... Allez ! Londres. Ensuite, quelle capacité pour mon cluster ?

" class="wp-image-7487" width="768" height="365"/><figcaption class="wp-element-caption

Chez DO, j'ai le choix entre 1gb de ram 1cpu, 1gb 2cpu, 2.5gb 2cpu ou 6gb 4cpu j'aime le dernier palier, mais il coûte 40 euros par mois, et je n'ai pas encore de client. Optons pour le moins cher et de redimensionner lorsque nécessaire. D'ailleurs, le moins cher, c'est le minimum nécessaire pour faire tourner nos applications, et 3GB de ram est conseillé. Pour adapter à vos besoins, un calcul back-of-the-enveloppe peux vous aider à chiffrer vos ressources.

" class="wp-image-7490" width="1014" height="527"/><figcaption class="wp-element-caption

Ensuite, c'est par Node. En effet, pour éviter d'avoir des services inaccessibles, 2 nodes minimum sont conseillés. Mais, ouch 48€ par mois. Oui, Le zero-down-time coûte cher.

Je nomme ce pool de 2 nodes pool-api-preprod et je nomme le cluster mon-app. D'ailleurs, je me projette en imaginant que c'est le même cluster qui portera prod et preprod, sous des nodes et namespace différents)

Sur l'interface, le projet s'appelle "first project", je change le nom. Oh, visiblement je peux assigner un projet a un environnement. Je suis en train de déployer du dev. Alors je suppose que j'aurai un autre projet dédié à la prod. Ça me permettra de mieux contrôler les coûts plus tard.

" class="wp-image-7491" width="768" height="321"/><figcaption class="wp-element-caption

Je prends le parti de ne pas vous guider dans les interfaces de tous les fournisseurs possibles, ce sera à vous de vous adapter entre les connaissances que je vous partage, et votre expérience. Pendant ce temps-là, mon cluster semble être en cours de création (l'indicateur me dit 4min de délai en général, je vais me faire du thé).

" class="wp-image-7492" width="1191" height="359"/><figcaption class="wp-element-caption

Enfin, le thé est prêt, comme mon cluster. Désormais, un encart "get started" me recommande de configurer mon cluster et de m'y connecter pour le gérer, et deux approches sont proposées : le client de DO ou la méthode classique de kubernetes.

L'outil de DO est open source et pourra probablement m'aider plus tard, et je suppose qu'il n'est qu'une couche au-dessus de kubectl. Tentons. j'installe, et depuis l'admin API de DO, je crée un token pour m'y connecter.

" class="wp-image-7493" width="585" height="521"/><figcaption class="wp-element-caption

Je m'authentifie en nommant ma connexion michel

doctl auth init --context michel

" class="wp-image-7494" width="1321" height="117"/><figcaption class="wp-element-caption

et je colle le jeton dans le CLI

" class="wp-image-7495" width="817" height="90"/><figcaption class="wp-element-caption

doctl auth list

il y a plusieurs contexts je vais définir michel par défaut

doctl auth switch --context michel

Je vérifie que j'ai bien accès à mon compte.

doctl account get

" class="wp-image-7496" width="989" height="69"/><figcaption class="wp-element-caption

Je peux maintenant utiliser la commande de configuration proposée par DO. Ce qui a eu pour effet de sauvegarder mes accès et de définir le contexte courant sur le cluster. Top, tout est prêt. J'ai accès à un tableau de bord de mon cluster.

Je vérifie que kubectl est bien connecté.

kubectl config view

Je vois bien mes infos.

Pour déployer sur Minikube, nous avions envoyé nos images sur un registry porté par Minikube lui-même. Sauf que pour Digital Ocean, ça n'existe pas, il faut un registry public. C'est aussi payant (évidemment) selon le nombre de dépôts et l'espace disque. Mais heureusement que côté espace, nos images sont petites. Pour vérifier, l'usage de Dive est idéal.

Hop j'ai maintenant mon registry.digitalocean.com/michel-registry

Je vais commencer par build et tag mes images, que je pousserai ensuite sur leurs dépôts. (attention, si vous êtes sous macos, il faut bien penser à ajouter l'option --platform linux/amd64 pour que les images soient construites pour être compatible avec Linux)

docker build -t registry.digitalocean.com/michel-registry/php:0.1.0 -t registry.digitalocean.com/michel-registry/php:latest api --target app_php

Grâce à dive, je vais pouvoir vérifier la taille de cette image dive docker://registry.digitalocean.com/michel-registry/php:latest visiblement environ 183MB. Je m'occupe de Caddy maintenant.

docker build -t registry.digitalocean.com/michel-registry/caddy:0.1.0 -t registry.digitalocean.com/michel-registry/caddy:latest api --target app_caddy dive docker://registry.digitalocean.com/michel-registry/caddy:latest

Qui pèsera moins de 94MB. Et enfin la PWA.

docker build -t registry.digitalocean.com/michel-registry/pwa:0.1.0 -t registry.digitalocean.com/michel-registry/pwa:latest pwa --target prod dive docker://registry.digitalocean.com/michel-registry/pwa:latest Pour 195MB.

Ce qui fait 472MB, ce qui serait suffisant pour sélectionner le dépôt gratuit si je n'avais qu'une seule image à envoyer. Pas de chance, j'en ai 3.

L'étape suivante c'est d'envoyer les images chez DO. La commande proposée par la documentation est gcloud auth configure-docker je dois trouver l'équivalent Digital Ocean.

doctl registry login

docker push registry.digitalocean.com/michel-registry/php:latest docker push registry.digitalocean.com/michel-registry/caddy:latest docker push registry.digitalocean.com/michel-registry/pwa:latest

Éventuellement, je pourrais push les tags de version, mais pour le moment latest me suffit. Je vais pouvoir ensuite reprendre la commande Helm.

helm upgrade my-project ./helm/api-platform --namespace=default --create-namespace --wait \
    --install \
    --set "php.image.repository=registry.digitalocean.com/michel-registry/php" \
    --set php.image.tag=latest \
    --set "caddy.image.repository=registry.digitalocean.com/michel-registry/caddy" \
    --set caddy.image.tag=latest \
    --set "pwa.image.repository=registry.digitalocean.com/michel-registry/pwa" \
	--set pwa.image.tag=latest \
    --set php.appSecret='!ChangeMe!' \
    --set postgresql.postgresqlPassword='!ChangeMe!' \
    --set postgresql.persistence.enabled=true \
    --set "corsAllowOrigin=*"

Cette commande est longue.

" class="wp-image-7497"/><figcaption class="wp-element-caption

D'ailleurs, cela me permet de faire cette réflexion, ce n'est pas idéal d'avoir PostgreSQL dans Kubernetes, il serait meilleur d'opter pour un système extérieur, un PostgreSQL managé par Digital Ocean directement, pourquoi pas. Sinon (et quoi qu'il arrive) il faudra que je m'assure que des sauvegardes et redondances soient en place à terme.

Ah! je me suis pris un TimeOut.

En allant sur l'admin je clique sur droplets et sélectionne un node. Je vois du trafic. Le CPU est monté a 7%, 20% de mémoire, et des i/o a 3,5Mo/sec sur le disque, 8% d'usage, 1,5Mb/sec en bande passante, rien d'alarmant, mais, alors qu'est-ce qui a planté ? Était-ce juste long ?

D'ailleurs c'est dans cette interface que j'ai accès à l'IP de la machine. Il y a aussi de quoi s'y connecter en shell via l'interface, de quoi éteindre la machine, vérifier les volumes, de quoi redimensionner la machine. Ah, et le plan que j'ai que j'ai utilise des CPU partagés (bon à savoir).

" class="wp-image-6249" width="635"/><figcaption class="wp-element-caption

Comme c'est une situation commune lorsque l'on découvre Kubernetes, je partage avec vous mon débogage. Je lance la commande kubectl get pods pour essayer de comprendre ce qu'il a su faire et pas su faire.

" class="wp-image-6250" width="768"/><figcaption class="wp-element-caption

Je vois 3 pods : api, pwa et postgresql. Postgresql est running et prête (1/1) tandis que les autres sont en statut ImagePullBackOff. Ce qui signifie qu'il n'a pas su récupérer l'image.

Les registry sont généralement privés. Il faut créer les secrets associés pour pouvoir discuter avec lui. Chacun à sa procédure. Chez Digital Ocean, en suivant la documentation, il faut télécharger un document, récupérer le fichier de configuration grâce à une commande et le placer dans notre projet puis enregistrer les secrets avec la commande suivante :

kubectl create secret generic michel-registry \
  --from-file=.dockerconfigjson=docker-config.json \
  --type=kubernetes.io/dockerconfigjson

Le secret est référencé sous le label michel-registry. Ensuite, il faut l'indiquer auprès de nos configurations de pods. Soit pour un à un, ou de façon globale.

Je suis parti dans l'idée de le faire au global.

kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "michel-registry"}]}'

Puis je mets à jour le fichier values.yaml

imagePullSecrets:
    - name: michel-registry

J'exécute à nouveau la commande de déploiement. J'ai une nouvelle erreur fréquente également : CrashLoopBackOff.

" class="wp-image-7498" width="773" height="109"/><figcaption class="wp-element-caption

C'est une erreur que vous allez aimer détester. Par défaut sur le cluster le restartPolicy est à Always, le pod crash, le Kubelet utilise cette option par défaut et redémarre en boucle, puis redémarre en boucle, puis redémarre en boucle... Alors c'est chouette parce que si on attend un autre service, cela laisse le temps à celui-ci de démarrer, mais là tout plante. C'est pour la partie CrashLoop. Pour le BackOff, Kubelet redémarre le pod en augmentant le délai. 10s, 20s, etc.

Et les vrais soucis commencent là. Ça peut-être un souci de configuration, un volume inaccessible, mauvais argument dans ma ligne de commande, des bugs de l'application faisant crasher le conteneur, un bind sur un port déjà utilisé, mémoire trop basse, les services de liveliness qui n'arrivent pas à déclarer les pods comme prêt, ou manque de permission dans le système de fichier... Il va falloir jouer les détectives.

La commande kubectl describe pod PODNAME en remplaçant le PODNAME par celui affiché par kubectl get pods, permet d'avoir pas mal d'infos sur la raison du plantage. Mais là, pas de chance , rien d'expressif. Ou encore la commande kubectl logs PODNAME, pour obtenir les derniers messages apparus sur la sortie standard.

Et si vraiment vous ne trouvez pas, vous pouvez essayer de monter les images localement.

docker run registry.digitalocean.com/michel-registry/php fonctionne, malgré une erreur normale et attendue, l'image cherche à joindre la base de données, mais je ne l'ai pas mise en route.

docker run registry.digitalocean.com/michel-registry/caddy ne fonctionne pas telle quelle, elle attend des arguments en environnement pour Mercure.

Error: loading initial config:  http.handlers.mercure: a JWT key for publishers must be provided

Voilà le souci ! Je ne les ai pas envoyés dans ma commande.

Concernant les configurations manquantes pour mercure, j'ajoute l'étape de génération de clé JWT, j'enlève le mot de passe de la bdd, je laisse celui par défaut pour le moment, pour me simplifier la tâche, j'écris ça sous la forme un script bash.

JWT_PASSPHRASE=$(openssl rand -base64 32)
JWT_SECRET_KEY=$(openssl genpkey -pass file:<(echo "$JWT_PASSPHRASE") -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096)
MERCURE_EXTRA_DIRECTIVES=$(cat <<EOF  
demo  
cors_origins http://localhost:8080 http://localhost:8081 https://localhost http://localhost 
EOF  
)
helm upgrade my-project ./helm/api-platform --namespace=default --create-namespace --wait \
    --install \
    --set "php.image.repository=registry.digitalocean.com/michel-registry/php" \
    --set php.image.tag=latest \
    --set php.image.pullPolicy=Always \
    --set "caddy.image.repository=registry.digitalocean.com/michel-registry/caddy" \
    --set caddy.image.tag=latest \
    --set caddy.image.pullPolicy=Always \
    --set "pwa.image.repository=registry.digitalocean.com/michel-registry/pwa" \
	--set pwa.image.tag=latest \
	--set pwa.image.pullPolicy=Always \
    --set php.jwt.secretKey="$JWT_SECRET_KEY" \
    --set php.jwt.publicKey="$(openssl pkey -in <(echo "$JWT_SECRET_KEY") -passin file:<(echo "$JWT_PASSPHRASE") -pubout)" \
    --set php.jwt.passphrase=$JWT_PASSPHRASE \
    --set php.appSecret='!ChangeMe!' \
    --set postgresql.persistence.enabled=true \
    --set corsAllowOrigin='*' \
    --set trustedHosts='*' \
    --set php.host=localhost \
    --set php.appDebug='1' \
    --set php.appEnv='dev' \
    --set "mercure.publicUrl=https://localhost/.well-known/mercure" \
    --set mercure.extradirectives="$MERCURE_EXTRA_DIRECTIVES" \
    --set "ingress.hosts[0].host=localhost" \
    --set "ingress.hosts[0].paths[0].pathType=ImplementationSpecific"

Et je lance la commande.

" class="wp-image-7499" width="1418" height="328"/><figcaption class="wp-element-caption

Enfin ! Notre application est déployée 😀. Pour la tester, je lance les commandes proposées par Helm.

" class="wp-image-7500" width="1618" height="158"/><figcaption class="wp-element-caption

J'accède aux interfaces, crée une ressource et vérifie que l'enregistrement s'est bien déroulé :

" class="wp-image-7501" width="1343" height="571"/><figcaption class="wp-element-caption

Fiesta !

Et maintenant ?

Maintenant il vous reste quelques étapes pour rendre votre application accessible via un nom de domaine. Acheter le domaine bien sûr, le mapper avec une IP, celle d'un Ingress. Il existe plusieurs types d'Ingress, proposé par plusieurs éditeurs, je vous laisse cette fois-ci en autonomie et entamer votre propre route de l'infonuagique.

Quelques points d'attention : utiliser helm n'interdit pas de continuer à manipuler le cluster au travers kubcetl. Ces modifications-là ne seront pas reflétées automatiquement par helm et pourront empêcher un futur déploiement helm, ou pire, constater la disparition de service non reporté dans les charts.

Enfin, vos hébergeurs (GCP, AWS, Digital Ocean, etc), eux aussi ont leurs opérations de maintenances. Ce qui peut vous réserver des surprises.

Merci de m'avoir lu !!

--

Chez Les-Tilleuls.coop, nous utilisons GCP et proposons des offres d'accompagnements Kubernetes. Nos consultants et nos SRE pourront vous accompagner sur vos projets.

Nos offres Cloud et DevOps
Grégoire Hébert

Grégoire Hébert

Principal developer

Mots-clésAPI Platform, Kubernetes

Le blog

Pour aller plus loin