Déploiement d’une page de maintenance sur OpenShift via GitLab (2)

Dans l’article Déploiement d’une page de maintenance sur OpenShift via GitLab (1), nous avons pu aborder les différentes étapes conduisant à la mise en place d’une page de maintenance. Dans ce second article, nous allons automatiser le processus via GitLab.

J’ai fait le choix d’utiliser dans cette mise en pratique le gestionnaire de packages Helm. Nous retrouvons donc les ressources énoncées dans le premier article auxquels nous ajoutons les fichiers Chart.yaml et values.yaml :

maintenance-page-helm
├── Chart.yaml
├── values.yaml
├── buildconfig.yaml
├── imagestream.yaml
├── deployment.yaml
├── service.yaml
└── networkpolicy.yaml

Nous gardons la ressource route hors du scope du package Helm afin de pouvoir créer autant de routes que nécessaires pour les applications en maintenance.

Voici les templates Helm permettant de “builder” et déployer la page de maintenance ainsi que les ressources nécessaires à sa future exposition:

buildconfig.yaml :

apiVersion: "build.openshift.io/v1"
kind: "BuildConfig"
metadata:
  labels:
    app: {{ .Chart.Name }}
    app.kubernetes.io/managed-by: Helm
    version: {{ .Values.imageTag }}
  name: {{ .Chart.Name }}
  annotations:
    meta.helm.sh/release-name: {{ .Chart.Name }}
spec:
  triggers: []
  failedBuildsHistoryLimit: 5
  nodeSelector: null
  output:
    to:
      kind: "ImageStreamTag"
      name: "{{ .Chart.Name }}:{{ .Values.imageTag }}"
    pushSecret:
      name: "docker-registry-secret"
  source:
    binary: {}
    type: Binary
  strategy:
    dockerStrategy: {}
    type: "Docker"

imagestream.yaml:

apiVersion: "image.openshift.io/v1"
kind: "ImageStream"
metadata:
  labels:
    app: {{ .Chart.Name }}
    app.kubernetes.io/managed-by: Helm
  name: {{ .Chart.Name }}
  annotations:
    meta.helm.sh/release-name: {{ .Chart.Name }}

deployment.yaml:

kind: "Deployment"
apiVersion: "apps/v1"
metadata:
  name: {{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
    app.kubernetes.io/managed-by: Helm
    version: {{ .Values.imageTag }}
  annotations:
    meta.helm.sh/release-name: {{ .Chart.Name }}
    image.openshift.io/triggers: |-
      [
        {
          "from": {
          "kind": "ImageStreamTag",
          "name": "{{ .Chart.Name }}:latest"
          },
          "fieldPath": "spec.template.spec.containers[0].image"
        }
      ]
spec:
  strategy:
    type: RollingUpdate
  replicas: 1
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
      name: {{ .Chart.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: maintenance-page:latest
          imagePullPolicy: "Always"
          ports:
            - name: http
              containerPort: 8080
          resources:
            requests:
              memory: {{ .Values.resources.memoryRequest }}
            limits:
              memory: {{ .Values.resources.memoryLimit }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}

service.yaml:

kind: "Service"
apiVersion: "v1"
metadata:
  name: {{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
    app.kubernetes.io/managed-by: Helm
  annotations:
    meta.helm.sh/release-name: {{ .Chart.Name }}
spec:
  selector:
    app: {{ .Chart.Name }}
  ports:
    - port: 8080
      name: http

networkpolicy.yaml:

kind: "NetworkPolicy"
apiVersion: "networking.k8s.io/v1"
metadata:
  name: {{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
    app.kubernetes.io/managed-by: Helm
  annotations:
    meta.helm.sh/release-name: {{ .Chart.Name }}
spec:
  ingress:
    - ports:
        - port: 8080
          protocol: TCP
  podSelector:
    matchLabels:
      app: {{ .Chart.Name }}
  policyTypes:
    - Ingress

Voici les fichiers Chart.yaml et values.yaml essentiels au fonctionnement de Helm:

Chart.yaml:

apiVersion: v2
name: maintenance-page
description: A Helm chart to deploy maintenance page
version: v0.0.1

values.yaml:

imageTag: ''

resources:
  memoryRequest: 12Mi
  memoryLimit: 24Mi

Enfin, voici le template visant à créer les routes menant à la page de maintenance :

route.yaml.template:

kind: "Route"
apiVersion: "route.openshift.io/v1"
metadata:
  labels:
    app: "${APP_NAME}-maintenance-page"
  name: "${APP_NAME}-maintenance-page"
  annotations:
    kubernetes.io/tls-acme: "true"
spec:
  host: "${HOSTNAME}"
  ${SUBPATH_TO_PASS}
  to:
    name: "maintenance-page"
  tls:
    termination: "Edge"
    insecureEdgeTerminationPolicy: "Redirect"

Passons à la mise en place de la CI

Avant de commencer, je souhaiterais préciser que toutes les références à la "route" réfèrent à la ressource route utilisée par OpenShift.

J’ai fait le choix de découper le pipeline GitLab en trois jobs:

  • build-and-deploy ;
  • deploy-route ;
  • delete

Le job de build-and-deploy permet dans un premier temps de construire l’image Docker de la page de maintenance et de la stocker sur un registre Docker, puis dans un second temps de la déployer sans l’exposer.

Le job deploy-route permet de réaliser le routage du trafic vers la page de maintenance.

Le dernier job, delete, permet un retour à la normale. L’application est donc de nouveau disponible et les ressources liées à la page de maintenance sont supprimées si aucune autre application n’est en phase de maintenance.

Lors de la mise en maintenance d’une application, nous lançons donc la CI GitLab en définissant en entrée le nom de la route menant à l’application à placer en maintenance. De même, nous pouvons définir si nécessaire un subpath à placer en maintenance (le reste de l’application restera donc accessible). Si le paramètre subpath est vide, toute l’application est placée en maintenance. Enfin le job delete est à lancer manuellement à la fin de la phase de maintenance afin que l’application redevienne accessible. La CI GitLab est lancée une fois pour chaque application à placer en maintenance.

Nous définissons dans la partie before_script, le script d’authentification au cluster OKD et de sélection du projet dans lequel déployer la page de maintenance:

 before_script:
   - oc login --token=$OKD_TOKEN --insecure-skip-tls-verify=true $OKD_URL
   - oc project $PROJECT

Phase de build et de déploiement

build-maintenance-page:
 stage: build-and-deploy
 script:
   - helm upgrade -i maintenance-page ./maintenance-page-helm --namespace $PROJECT
       --set imageTag=$IMAGE_TAG
   - oc start-build maintenance-page --from-dir=docker --follow
   - oc tag maintenance-page:$IMAGE_TAG maintenance-page:latest
   - oc rollout status -w deployment/maintenance-page

Dans ce premier job, nous déployons le Chart Helm permettant la mise en place de la page de maintenance. Puis nous lançons le build depuis le dossier /docker du projet GitLab. Le dossier /docker doit inclure les fichiers Dockerfile, default.conf et index.html abordés dans l’article précédent. Enfin nous attendons que le pod déployé soit disponible.

Amélioration de la phase de build et déploiement de la page de maintenance :

Nous allons maintenant compléter cette phase afin d’obtenir le résultat escompté.

Afin de ne pas déployer ou "builder" une nouvelle version de la page de maintenance à chaque mise en maintenance d’une nouvelle application, nous pouvons ajouter une variable "BUILD_MAINTENANCE_PAGE". Cette variable est définie à la valeur true lorsque nous souhaitons "builder" puis déployer une nouvelle version de la page de maintenance.

L’utilisateur spécifie de même la variable "IMAGE_TAG":

build-maintenance-page:
 stage: build-and-deploy
 script:
   - helm upgrade -i maintenance-page ./maintenance-page-helm --namespace $PROJECT
       --set imageTag=$IMAGE_TAG
   - |
if [[! -z $BUILD_MAINTENANCE_PAGE ]]; then
    oc start-build maintenance-page --from-dir=docker --follow;
    oc tag ${DOCKER_REGISTRY}/maintenance-page:$IMAGE_TAG maintenance-page:latest;
          oc rollout status -w dc/maintenance-page;
    fi

Routage du trafic vers la page de maintenance

Ici nous voulons que les utilisateurs du service ne reçoivent pas de code 5xx en retour de leurs requêtes au service indisponible. Nous devons donc créer dans un premier temps, la route menant vers la page de maintenance, puis dans un second temps détruire la route menant au service.

Nous admettons que l’utilisateur spécifie en entrée du pipeline, le nom de la route "ROUTE_NAME" à mettre en maintenance. De même l’utilisateur peut placer en maintenance un lien spécifique de l’application en définissant "SUBPATH" en entrée du pipeline.

Par exemple, si l’utilisateur souhaite passer en maintenance l’application ayant pour hostname: example.com, il définit en paramètres:

  • ROUTE_NAME: (le nom de la route OpenShift menant vers l’application)

S’il souhaite ne mettre en maintenance seulement un "SUBPATH" de cette application, il définit en paramètres:

  • ROUTE_NAME: (le nom de la route OpenShift menant vers l’application)
  • SUBPATH: /subpath_example

Mise_en_place_routage_trafic_vers_page_de_maintenance

Processus de mise en place du routage du trafic vers la page de maintenance

Voici le job GitLab lié au processus précédent:

deploy-maintenance-page-route:
 stage: deploy-route
 script:
   - export HOSTNAME=`oc get route $ROUTE_NAME -o jsonpath={.spec.host}`
   - export APP_NAME=${ROUTE_NAME}
   - |
	if [[ "$SUBPATH" != "" ]]; then
	  export SUBPATH_TO_PASS="path: $SUBPATH";
	else
	  export SUBPATH_TO_PASS="";
     fi
   - envsubst < route.yaml.template > route.yaml
   - oc apply -f route.yaml;
   - |
     if [[ "$SUBPATH_TO_PASS" == "" ]]; then
       oc delete route/${ROUTE_NAME} || true;
	fi

Nous pouvons donc avec les jobs précédents, construire et déployer une page de maintenance dans un namespace. Puis mettre en place le routage du trafic pour un ou plusieurs services vers le pod renvoyant la page de maintenance à l’aide du dernier job présenté.

Dans la partie suivante, nous verrons comment mettre en place le job permettant de sortir de maintenance le(s) application(s).

Suppression de la page de maintenance :

Une fois la maintenance terminée, nous avons besoin d’un job pouvant être exécuté manuellement par l’utilisateur pour rediriger le trafic vers l’application sortant de la phase de maintenance.

Processus_de_sortie_de_phase_de_maintenance

Processus de sortie de la phase de maintenance

Afin de faciliter la mise en place de cette dernière phase, je suggère de créer un projet GitLab et de stocker vos templates de mise en place des routes dans ce dernier. Ainsi vous pouvez aisément récupérer ces templates au cours du job et appliquer le bon template afin de créer une route vers le service sortant de maintenance.

Voici une suggestion de script pouvant être mis en place (SUBPATH correspond au PATH du processus ci-dessus et GOOD_ROUTE_TEMPLATE, le template récupéré, habituellement utilisé pour créer la route menant vers l’application en maintenance):

delete-maintenance-page-route:
 stage: delete
 when: manual
 script:
   - |
     if [[ "$SUBPATH" == "" ]]; then
       oc process -f ${GOOD_ROUTE_TEMPLATE} | oc apply -f -;
     fi
     oc delete route/$ROUTE_NAME-maintenance-page;
     if [[ ! $(oc get route | grep maintenance-page 2> /dev/null) ]]; then
       helm del maintenance-page
     fi

Les ressources nécessaires à la page de maintenance ne sont donc supprimées seulement si elles ne sont plus utiles.

Conclusion

Dans ce second article nous avons pu observer comment automatiser le déploiement et la suppression d’une page de maintenance via GitLab.

Comme annoncé en introduction du premier article, je ne saurais que vous conseiller de mettre en place un tel processus. D’autant plus si certains de vos services critiques doivent observer une phase d’indisponibilité lors d’éventuelles mises à jour ou autres opérations.

De même, placer en maintenance toutes applications liées à un service indisponible permet de ne pas les exposer au cours de ce laps de temps critique. Ainsi, vous limiterez les tentatives d’attaques sur ces derniers le temps de la maintenance. Nous pouvons également noter que recevoir côté utilisateur une page de maintenance est plus rassurant qu’un code 5xx ;)