Tenir le Cap avec les Meilleures Pratiques pour les Charts Helm

Kubernetes, le célèbre outil d’orchestration pour les applications de conteneurs, tire son nom du mot grec pour « pilote », ou celui qui gouverne le navire. Mais comme dans tout voyage, le succès du navigateur dépend de la carte dont il dispose.

Le chart Helm d’une application est cette carte. Une collection de fichiers qui peuvent être déployés à partir d’un dépôt de charts Helm qui décrivent un ensemble connexe de ressources K8s. La confection la plus efficace possible de vos charts Helm permet à Kubernetes de naviguer entre les écueils lors du déploiement des conteneurs dans votre environnement de production.

Mais il existe d'autres façons de partir à la dérive, comme je l'ai constaté en développant des charts K8s accessibles au public pour déployer des produits. Avec chaque demande d'extraction, les commentaires de la communauté Helm m'ont aidé à m'orienter vers les meilleures pratiques des charts Helm qui offraient les meilleurs résultats pour l'exploitation et la mise à jour des conteneurs.

Voici quelques éléments à prendre en compte lors de l'écriture de charts K8s qui seront utilisés par la communauté ou les clients en production. Les questions à se poser sont notamment les suivantes :

  • Quelles dépendances devez-vous définir ?
  • Votre application aura-t-elle besoin d’un état persistant pour fonctionner ?
  • Comment allez-vous gérer la sécurité via les secrets et les autorisations ?
  • Comment allez-vous contrôler l’exécution des conteneurs kubelet ?
  • Comment vous assurer que vos applications sont en cours d’exécution et en mesure de recevoir des appels ?
  • Comment allez-vous présenter au monde les services de l’application ?
  • Comment allez-vous tester votre chart ?

Ce guide propose quelques bonnes pratiques pour structurer et spécifier vos charts Helm qui aideront K8s à faire accoster délicatement vos applications conteneurisées.

Démarrer

Avant de commencer, assurez-vous que vous êtes familiarisé avec les procédures essentielles pour le développement de charts Helm.

Dans ce guide, nous allons créer un chart Helm selon les meilleures pratiques que nous recommandons pour déployer une application de création, lecture, mise à jour et suppression (CRUD) à deux niveaux pour la base de données Mongo à l’aide d’Express.js.

Vous trouverez le code source de notre exemple d’application dans express-crud dans GitHub.

Création et remplissage du chart Helm

Créons notre modèle de chart Helm à l’aide de la commande de créationdu client Helm :

$ helm create express-crud

Nous créerons ainsi une structure de répertoires pour un chart Helm express-crud.

Pour commencer, mettez à jour les métadonnées du chart dans le fichier Chart.yaml qui vient d’être créé. Veillez à ajouter des informations appropriées pour l'appVersion (la version de l’application à utiliser comme balise d'image docker), la description, la version (une chaîne de version SemVer 2), les sources, les responsables et l'icône.

apiVersion: v1
appVersion: "1.0.0"
description: Un chart Helm pour une application express-crud
name: express-crud
version: 0.1.0
sources:
- https://github.com/jainishshah17/express-mongo-crud
maintainers:
- name: myaccount
 email: myacount@mycompany.com
icon: https://github.com/mycompany17/mycompany.com/blob/master/app/public/images/logo.jpg
home: https://mycompany.com/

Définition des Dépendances

Si votre application a des dépendances, vous devez créer un fichier requirements.yaml dans la structure de répertoires du chart Helm qui les spécifie. Étant donné que notre application a besoin de la base de données mongodb, nous devons le spécifier dans la liste des dépendances du fichier requirements.yaml que nous créons.

Un fichier requirements.yaml pour cet exemple contient :

dependencies:
- name: mongodb
 version: 3.0.4
 repository: https://kubernetes-charts.storage.googleapis.com/
 condition: mongodb.enabled

Une fois qu’un fichier requirements.yaml est créé, vous devez exécuter la commande de mise à jour des dépendances dans le client Helm :

$ helm dep update

Création de fichiers de déploiement

Les fichiers de déploiement de votre chart Helm résident dans le sous-répertoire \templates et spécifient la façon dont K8s déploiera l’application conteneur.

Le développement de vos fichiers de déploiement implique la prise de certaines décisions clés.

Objet de Déploiement ou Objet StatefulSet

Le fichier de déploiement que vous créez dépendra du fait que l'application nécessite que K8s la gère comme un Objet de Déploiement ou un Objet StatefulSet.

Un Objet de Déploiement est une application sans état qui est déclarée dans le fichier deployment.yaml et spécifie le paramètre kind comme deployment.

Un Objet Stateful est destiné aux applications avec état et utilisées avec des systèmes distribués. Ils sont déclarés dans le fichier stateless.yaml et spécifient le paramètre kind comme stateful.

Deployment StatefulSet
Les Deployments sont destinés à une utilisation sans état et sont plutôt légers.  Les StatefulSets sont utilisés lorsque l'état doit être rendu persistant. Par conséquent, il utilise volumeClaimTemplates sur les volumes persistants pour s’assurer qu’ils peuvent conserver l’état entre les redémarrages des composants.
Si votre application est sans état ou si l’état peut être créé à partir de systèmes backend au démarrage, utilisez Deployments. Si votre application est avec état ou si vous souhaitez déployer un stockage avec état en plus de Kubernetes, utilisez un StatefulSet.

 

Cette application n’ayant pas besoin d’état pour être rendue persistante, j’utilise un objet de déploiement.
Le fichier deployment.yaml a déjà été créé par la commande helm create.

Nous utiliserons AppVersion comme balise d’image Docker pour notre application. Cela nous permet de mettre à niveau le chart Helm avec la nouvelle version de l’application en changeant simplement la valeur dans Chart.yaml

image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"

Secret Ou ConfigMap

Vous devrez déterminer quels sont les identifiants ou données de configuration qu'il convient de stocker en tant que secrets et celles qui peuvent se trouver dans une ConfigMap.

Les secrets sont destinés aux informations sensibles telles que les mots de passe que K8s stockera dans un format crypté.

Une ConfigMap est un fichier qui contient des informations de configuration qui peuvent être partagées par les applications. Les informations contenues dans une ConfigMap ne sont pas chiffrées, elle ne doit donc pas contenir d’informations sensibles.

Secret ConfigMap
Le fait de placer ces informations dans un secret est plus sûr et plus souple que de les placer textuellement dans une définition de pod ou dans une image docker ; Une ConfigMap vous permet de découpler les artefacts de configuration du contenu de l'image afin de conserver les applications conteneurisées portables
Utilisation pour les données confidentielles Utilisation pour les données non confidentielles
Exemples d'utilisation : Clés API, mot de passe, jetons et clés ssh Exemples d'utilisation : Rotateurs de journaux, Configuration sans données confidentielles

 

Dans cet exemple, nous allons autoriser Helm à extraire des images docker à partir de registres docker privés à l’aide de secrets d’extraction d’images.

Cette procédure s'appuie sur le fait que le cluster Kubernetes dispose d'un secret qui spécifie les informations d'identification du gestionnaire de dépôts. Ce secret peut être créé par une ligne de commande kubectl telle que :

$ kubectl create secret docker-registry regsecret --docker-server=$DOCKER_REGISTRY_RUL --docker-username=$USERNAME --docker-password=$PASSWORD --docker-email=$EMAIL

Dans le fichier values.yaml de votre chart Helm, vous pouvez ensuite passer le nom du secret à une valeur :

imagePullSecrets: regsecret

Vous pouvez ensuite utiliser le secret pour permettre à Helm d’accéder au registre docker via ces lignes dans deployment.yaml :

{{- if .Values.imagePullSecrets }}
 imagePullSecrets:
 - name: {{ .Values.imagePullSecrets }}
{{- end }}

Pour les secrets disponibles pour l’application, vous devez ajouter ces informations directement à values.yaml.

Par exemple, pour configurer notre application pour accéder à mongodb avec un utilisateur et une base de données pré-créés, ajoutez ces informations dans values.yaml

mongodb:
 enabled: true
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword: 
 mongodbDatabase: test

Notez qu’ici, nous ne codons pas en dur les identifiants par défaut dans notre chart Helm. Au lieu de cela, nous utilisons une logique pour générer aléatoirement le mot de passe quand il n'est pas fourni via –set flag ou values.yaml

Nous utiliserons un secret pour transmettre les identifiants mongodb à notre application, via ces lignes dans deployment.yaml.

env:
- name: DATABASE_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ .Release.Name }}-mongodb
      key: mongodb-password

Vous pouvez contrôler l’exécution des conteneurs kubelet via des Init Containers spécialisés ou des Container Lifecycle Hooks.

 

InitContainers Conteneur Lifecycle Hooks
Les InitContainers sont des conteneurs spécialisés qui s’exécutent avant les conteneurs d’application et peuvent contenir des utilitaires ou des scripts de configuration qui ne sont pas présents dans une image d’application. Les conteneurs peuvent utiliser le framework Container lifecycle hook pour exécuter du code déclenché par des événements pendant leur cycle de vie de gestion.
Un Pod peut avoir un ou plusieurs Init Containers, qui sont exécutés avant le démarrage des conteneurs d'applications.

 

Un Pod ne peut avoir qu’un seul hook PostStart ou PreStop
Le hook PostStart s’exécute immédiatement après la création d’un conteneur. Toutefois, il n’y a aucune garantie que le hook s’exécutera avant l'ENTRYPOINT du conteneur. Aucun paramètre n'est passé au handler.
par ex., déplacement de fichiers montés à l’aide de ConfigMap/Secrets vers un emplacement différent.
Le hook PreStop est appelé immédiatement avant qu'un conteneur se termine. Il est bloquant, ce qui veut dire qu'il est synchrone, et doit donc se terminer avant que l'appel pour supprimer le conteneur soit envoyé.

ex., Arrêt progressif de l'application

Vous pouvez utiliser des initContainers pour ajouter des attentes, afin de vérifier que les microservices dépendants sont fonctionnels avant de continuer. Vous pouvez utiliser le hook PostStart pour mettre à jour le fichier dans le même pod, par exemple pour mettre à jour les fichiers de configuration avec l'IP Service

 

Dans notre exemple, ajoutez ces spécifications initContainers au fichier deployments.yaml pour suspendre le démarrage de notre application jusqu’à ce que la base de données soit opérationnelle.

initContainers:
- name: wait-for-db
 image: "{{ .Values.initContainerImage }}"
 command:
 - 'sh'
 - '-c'
 - >
   until nc -z -w 2 {{ .Release.Name }}-mongodb 27017 && echo mongodb ok;
     do sleep 2;
   done

Ajout de sondes de disponibilité et d'activité (Readiness et Liveness)

Il est souvent utile d'ajouter une sonde de disponibilité et une sonde d'activité pour vérifier la santé de l'application. Si vous ne le faites pas, l'application peut échouer de telle sorte qu'elle semble fonctionner, mais ne répond pas aux appels ou aux requêtes.

Ces lignes dans le fichier deployment.yaml ajouteront ces sondes pour effectuer des vérifications périodiques :

livenessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10
readinessProbe:
 httpGet:
   path: '/health'
   port: http
 initialDelaySeconds: 60
 periodSeconds: 10
 failureThreshold: 10

Ajout de la prise en charge d'un RBAC

Ces procédures ajouteront la prise en charge d'un contrôle d’accès basé sur les rôles (RBAC)  à notre chart, lorsqu’une application l’exige.

Étape 1 : Créez un Rôle en ajoutant le contenu suivant dans un fichier role.yaml :
Un rôle peut uniquement être utilisé pour accorder l’accès aux ressources dans un espace de noms unique.

{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
rules:
{{ toYaml .Values.rbac.role.rules }}
{{- end }}

Étape 2 : Créez un RoleBinding en ajoutant le contenu suivant dans un fichier rolebinding.yaml :
Un ClusterRole peut être utilisé pour accorder les mêmes autorisations qu'un Rôle, mais étant à l'échelle du cluster, ils peuvent également être utilisés pour accorder l'accès à :

  • ressources à l'échelle du cluster (comme des nœuds)
  • points de terminaison hors ressources (comme "/healthz")
  • ressources avec espace de noms (comme des pods) dans tous les espaces de noms
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.fullname" . }}
subjects:
- kind: ServiceAccount
 name: {{ template "express-crud.serviceAccountName" . }}
roleRef:
 kind: Role
 apiGroup: rbac.authorization.k8s.io
 name: {{ template "express-crud.fullname" . }}
{{- end }}

Étape 3 : Créez un ServiceAccount en ajoutant le contenu suivant dans un fichier serviceaccount.yaml :

Un compte de service fournit une identité pour les processus qui s'exécutent dans un Pod.

{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   heritage: {{ .Release.Service }}
   release: {{ .Release.Name }}
 name: {{ template "express-crud.serviceAccountName" . }}
{{- end }}

Étape 4 : Utilisez le modèle d’assistance pour définir le nom du ServiceAccount.
Pour ce faire, ajoutez le contenu suivant dans le fichier _helpers.tpl

{{/*
Créez le nom du compte de service à utiliser
*/}}
{{- define "express-crud.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "express-crud.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

Ajout d’un service

Il est maintenant temps de présenter notre application au monde à travers un service.

Un service permet à votre application de recevoir du trafic via une adresse IP. Les services peuvent être présentés de différentes manières en spécifiant un type :

ClusterIP Le service est uniquement accessible par une adresse IP interne depuis le cluster.
NodePort Le service est accessible depuis l’extérieur du cluster via NodeIP et NodePort.
LoadBalancer Le service est accessible depuis l’extérieur du cluster via un équilibreur de charge externe. Peut Entrer dans l’application..


Pour ce faire, ajouter le contenu suivant à service.yaml :

apiVersion: v1
kind: Service
metadata:
 name: {{ template "express-crud.fullname" . }}
 labels:
   app: {{ template "express-crud.name" . }}
   chart: {{ template "express-crud.chart" . }}
   release: {{ .Release.Name }}
   heritage: {{ .Release.Service }}
spec:
 type: {{ .Values.service.type }}
 ports:
   - port: {{ .Values.service.externalPort }}
     targetPort: http
     protocol: TCP
     name: http
 selector:
   app: {{ template "express-crud.name" . }}
   release: {{ .Release.Name }}

Il convient de noter que dans ce qui précède, pour notre type de service nous faisons référence à un paramètre dans notre values.yaml:

service:
  type: LoadBalancer
  internalPort: 3000
  externalPort: 80

Résumé Values.yaml

La définition d'un grand nombre de nos paramètres dans un fichier values.yaml  est une bonne pratique pour aider à maintenir vos charts Helm.

Voici comment le fichier values.yaml de notre exemple s'affiche, montrant la variété des paramètres que nous définissons pour de nombreuses fonctionnalités décrites ci-dessus :

# Valeurs par défaut pour express-mongo-crud.
# Il s’agit d’un fichier au format YAML.
# Déclarez les variables à passer dans vos modèles.

## Contrôle d’accès basé sur un rôle
## Réf : https://kubernetes.io/docs/admin/authorization/rbac/
rbac:
 create: true
 role:
   ## Règles à créer. Selon la spécification de rôle
   rules:
   - apiGroups:
     - ''
     resources:
     - services
     - endpoints
     - pods
     verbs:
     - get
     - watch
     - list

## Compte de service
## Réf : https://kubernetes.io/docs/admin/service-accounts-admin/
##
serviceAccount:
 create: true
 ## Nom du ServiceAccount à utiliser.
 ## Si non défini et que la valeur de create est true, un nom est généré à l’aide du modèle fullname
 name:

## Valeurs de configuration pour la dépendance mongodb
## réf. : https://github.com/kubernetes/charts/blob/master/stable/mongodb/README.md
##
mongodb:
 enabled: true
 image:
   tag: 3.6.3
   pullPolicy: IfNotPresent
 persistence:
   size: 50Gi
 # resources:
 #  requests:
 #    memory: "12Gi"
 #    cpu: "200m"
 #  limits:
 #    memory: "12Gi"
 #    cpu: "2"
 ## Assurez-vous que la valeur de --wiredTigerCacheSizeGB n’est pas supérieure à la moitié de la limite de mémoire !
 ## C’est essentiel pour se protéger contre OOMKill par Kubernetes !
 mongodbExtraFlags:
 - "--wiredTigerCacheSizeGB=1"
 mongodbRootPassword:
 mongodbUsername: admin
 mongodbPassword:
 mongodbDatabase: test
#  livenessProbe:
#    initialDelaySeconds: 60
#    periodSeconds: 10
#  readinessProbe:
#    initialDelaySeconds: 30
#    periodSeconds: 30

ingress:
 enabled: faux
 annotations: {}
   # kubernetes.io/ingress.class: nginx
   # kubernetes.io/tls-acme: "true"
 path: /
 hosts:
   - chart-example.local
 tls: []
 #  - secretName: chart-example-tls
 #    hosts:
 #      - chart-example.local

initContainerImage: "alpine:3.6"
imagePullSecrets:
replicaCount: 1

image:
 repository: jainishshah17/express-mongo-crud
 # tag: 1.0.1
 pullPolicy: IfNotPresent

service:
 type: LoadBalancer
 internalPort: 3000
 externalPort: 80

resources: {}
 # Nous recommandons généralement de ne pas spécifier les ressources par défaut et de laisser cela comme un
 # choix conscient de l’utilisateur. Cela augmente également les chances que les charts s’exécutent dans des environnements avec peu
 # de ressources, par ex. Minikube. Si vous souhaitez spécifier des ressources, décommentez les
 # lignes suivantes, modifiez-les si nécessaire, et supprimez les accolades après 'resources:'.
 # limits:
 #  cpu: 100m
 #  memory: 128Mi
 # requests:
 #  cpu: 100m
 #  memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}

Tests et installations du chart Helm

Il est essentiel de tester notre chart Helm, ce que nous ferons avec la commande helm lint.

$ helm lint ./

## Output
==> Linting ./
Lint OK

1 chart testé, pas d’échecs

Utilisez la commande helm install pour déployer notre application à l'aide d'un chart helm sur Kubernetes.

$ helm install --name test1 ./ 

## Output
NAME:   test1
LAST DEPLOYED: Sat Sep 15 09:36:23 2018
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==> v1beta1/Deployment
NAME           DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
test1-mongodb  1        1        1           0          0s

==> v1beta2/Deployment
test1-express-crud  1  1  1  0  0s

==> v1/Secret
NAME           TYPE    DATA  AGE
test1-mongodb  Opaque  2     0s

==> v1/PersistentVolumeClaim
NAME           STATUS   VOLUME    CAPACITY  ACCESS MODES  STORAGECLASS  AGE
test1-mongodb  Pending  standard  0s

==> v1/ServiceAccount
NAME                SECRETS  AGE
test1-express-crud  1        0s

==> v1/Service
NAME                TYPE          CLUSTER-IP     EXTERNAL-IP  PORT(S)       AGE
test1-mongodb       ClusterIP     10.19.248.205         27017/TCP     0s
test1-express-crud  LoadBalancer  10.19.254.169      80:31994/TCP  0s

==> v1/Role
NAME                AGE
test1-express-crud  0s

==> v1/RoleBinding
NAME                AGE
test1-express-crud  0s

==> v1/Pod(related)
NAME                                READY  STATUS    RESTARTS  AGE
test1-mongodb-67b6697449-tppk5      0/1    Pending   0         0s
test1-express-crud-dfdbd55dc-rdk2c  0/1    Init:0/1  0         0s


NOTES:
1. Get the application URL by running these commands:
     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
           You can watch the status of by running 'kubectl get svc -w test1-express-crud'
  export SERVICE_IP=$(kubectl get svc --namespace default test1-express-crud -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo https://$SERVICE_IP:80

L’exécution de la commande helm install ci-dessus produira une IP_Externe pour l’équilibreur de charge. Vous pouvez utiliser cette adresse IP pour exécuter l’application.

Voici comment notre application apparaît lors de l’exécution :

Result

Conclusion

Comme vous pouvez le voir dans cet exemple, Helm est un système extrêmement polyvalent qui vous offre une grande flexibilité dans la manière de structurer et de développer un chart. En procédant conformément aux conventions de la communauté Helm vous pourrez soumettre plus facilement vos charts Helm pour une utilisation publique, et vous pourrez les gérer beaucoup plus facilement lors de la mise à jour de votre application.

Les charts Helm achevés pour cet exemple de projet se trouvent dans le dépôt express-crud sur GitHub, et vous pouvez examiner ces fichiers fonctionnels pour vous aider à mieux comprendre leur structure.

Pour explorer d’autres exemples, vous pouvez consulter mon dépôt d'exemples de charts Helm pour le développement de produits sur Kubernetes.