Mon second déploiement avec Kompose¶
Nous devons recréer un cluster k3d d'une manière que le port interne 80 (où le contrôleur ingress traefik écoute) soit exposé au système (en port 8180). Nous allons aussi configurer le cluster avec deux noeuds "worker" virtuels.
k3d cluster delete mycluster &&\
k3d cluster create mycluster-lb -p "8180:80@loadbalancer" --agents 2
Puis changer de contexte et de namespace comme au début du tutoriel :
De Docker à k8s avec Kompose¶
Maintenant que nous avons vu toutes les étapes qui mènent au déploiement complet d'une application Jupyter, nous reprenons les 3 fichiers générés avec Kompose dans la partie du tutoriel sur Docker qui sont un déploiement, un service de type ClusterIP et un PersistentVolumeClaim. Nous les plaçons dans un dossier à part afin de les appliquer ensemble :
Pour appliquer tous au cluster k8s :
Et vous devez obtenir :
NAME READY STATUS RESTARTS AGE
pod/jupyter-7b8bbbf8b7-brpjq 0/1 ErrImagePull 0 14s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jupyter ClusterIP 10.43.169.51 <none> 8888/TCP 14s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jupyter 0/1 1 0 14s
NAME DESIRED CURRENT READY AGE
replicaset.apps/jupyter-7b8bbbf8b7 1 1 0 14s
Il y a une erreur de type ErrImagePull
dans le pod et cela est du au nom de l'image qu'il faut changer dans le yaml
(nous utilisons l'image de base de Jupyter).
...
spec:
containers:
- image: jupyter/base-notebook # <-- modifié
name: jupyter
env:
- name: JUPYTER_PORT
value: "8888"
...
Variable d'environnement
Il faut aussi ajouter la variable d'environnement JUPYTER_PORT
pour que le conteneur fonctionne. L'initialisation standard ajoute une mauvaise valeur dans le cas d'un cluster multi-node.
Ensuite réappliquer les manifests.
Comme lors du premier déploiement, il y a des ReplicaSet associé au déploiement. Il est possible de voir l'historique des updates de pod via :
k rollout history deployment jupyter
deployment.apps/jupyter
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Volumes¶
Nous avons utilisé des nouveaux objets que sont les Volumes/persistentVolumeClaim pour garder les données entre redémarrages de pod. Il est possible de les afficher avec :
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
pvc-8bb4749f-5b6c-45ab-b1a5-ef850ea39353 100Mi RWO Delete Bound tuto/jupyter-claim0 local-path <unset> 10s
Ce volume est créé à partir d'un PersistentVolumeClaim
(PVC) qui déclare des caractéristiques d'un Volume
(il est aussi possible de créer le volume manuellement, mais c'est peu pratique et ce n'est pas souvent utilisé).
k get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
jupyter-claim0 Bound pvc-901b9698-5ee0-47e6-b465-4bf5dabb448a 100Mi RWO local-path <unset> 16m
Nous pouvons inspecter le manifest responsable du PersistentVolumeClaim
:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
io.kompose.service: jupyter-claim0
name: jupyter-claim0
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
L'option clé du PVC
est le StorageClass
(SC, on utilise StorageClass
par défaut si manqué) :
k get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 177m
Ici le seul SC
fournie avec k3d est de type local-path
. Les données de ces volumes sont stockés dans un dossier local des noeud. On peut avoir des SCs différentes en fonction de l'installation particuliaire de k8s.
Remarque
Une autre option importante est le AccessModes
. Avec ReadWriteOnce
(RWO) le volume peut être attaché à un seul noeud en mode read-write, ReadOnlyMany
(ROX) permet d'attacher le volume à tous les noeuds en mode readonly et ReadWriteMany
(RWX) est utilisé si on a besoin d'écrire sur le volume à partir des différents noeuds de façon concourante.
Exposer le port web de l'application¶
Dans les parties précédentes, nous avons utilisé soit le service
de type NodePort
soit le port-forwarding pour accéder à l'application. Pour exposer l'application web (protocole HTTP/HTTPS) nous allons créer une autre ressource k8s : Ingress
.
Créez le manifest :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jupyter
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jupyter
port:
number: 8888
Et l'appliquez :
Nous pouvons accéder à la définition d'ingress :
Name: jupyter
Labels: <none>
Namespace: tuto
Address: 172.19.0.2,172.19.0.3,172.19.0.4
Ingress Class: traefik
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
*
/ jupyter:8888 (10.42.2.6:8888)
Annotations: ingress.kubernetes.io/ssl-redirect: false
Events: <none>
L'application Jupyter est maintenant accessible :
Configuration d'application¶
Nous allons configurer le mot de passe de l'application Jupyter Lab en utilisant le Configmap
et le Secret
. Nous allons utiliser quelques techniques fréquentes.
Le Configmap
sera utilisé pour stocker un script qui trouve un hash du mot de passe et l'écrit au fichier de la configuration. Le Secret
gardera le mot-de-passe lui-même.
Écrivons le script dans un fichier .py
:
#!/usr/bin/env python
from jupyter_server.auth import passwd
import os
import json
if os.getenv('JUPYTER_PWD') is not None:
pwd = passwd(os.getenv('JUPYTER_PWD'))
cfg_dir = os.path.join(os.getenv('HOME'), '.jupyter')
os.makedirs(cfg_dir, exist_ok=True)
cfg_json = os.path.join(cfg_dir, 'jupyter_server_config.json')
with open(cfg_json, 'w') as fd:
json.dump({"IdentityProvider": {"hashed_password": pwd}}, fd)
else:
print("JUPYTER_PWD environment variable is not set.")
A partir de ce fichier, nous créons le manifeste k8s (nous utilisons pour cela --dry-run=client
pour créer le manifeste en local sans l'appliquer pour le moment) :
Nous vérifions le manifeste :
Manifeste de Configmap
apiVersion: v1
data:
initpwd.py: |
#!/usr/bin/env python
from jupyter_server.auth import passwd
import os
import json
if os.getenv('JUPYTER_PWD') is not None:
pwd = passwd(os.getenv('JUPYTER_PWD'))
cfg_dir = os.path.join(os.getenv('HOME'), '.jupyter')
os.makedirs(cfg_dir, exist_ok=True)
cfg_json = os.path.join(cfg_dir, 'jupyter_server_config.json')
with open(cfg_json, 'w') as fd:
json.dump({"IdentityProvider": {"hashed_password": pwd}}, fd)
else:
print("JUPYTER_PWD environment variable is not set.")
kind: ConfigMap
metadata:
creationTimestamp: null
name: initscript
Et nous l'appliquons :
De même manière nous créons et appliquons le manifeste du Secret
:
k create secret generic jupyter-pwd --from-literal password=very_secure_jupyter_pwd --dry-run=client -o yaml | tee secret.yaml &&\
k apply -f secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: jupyter-pwd
type: Opaque
data:
password: dmVyeV9zZWN1cmVfanVweXRlcl9wd2Q=
Note
Notez que les valeurs sont codé avec base64
par défaut. Ce n'est pas un cryptage. En production nous devons utiliser des techniques supplémentaires pour protéger les secrets.
Pour consulter la valeur du secret il est utile d'utiliser le mode de sortie jsonpath
qui permet choisir une partie du manifeste d'objet d'API k8s :
Ce mode de sortie est disponible pour toutes les ressources k8s.
Nous modifions ensuite le Deploiement
en ajoutant le conteneur d'initialisation initContainer
qui est exécuté avant le conteneur principal. Nous y montons l'initscript qui est stocké dans Configmap comme un fichier. Pour partager le fichier de configuration entre initContainer
et conteneur principal on utilise un volume de type emptyDir
qui est le volume éphémère.
Nouveau Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.cmd: kompose --file compose.yaml convert
kompose.version: 1.35.0 (9532ceef3)
labels:
io.kompose.service: jupyter
name: jupyter
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: jupyter
strategy:
type: Recreate
template:
metadata:
annotations:
kompose.cmd: kompose --file compose.yaml convert
kompose.version: 1.35.0 (9532ceef3)
labels:
io.kompose.service: jupyter
spec:
containers:
- image: jupyter/base-notebook
name: jupyter
ports:
- containerPort: 8888
protocol: TCP
env:
- name: JUPYTER_PORT
value: "8888"
- name: JUPYTER_PWD
valueFrom:
secretKeyRef:
name: jupyter-pwd
key: password
volumeMounts:
- mountPath: /home/jovyan/work/local
name: jupyter-claim0
- name: jupyter-confdir
mountPath: /home/jovyan/.jupyter
initContainers:
- image: jupyter/base-notebook
name: pwd-init
command:
- python
- /initpwd.py
volumeMounts:
- name: initscript
mountPath: /initpwd.py
subPath: initpwd.py
- name: jupyter-confdir
mountPath: /home/jovyan/.jupyter
restartPolicy: Always
volumes:
- name: jupyter-claim0
persistentVolumeClaim:
claimName: jupyter-claim0
- name: initscript
configMap:
name: initscript
- name: jupyter-confdir
emptyDir: {}
Mince !
Nous devons avoir une instance d'application Jupyter avec un mot de passe configuré accessible au http://localhost:8180. Mais il semble que la configuration de mot de passe n'est pas appliquée. Essayez de localiser la faute et fixez-la.
Solution
Le problème c'est que le variable d'environnement a été défini pour le conteneur principal, tandis qu'il faut être mis au conteneur init. C'est évident en analysant les logs du conteneur init :
Le manifeste correct est :
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.cmd: kompose --file compose.yaml convert
kompose.version: 1.35.0 (9532ceef3)
labels:
io.kompose.service: jupyter
name: jupyter
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: jupyter
strategy:
type: Recreate
template:
metadata:
annotations:
kompose.cmd: kompose --file compose.yaml convert
kompose.version: 1.35.0 (9532ceef3)
labels:
io.kompose.service: jupyter
spec:
containers:
- image: jupyter/base-notebook
name: jupyter
ports:
- containerPort: 8888
protocol: TCP
env:
- name: JUPYTER_PORT
value: "8888"
volumeMounts:
- mountPath: /home/jovyan/work/local
name: jupyter-claim0
- name: jupyter-confdir
mountPath: /home/jovyan/.jupyter
initContainers:
- image: jupyter/base-notebook
name: pwd-init
command:
- python
- /initpwd.py
volumeMounts:
- name: initscript
mountPath: /initpwd.py
subPath: initpwd.py
- name: jupyter-confdir
mountPath: /home/jovyan/.jupyter
env:
- name: JUPYTER_PWD
valueFrom:
secretKeyRef:
name: jupyter-pwd
key: password
restartPolicy: Always
volumes:
- name: jupyter-claim0
persistentVolumeClaim:
claimName: jupyter-claim0
- name: initscript
configMap:
name: initscript
- name: jupyter-confdir
emptyDir: {}
Autres spécificités¶
Gestion des ressources¶
Les spécifications des ressources sont optionnels, mais elles sont aussi conseillés.
Il existe deux types des demandes de ressources, request
et limit
.
Si on met le request
pour le conteneur dans un Pod, le scheduler les utilise pour décider dans quel Node mieux l'exécuter. Le kubelet assure aussi que conteneur possède toujours au moins request
quantité de ressources.
Les limite
s sont assurées par kubelet
. Les limites de CPU sont strictes, assurées par le mécanisme de cgroups
. Aucun processus dans le conteneur ne peut l'utiliser plus que déclaré. Par contre, si un conteneur dépasse une limite de mémoire, il peut être tué par le noyau de système.
Nous ajoutons la spécification des ressources dans le déploiement :
...
spec:
...
template:
...
spec:
containers:
- image: jupyter/base-notebook
name: jupyter
resources:
requests:
cpu: 500m
memory: 500Mi
limits:
cpu: "1"
memory: 1024Mi
...
Après avoir appliqué la configuration, exécutons la commande pour suivre le status des pods :
Dans l'interface de Jupyter après avoir saisi le mot de passe, nous créons un nouveau notebook et exécutons le code pour consumer la mémoire sans restriction :
Nous allons voir un Kernel died unexpectedly
dans l'interface web et dans la console où nous pouvons suivre les pods :
NAME READY STATUS RESTARTS AGE
jupyter-58c5c6f65b-c7nsg 1/1 Running 0 55s
jupyter-58c5c6f65b-c7nsg 0/1 OOMKilled 0 2m19s
jupyter-58c5c6f65b-c7nsg 1/1 Running 1 (2s ago) 2m20s
Sondes¶
Les sondes sont de plusieurs types :
livenessProbe
est utilisé pour détecter quand redémarrer un conteneur. Par exemple, si à la cause de bug dans l'application le process est en cours d'exécution, mais il est incapable de traiter les requêtes.readinessProbe
est utilisé pour savoir quand un conteneur est prêt à accepter le trafic. Lorsqu'un Pod n'est pas prêt, il est retiré des équilibreurs de charge des Services.startupProbe
est utilisé pour savoir quand une application d'un conteneur a démarré. Si une telle probe est configurée, elle désactive les contrôles de liveness et readiness jusqu'à cela réussit, en s'assurant que ces probes n'interfèrent pas avec le démarrage de l'application.
Chaque type de sonde peut :
- exécuter une commande dans le conteneur
- envoyer une requête HTTP au serveur qui s'exécute dans le conteneur
- ouvrir un socket TCP vers le conteneur sur le port spécifié
Ajoutez une sonde de vie au conteneur principal de l'application Jupyter :
...
livenessProbe:
httpGet:
path: /api
port: 8888
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
...