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

De nos jours, nous désirons que l’expérience utilisateur soit la moins impactée possible par les mises à jour ou les changements critiques effectués sur nos applications. Notre réflexion porte donc naturellement sur la manière d’effectuer ces changements avec 0 downtime. Cependant, posséder une page de maintenance prête à être déployée peut s’avérer très utile et c’est ce dont nous allons parler dans cet article. Allons-y !

Pourquoi mettre en place une page de maintenance ?

Je vais aborder dans cette partie un cas de figure rencontré au cours de mes expériences de consultant.

Un client avait mis en place plusieurs applications ayant des liens avec du legacy pour certaines tâches. Ce lien était tel que l'indisponibilité du legacy était préjudiciable pour certaines actions sur ces applications. Lesdites applications reposaient sur de l’OpenShift et nous utilisions la stratégie du rolling update pour les mises jours visant le 0 downtime. Ce n’était pas le cas de la partie legacy pour laquelle, des phases de maintenance étaient observées. On a donc décidé de mettre en place un moyen de renvoyer aux clients une page de maintenance lors de la consultation de certains liens. Ceci dans l’optique de mettre certaines parties des applications hors service lors de phases de maintenance de la partie legacy.

Nous avions pensé tout d’abord à effectuer des redirections via la configuration DNS, cependant nous étions dans une phase de mise en place d’un WAF externe à notre cloud provider et tout le trafic ne transitait pas encore par le même service d’entrée.

Ainsi est venu l’idée de déployer temporairement un pod sur le cluster OpenShift dans le même namespace que le service à passer en mode de maintenance. Ce pod devait simplement renvoyer une page de maintenance.

Voici le cahier des charges que nous nous étions fixés :

  • Rediriger le flux vers le pod distribuant la page de maintenance pour l’ensemble des services ayant un lien avec la partie du legacy qui est indisponible ;
  • S’assurer que lorsque le service est indisponible, l’utilisateur ne reçoit pas de code 5xx ;
  • Une fois le service de nouveau accessible, re-basculer les requêtes utilisateurs vers ce service.

Mise_en_place_mode_maintenance

Mise en place du mode maintenance

Comme nous pouvons l’observer dans le schéma précédent, nous souhaitons créer un nouveau service vers lequel est redirigé le flux reçu par la Route A. Ce service doit cibler le pod nommé "Maintenance Page" déjà abordé précédemment.

Nous allons à présent nous concentrer sur les étapes visant à mettre en place ce processus.

Comment mettre en place une page de maintenance?

Phase 1: Build de l’image maintenance-page

Tout d’abord nous souhaitons construire l’image et la stocker dans un registre docker.

Pour faire cette action OpenShift permet de "builder" les images suivant la stratégie définie dans l’objet BuildConfig. Voici le BuildConfig que nous utilisons :

buildConfig-template.yaml :

apiVersion: "build.openshift.io/v1"
kind: "BuildConfig"
metadata:
  labels:
    build: "maintenance-page"
    version: "v1"
  name: "maintenance-page"
spec:
  failedBuildsHistoryLimit: 5
  nodeSelector: null
  output:
    to:
      kind: "ImageStreamTag"
      name: "maintenance-page:v1"
    pushSecret:
      name: "docker-registry-secret"
  source:
    binary: {}
    type: Binary
  strategy:
    dockerStrategy: {}
    type: "Docker"

Nous définissons dans la partie metadata de cette configuration :

  • le nom de l’image: maintenance-page ;
  • la version qui sera incrémentée au fur et à mesure des futurs build ;

De même, nous définissons dans la partie spec:

  • le nombre maximum de build qui échoue à retenir ;
  • le choix de ne pas déployer la page de maintenance sur un nœud spécifique ;
  • la création d’une ressource imageStreamTag associée à cette image ;
  • la stratégie de build utilisée, ici Docker ;
  • et pour finir la source qui permet de construire l’image.

Définissons maintenant le Dockerfile permettant de construire notre image :

FROM nginxinc/nginx-unprivileged:1.20
COPY default.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html

Nous utilisons ici pour base du build l’image nginx officielle et nous copions à l’intérieur de l’image construite les fichiers default.conf et index.html que nous définissons ainsi :

default.conf:

server {
   listen       8080;
   server_name  localhost;

   location / {
       root   /usr/share/nginx/html;
       index  index.html;
       try_files $uri $uri/ /index.html;
   }

   error_page   500 502 503 504  /50x.html;
   location = /50x.html {
       root   /usr/share/nginx/html;
   }

}

Nous désirons que le serveur nginx écoute sur le port 8080 et renvoie la page définie dans le fichier index.html aux requêtes reçues.

exemple simple de fichier index.html:

<!doctype html>
<title>Maintenance Page</title>

<article>
   <h1>We&rsquo;ll be back soon!</h1>
   <div>
       <p>Sorry we are under maintenance procedure, please come back later ...</p>
       <p>&mdash; Maintenance Team</p>
   </div>
</article>

Nous avons ici les composants suffisants pour construire l’image maintenance-page.

Déployons la buildConfig à l'aide de la commande suivante :

oc create -f buildConfig-template.yaml

Nous utiliserons cette commande pour appliquer tous les futurs templates.

Avant de construire l’image, nous allons déployer une ressource ImageStream. Cette dernière a pour but de faire le lien entre l’image construite et le déploiement de la page de maintenance.

imagestream-template.yaml:

apiVersion: "image.openshift.io/v1"
kind: "ImageStream"
metadata:
  labels:
    app: "maintenance-page"
  name: "maintenance-page"

Construisons ensuite l’image maintenance-page depuis le dossier principal où nous stockons le Dockerfile, les fichiers default.conf et index.html :

oc start-build maintenance-page --from-dir=.

Au cours du build, le dossier présent en local est "uploadé" vers le cluster OpenShift, un pod est déployé et le build s’effectue à l’intérieur de ce pod. Nous créons enfin une ressource imageStreamTag associée à l'image résultante.

Nous pouvons par la suite apposer le tag "latest" sur l’image buildée via la commande:

oc tag "maintenance-page:v1" maintenance-page:latest

Phase 2: Déploiement de la page de maintenance:

Sur OpenShift nous pouvons créer une ressource deployment, laquelle permet de définir la stratégie de déploiement de notre page de maintenance :

deployment-template.yaml:

kind: "Deployment"
apiVersion: "apps/v1"
metadata:
  name: "maintenance-page"
  labels:
    app: "maintenance-page"
  annotations:
    # L'annotation permet de faire le lien entre l'ImageStreamTag
    # "maintenance-page:latest" et le déploiement de la page de
    # maintenance. Ainsi un nouveau déploiement est effectué à
    # chaque modification de l'image ciblée par ce tag
    image.openshift.io/triggers: |-
      [
        {
          "from": {
          "kind": "ImageStreamTag",
          "name": "maintenance-page:latest"
          },
          "fieldPath": "spec.template.spec.containers[0].image"
        }
      ]

spec:
  # L'ancien pod n'est pas détruit tant que le nouveau pod n'est 
  # pas disponible
  strategy:
    type: "RollingUpdate" 
  replicas: 1
  template:
    metadata:
      labels:
        app: "maintenance-page"
        deployment: "maintenance-page"
      name: "maintenance-page"
    spec:
      containers:
        - name: "maintenance-page"
          image: maintenance-page:latest
          imagePullPolicy: "IfNotPresent"
          # Le container écoute le port 8080 afin de servir
          # la page de maintenance
          ports:
            - name: http
              containerPort: 8080
          resources:
            requests:
              memory: "12Mi"
            limits:
              memory: "24Mi"
  # Le selector fait le lien entre le pod déployé et le
  # déploiement via le label `deployment`
  selector:
    matchLabels:
      deployment: "maintenance-page"

Afin de diriger le trafic vers ce pod, nous créons un service via le template suivant :

service-template.yaml:

kind: "Service"
apiVersion: "v1"
metadata:
  name: "maintenance-page"
  labels:
    app: "maintenance-page"
spec:
  # Ce service est lié aux pods ayant le label `deployment`
  # suivant:
  selector:
    deployment: "maintenance-page"
  # Ce service écoute le port `8080`
  ports:
    - port: 8080
      name: http

Pour finir il ne reste plus qu’à créer une route afin de router le trafic vers la page de maintenance ainsi qu’une NetworkPolicy autorisant ce même trafic :

network-policy-template.yaml:

kind: "NetworkPolicy"
apiVersion: "networking.k8s.io/v1"
metadata:
  name: "maintenance-page"
  labels:
    app: "maintenance-page"
spec:
  ingress:
    - ports:
        - port: 8080
          protocol: TCP
  # Nous autorisons le pod ayant pour label "maintenance-page" à
  # recevoir du trafic depuis l'extérieur
  podSelector:
    matchLabels:
      app: "maintenance-page"
  policyTypes:
    - Ingress

Nous pouvons changer le service ciblé par la route déjà existante.

Ou tout simplement déployer une nouvelle route avec :

  • SERVICE_NAME: le nom du service à passer en maintenance ;
  • HOSTNAME: l’url menant à ce même service (exemple: service.domain.com)

route-template.yaml:

kind: "Route"
apiVersion: "route.openshift.io/v1"
metadata:
  labels:
    app: "${SERVICE_NAME}-maintenance-page"
  name: "${SERVICE_NAME}-maintenance-page"
  annotations:
    # Si vous utilisez le contrôleur openshift-acme, l'annotation
    # suivante permet d'automatiser la génération des certificats
    # SSL/TLS.
    kubernetes.io/tls-acme: "true"
spec:
  host: "${HOSTNAME}"
  to:
    name: "maintenance-page"
  tls:
    termination: "Edge"
    insecureEdgeTerminationPolicy: "Redirect"

Puis dans un second temps supprimer l’ancienne route via la commande suivante:

oc delete route/${ROUTE_NAME}

Si vous vous rendez sur l’host précédemment défini vous devriez recevoir en retour le message suivant :

Exemple_page_maintenance-1

Votre service est donc en maintenance! Tester le bon fonctionnement de votre application au cours ou après la phase de maintenance avant de rouvrir le flux est toutefois possible ;)

Pour ce faire, vous avez la possibilité d’effectuer un port-forward sur l’un de vos pods via la commande suivante :

oc port-forward ${POD_NAME} 8443:8443

Dans l’exemple précédent nous avons effectué un port-forward depuis le pod ${POD_NAME} vers notre machine via le port 8443 (en supposant que le port 8443 soit utilisé par le pod). Nous pouvons donc accéder à l’application via le localhost:

http://localhost:8443

Nous pouvons placer en maintenance ainsi plusieurs applications et diriger tout le trafic vers le pod maintenance-page en créant des routes distinctes pour chaque application sur le modèle précédent :

Multiple_applications_en_maintenance

Une fois la maintenance terminée, nous redirigeons le trafic sur le service habituel. L’application retrouve ainsi son comportement attendu.

Nous pouvons pour finir, détruire toutes les ressources précédemment créées si nous n’avons plus d’application en maintenance ou les conserver dans le cas contraire.

Conclusion

Avoir un processus de mise en place d’une page de maintenance est donc un atout indéniable pour une application et surtout pour les utilisateurs finaux. D’autant plus si votre application possède des liens critiques avec des services observant une phase d’indisponibilité lors de maintenances (comme c’est le cas ici avec le legacy).

Nous avons pu observer toutes les étapes menant à la mise en place de cette page. J’espère que cet article a été instructif et vous sera utile pour vos travaux en cours ou futurs.

Cependant, bien qu'effectuer ces actions manuellement soit possible, je suggère d’automatiser ce processus d’autant plus si il est effectué fréquemment.

J’aborderai l’automatisation de ce processus dans un second article dans lequel nous utiliserons la CI de GitLab.