IINF170 - Déploiement Continu/CD - 05/02/2024

Objectifs

Cette journée fait suite à la mise en place du pipeline GitLab CI, puis déploiement d’une application Node.js vers un cluster AKS.

Nous allons cette fois aborder les points suivants :

  • Création d’une base de données PostgreSQL sur Azure

  • Connexion à la BDD PostgreSQL depuis une application Node.js

  • Gestion de paramètres sensibles (mots de passe, etc.) avec Kubernetes

  • Gérer les migrations d’un schéma de base de données (changement du schéma) dans un contexte de CD

  • Utilisation de branches et de merge requests plutôt que de pousser des commits directement sur main

  • Amélioration du pipeline GitLab CI afin de :

    • Ne construire et pousser l’image Docker vers le registre ACR que lors de push sur la branche main/master ou de push d’un tag vX.Y.Z.
    • Ne déployer l’image que lors du push d’un tag vX.Y.Z.
  • Gestion du stockage dans Kubernetes via des volumes persistants

Point de départ

❗️ IMPORTANT ❗️ : pensez à documenter ce que vous faites au fur et à mesure. Créez-vous un fichier Markdown (vous pouvez même remplacer le README.md du repo fourni) ou un document Word sur Office 365. Documentez chaque étape même si tout est balisé : notamment, documentez les choses qui ne marchent pas.

Repo

Nous allons partir d’un nouveau repo. Oui, encore un nouveau repo 😅 !

Ce sera le plus simple pour s’assurer que tout le monde part de la même base.

Après avoir forké :

  • Clonez votre fork
  • Placez-vous dans le dossier de votre fork, et lancez : git remote add upstream https://gitlab.com/bhubert/ipi-cicd-image-gallery.git

Cela vous permettra de récupérer des branches depuis mon dépôt original (afin de vous éviter de saisir trop de code - ce n’est pas le but !).

Pipeline

Prenez quelques minutes pour examiner le pipeline .gitlab-ci.yml.

Il reprend ce que nous avions fait lors des dernières sessions.

Quelques rappels si vous en avez besoin (sinon vous pouvez passer à la suite) :

  • L’étape install installe les dépendances (node_modules) et ceux-ci sont mis en cache pour les étapes suivantes.

  • L’étape lint effectue de l’analyse statique pour détecter des erreurs potentielles.

  • L’étape build transforme le source TypeScript en code JavaScript exécutable nativement par Node.js.

  • L’étape build_push_image :

    • construit une variable IMAGE_NAME_TAG à partir du nom du registre ACR, du nom de la branche, et du “SHA1” commit,
    • construit l’image Docker et la tagge en utilisant cette variable,
    • se cconnecte au registre ACR,
    • y pousse l’image
  • L’étape deploy permet de déployer l’app sur un cluster Kubernetes managé par Azure (AKS) :

    • se connecte à Azure via un Principal de Service,
    • installe l’outil kubectl pour contrôler le cluster,
    • reconstruit la même variable IMAGE_NAME_TAG que lors de l’étape précédente,
    • récupère des identifiants pour se connecter au cluster AKS,
    • informe le déploiement Kubernetes nommé server qu’il doit changer son image pour utiliser le nom + tag définis par IMAGE_NAME_TAG,
    • ce qui aura pour effet de démarrer un nouveau pod exécutant un conteneur créé à partir de la dernière image.

Remise en route

Maintenant, si vous faites une modification du code ou du pipeline, et que vous poussez, tout ne va pas marcher directement !

Les étapes build_push_image et deploy vont échouer, pour deux raisons :

  • Nous avons désactivé voire supprimé les ressources créées la dernière fois.
  • Comme nous repartons d’un nouveau repo, plusieurs variables d’environnement doivent être renseignées sur GitLab.

Re-création des ressources

Si vous aviez gardé vos ressources (groupe de ressources, ACR, cluster AKS) : je pense qu’il est plus fiable de tout supprimer. Par contre, vous devriez en principe toujours avoir un Principal de service.

Pour accélérer la création des ressources, je vous propose d’utiliser une petite page web qui vous donnera les noms des ressources, et les commandes pour les recréer : Génération des commandes Azure.

Petit avertissement si vous avez un prénom + nom assez longs (plus de 15 caractères environ), ne mettez que l’initiale de votre prénom (par exemple).

Suivez les instructions générées par la web app pour recréer un groupe de ressources, un registre ACR, un cluster AKS.

Optionnel : re-création d’un principal de service

À suivre uniquement si vous n’avez pas gardé les identifiants du principal de service de la dernière fois, vous allez devoir en recréer un.

C’est redondant avec le support précédent, mais voici les instructions condensées.

Lancez d’abord : az account show qui donnera quelque chose comme ceci :

{
  "environmentName": "AzureCloud",
  "homeTenantId": "1b482281-e94e-4617-ac3f-e628f4d7f3eb",
  "id": "9d4d31c7-1010-45b0-bf57-4661527ebe27",
  "isDefault": true,
  "managedByTenants": [],
  "name": "Azure subscription 1",
  "state": "Enabled",
  "tenantId": "c08cd9ac-5400-4aa2-b32e-1eabbd073423",
  "user": {
    "name": "yourfullname@gmail.com",
    "type": "user"
  }
}

Notez la valeur de la ligne "id", ici 9d4d31c7-1010-45b0-bf57-4661527ebe27.

Lancez ensuite cette commande, en remplaçant l’id par le votre :

az ad sp create-for-rbac --role Contributor --scope /subscriptions/9d4d31c7-1010-45b0-bf57-4661527ebe27

Notez en lieu sûr le résultat, particulièrement appId, password, tenant :

{
  "appId": "cf39fdbd-d70a-49e4-88b6-edad5ca4dc16",
  "displayName": "azure-cli-2024-01-17-18-44-23",
  "password": "ZO8jw~vFW4Vm3gdjwW2cU3d5bK_BgJVfoIThmGZm",
  "tenant": "7e301642-6fa4-4989-a914-8d6cd6df1146"
}

Re-création des variables d’environnement

Pour re-créer les variables nécessaires, rendez-vous sous Settings > CI/CD > Variables, accessibles en bas de la barre latérale gauche.

Revoyez si besoin le support de la session précédente).

Vous avez 6 variables à renseigner.

Les trois suivantes sont fournies par la web app :

  • AZURE_RESOURCE_GROUP → nom du groupe de ressources
  • AZURE_ACR_NAME → nom de l’ACR
  • AZURE_AKS_NAME → nom de l’AKS

Les trois suivantes ne le sont pas. Ce sont les identifiants associés à votre Principal de Service (SP).

  • AZURE_TENANT_ID → correspond à la ligne tenant du résultat de la commande de création du SP.
  • AZURE_CLIENT_ID → ligne appId
  • AZURE_CLIENT_SECRET → ligne password

Premières modifications

Dans un monde idéal - sans contraintes de temps - on pourrait dès maintenant travailler sur des branches, suivant ce workflow :

  • Création d’une branche de travail
  • Modification du code
  • Add, commit, push
  • Création d’une merge request sur GitLab
  • Fusion (merge) de la merge request vers main
  • git pull localement pour récupérer la dernière version

Nous essaierons de mettre cela en place pour la dernière partie.

Dans un premier temps, nous voulons déjà nous assurer que tout fonctionne : restez sur main.

Ensuite, dans le fichier src/routes/home.ts, modifiez le message renvoyé dans la fonction associée à l’URL / :

router.get('/', (req, res) => {
  res.send('Hello, John Doe!'); // <-- message modifié
});

Faites un git add, git commit, git push et suivez la progression des jobs sur GitLab.

  • Si vous êtes sur une branche, seules les étapes install, lint et build seront exécutées. Les autres ne le seront que lorsque vous mergerez cette branche via une merge request.
  • Si vous êtes resté·e sur main, tout le pipeline va s’exécuter.

Il est normal, à ce stade, que l’étape deploy échoue, car nous n’avons pas encore déployé l’application sur AKS.

Par contre, à ce stade, l’étape build_push_image a du réussir, et l’image doit se trouver sur votre registre ACR.

Pour la trouver, sur le portail Azure :

  1. recherchez “container registries” ou “registres de conteneurs”.
  2. allez sur la ligne correspondant à votre nouvel ACR, par ex. acrbenhub240205.
  3. dans la 2ème barre latérale, cherchez Services puis Repositories (ou Dépôts).
  4. cliquez sur ipi-cicd-image-gallery.
  5. vous verrez alors la liste des tags pour cette image Docker, par ex main-235b45b7, etc.

Vous allez combiner le nom de votre ACR (spécifique à chacun·e), celui du dépôt (ipi-cicd-image-gallery pour tout le monde), et le tag pour connaître le nom de votre image initiale à déployer sur Kubernetes. Dans mon exemple :

acrbenhub240205.azurecr.io/ipi-cicd-image-gallery:main-235b45b7

Déploiement sur AKS

Modifiez le fichier aks-config.yml à la ligne 19 :

          image: acrbenhub240205.azurecr.io/ipi-cicd-image-gallery:main-235b45b7

Remplacez le nom de l’image par le vôtre.

Vérifiez ensuite que vous êtes sur le bon cluster, via la commande kubectl config get-contexts. Chez moi (j’ai plusieurs clusters en route) cela donne (colonnes tronquées pour des raisons de lisibilité) :

CURRENT   NAME                        CLUSTER                     AUTHINFO                                                      
          MyAzureAKSCluster           MyAzureAKSCluster           clusterUser_MyAzure...
*         aks-ben-hub-240205          aks-ben-hub-240205          clusterUser_rg-ben-...
          aks-ipi-cicd-bhubert-tmp1   aks-ipi-cicd-bhubert-tmp1   clusterUser_rg-ipi-...
          aks-ipi-cicd-cluster        aks-ipi-cicd-cluster        clusterUser_rg-ipi-...

La petite astérisque en début de ligne doit indiquer le cluster AKS que vous avez créé précédemment (je prends cette précaution en sachant que vous travaillez par ailleurs sur Kubernetes).

Si ce n’est pas le cas, vous pouvez passer sur un autre cluster avec cette commande, en l’adaptant avec les noms de votre groupe de ressources et de votre cluster :

az aks get-credentials --name YourClusterName --resource-group YourResourceGroupName

Puis exécutez kubectl apply -f aks-config.yml. Vous devriez obtenir la sortie suivante :

deployment.apps/server created
service/server created

Vérifiez l’état du service avec kubectl get service server.

Cela donne chez moi :

NAME     TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
server   LoadBalancer   10.0.195.195   20.19.66.200   80:32413/TCP   47s

La colonne EXTERNAL-IP donne l’adresse publique du service, via laquelle on pourra accéder à l’app. Ici, ce sera http://20.19.66.200.

Vous pouvez vérifier l’état des pods avec kubectl get pods :

NAME                      READY   STATUS    RESTARTS   AGE
server-7956d8c5d6-zpkhj   1/1     Running   0          2m18s

Et examiner les logs d’un pod, ici avec kubectl logs server-7956d8c5d6-zpkhj :

> ipi-cicd-node-ts-ecommerce@1.0.0 start
> node dist/index

Server is running on port 3000
GET / 200 98.355 ms - 13
GET / 200 0.724 ms - 13
GET / 200 0.416 ms - 13

Déclencher une mise à jour

Modifier à nouveau le message renvoyé dans src/routes/home.ts. Committez, poussez.

Cette fois le pipeline devrait s’exécuter jusqu’au bout, un premier déploiement ayant été effectué.

Connexion de l’application à une base de données PostgreSQL

Outre le fait qu’une application serveur sans base de données risque d’être très limitée, l’intérêt de connecter notre app à une BDD est de nous donner un prétexte pour aborder plusieurs points cruciaux :

  • la gestion de données sensibles telles que clés d’API, mots de passe, etc.
  • l’évolution du schéma de la base de données au fil du temps. Celui-ci doit être versionné.

Création d’une BDD PostgreSQL sur Azure

Nous allons utiliser le service PostgreSQL Single Server d’Azure. À noter que ce service déprécié en 2025 - mais qu’un équivalent avec des commandes semblables est déjà disponible.

Lancez la commande ci-dessous pour créer d’abord un serveur de bases de données PostgreSQL, en adaptant (sans les < et >) :

  • le nom du serveur
  • le nom du groupe de ressources
  • le mot de passe - de préférence majuscules, minuscules et chiffres sans caractères spéciaux (cela peut compliquer les choses). NOTEZ-LE !!
az postgres server create --name <MyPostgreSQLServer> --resource-group <rg-your-resource-group> --location francecentral --admin-user adminuser --admin-password <your-password> --sku-name B_Gen5_1 --version 11

Par exemple dans mon cas :

az postgres server create -n IpiCiCDServer -g rg-ben-hub-240205 --location francecentral --admin-user adminuser --admin-password '*Pas.le.vrai.mdp*' --sku-name B_Gen5_1 --version 11

Ce qui donne (cela peut prendre du temps) :

Checking the existence of the resource group 'rg-ben-hub-240205'...
Resource group 'rg-ben-hub-240205' exists ? : True 
Creating postgres Server 'ipicicdserver' in group 'rg-ben-hub-240205'...
Your server 'ipicicdserver' is using sku 'B_Gen5_1' (Paid Tier). Please refer to https://aka.ms/postgres-pricing  for pricing details
 / Running ..

Au bout d’un moment, plus de détails sont donnés, et on vous rappelle de bien noter le mot de passe.

Vous allez pouvoir également noter le contenu de la ligne connectionString, qui vous sera utilisée par l’application Node.js pour se connecter.

Make a note of your password. If you forget, you would have to reset your password with 'az postgres server update -n ipicicdserver -g rg-ben-hub-240205 -p <new-password>'.
{
  "additionalProperties": {},
  "administratorLogin": "adminuser",
  "byokEnforcement": "Disabled",
  "connectionString": "postgres://adminuser%40ipicicdserver:*Pas.le.vrai.mdp*@ipicicdserver.postgres.database.azure.com/postgres?sslmode=require",
  "earliestRestoreDate": "2024-02-05T10:30:54.147000+00:00",
  "fullyQualifiedDomainName": "ipicicdserver.postgres.database.azure.com",
  "id": "/subscriptions/22dfff83-e6fa-4d4b-a6c3-02ac1c4830a8/resourceGroups/rg-ben-hub-240205/providers/Microsoft.DBforPostgreSQL/servers/ipicicdserver",
  "identity": null,
  "infrastructureEncryption": "Disabled",
  "location": "francecentral",
  "masterServerId": "",
  "minimalTlsVersion": "TLSEnforcementDisabled",
  "name": "ipicicdserver",
  "password": "*Pas.le.vrai.mdp*",
  "privateEndpointConnections": [],
  "publicNetworkAccess": "Enabled",
  "replicaCapacity": 5,
  "replicationRole": "None",
  "resourceGroup": "rg-ben-hub-240205",
  "sku": {
    "additionalProperties": {},
    "capacity": 1,
    "family": "Gen5",
    "name": "B_Gen5_1",
    "size": null,
    "tier": "Basic"
  },
  "sslEnforcement": "Enabled",
  "storageProfile": {
    "additionalProperties": {},
    "backupRetentionDays": 7,
    "geoRedundantBackup": "Disabled",
    "storageAutogrow": "Enabled",
    "storageMb": 5120
  },
  "tags": null,
  "type": "Microsoft.DBforPostgreSQL/servers",
  "userVisibleState": "Ready",
  "version": "11"
}

Créez ensuite une base de données sur ce serveur, en adaptant (gardez peut-être le nom de la BDD en minuscules, j’ai quelques doutes sur le fait de mettre des majuscules) :

az postgres db create --resource-group <rg-your-resource-group> --server-name <MyPostgreSQLServer> --name <mydatabase>

Vous pouvez obtenir la chaîne de connexion à cette base de données via cette commande :

az postgres server show-connection-string --server-name <MyPostgreSQLServer> -u adminuser -d imagegallery

Qui donne :

{
  "connectionStrings": {
    "C++ (libpq)": "host=IpiCiCDServer.postgres.database.azure.com port=5432 dbname=imagegallery user=adminuser@IpiCiCDServer password={password} sslmode=require",
    "ado.net": "Server=IpiCiCDServer.postgres.database.azure.com;Database=imagegallery;Port=5432;User Id=adminuser@IpiCiCDServer;Password={password};",
    "jdbc": "jdbc:postgresql://IpiCiCDServer.postgres.database.azure.com:5432/imagegallery?user=adminuser@IpiCiCDServer&password={password}",
    "node.js": "var client = new pg.Client('postgres://adminuser@IpiCiCDServer:{password}@IpiCiCDServer.postgres.database.azure.com:5432/imagegallery');",
    "php": "host=IpiCiCDServer.postgres.database.azure.com port=5432 dbname=imagegallery user=adminuser@IpiCiCDServer password={password}",
    "psql_cmd": "postgresql://adminuser@IpiCiCDServer:{password}@IpiCiCDServer.postgres.database.azure.com/imagegallery?sslmode=require",
    "python": "cnx = psycopg2.connect(database='imagegallery', user='adminuser@IpiCiCDServer', host='IpiCiCDServer.postgres.database.azure.com', password='{password}', port='5432')",
    "ruby": "cnx = PG::Connection.new(:host => 'IpiCiCDServer.postgres.database.azure.com', :user => 'adminuser@IpiCiCDServer', :dbname => 'imagegallery', :port => '5432', :password => '{password}')"
  }
}

Autoriser les accès externes à la BDD

Une fois la BDD créée, vous devriez la voir apparaître sur la page d’accueil votre portail Azure.

Cliquez dessus, puis allez, dans la barre latérale, sous Paramètres > Sécurité de la connexion.

Il faut autoriser la connexion à la BDD :

  • depuis chez vous
  • depuis d’autres services Azure.

Dans la capture ci-dessous, ces modifications ont déjà été apportées. Il faut cliquer les zones 1 et 2, puis sauvegarder en 3.

Paramètres de sécurité de la connexion à Azure PostgreSQL

Connexion depuis Node.js

À partir de votre branche main, vous allez créer une branche nommée 1-postgresql-connection, comme ceci :

git checkout -b 1-postgresql-connection

Ensuite, vous allez récupérer le code de ma branche 1-postgresql-connection, comme ceci :

git pull upstream 1-postgresql-connection

Lancez yarn pour installer les nouvelles dépendances qui n’étaient pas présentes dans la branche main.

Ensuite, copiez le fichier sample.env en tant que .env. Ce fichier contiendra des chaînes secrètes qui n’ont pas vocation à être committées. .env est d’ailleurs dans le .gitignore.

Dans .env, modifiez la valeur derrière le signe = et mettez-y la chaîne de connexion pour votre BDD, que vous allez obtenir la chaîne de connexion via cette commande :

az postgres server show-connection-string --server-name <MyPostgreSQLServer> -u adminuser -d imagegallery

Qui donne :

{
  "connectionStrings": {
    "C++ (libpq)": "host=IpiCiCDServer.postgres.database.azure.com port=5432 dbname=imagegallery user=adminuser@IpiCiCDServer password={password} sslmode=require",
    "ado.net": "Server=IpiCiCDServer.postgres.database.azure.com;Database=imagegallery;Port=5432;User Id=adminuser@IpiCiCDServer;Password={password};",
    "jdbc": "jdbc:postgresql://IpiCiCDServer.postgres.database.azure.com:5432/imagegallery?user=adminuser@IpiCiCDServer&password={password}",
    "node.js": "var client = new pg.Client('postgres://adminuser@IpiCiCDServer:{password}@IpiCiCDServer.postgres.database.azure.com:5432/imagegallery');",
    "php": "host=IpiCiCDServer.postgres.database.azure.com port=5432 dbname=imagegallery user=adminuser@IpiCiCDServer password={password}",
    "psql_cmd": "postgresql://adminuser@IpiCiCDServer:{password}@IpiCiCDServer.postgres.database.azure.com/imagegallery?sslmode=require",
    "python": "cnx = psycopg2.connect(database='imagegallery', user='adminuser@IpiCiCDServer', host='IpiCiCDServer.postgres.database.azure.com', password='{password}', port='5432')",
    "ruby": "cnx = PG::Connection.new(:host => 'IpiCiCDServer.postgres.database.azure.com', :user => 'adminuser@IpiCiCDServer', :dbname => 'imagegallery', :port => '5432', :password => '{password}')"
  }
}

Prenez la chaîne entre '' dans la ligne node.js :

postgres://adminuser@IpiCiCDServer:{password}@IpiCiCDServer.postgres.database.azure.com:5432/imagegallery

Remplacez {password} par votre mot de passe.

Le .env devrait donc ressembler à :

PG_CONNECTION_STRING=postgres://adminuser@VotreBDD:VotreMotDePasse@VotreBDD.postgres.database.azure.com:5432/imagegallery

Lancez l’application avec yarn dev puis visitez l’URL http://localhost:3000.

Vous devriez voir le message Hello from PostgreSQL!, signe que la connexion s’est effectuée avec succès.

Voir les fichiers src/pg-pool.ts et src/routes/home.ts pour plus de détails.

En l’état, on se sert de notre base de données Postgres de prod sur notre app locale. On fait cela à des fins de test.

Normalement, il faudrait installer PostgreSQL sur votre machine locale (ou un Postgres dockerisé) et se connecter à cette instance locale.

Gestion des secrets dans Kubernetes

En local, nous avons pouvons stocker la chaîne de connexion à Postgres dans le fichier .env.

En production, nous allons utiliser une autre méthode : les secrets Kubernetes.

Comme le dit l’introduction de la doc :

Les objets secret de Kubernetes vous permettent de stocker et de gérer des informations sensibles, telles que les mots de passe, les jetons OAuth et les clés ssh.

Nous allons :

  • Créer un secret
  • Le référencer dans le fichier aks-config.yml, pour le passer en tant que variable d’environnement PG_CONNECTION_STRING au pod.

Créer un secret

Commande générique pour créer un secret :

kubectl create secret generic <nom-du-secret> --from-literal=cle='valeur'

Pour créer un secret contenant votre chaîne de connexion à Postgres, saisissez ceci en changeant la valeur de la chaîne de connexion, pour mettre la même que dans le .env.

kubectl create secret generic pg-conn-string --from-literal=url='postgres://adminuser@VotreServeur:Password1234@VotreServeur.postgres.database.azure.com:5432/VotreDB?ssl=true'

Utiliser le secret dans le manifeste K8s

Ouvrez le fichier aks-config.yml. Dans la section env: (ligne 22), sous les deux lignes de la variable existante PORT, ajoutez ceci (attention à conserver l’indentation) :

            - name: PG_CONNECTION_STRING
              valueFrom:
                secretKeyRef:
                  name: pg-conn-string
                  key: url

Ceci permet de :

  • référencer le secret pg-conn-string que nous avons créé,
  • lire la valeur associée à la clé url,
  • et s’en servir comme valeur pour PG_CONNECTION_STRING

Vous pouvez ensuite :

  • appliquer le manifeste avec kubectl apply -f aks-config.yml,
  • ou utiliser kubectl set env pour ajouter cette variable d’enviromment, comme ceci : kubectl set env deployment/server PG_CONNECTION_STRING=--from=secret/pg-conn-string:url

À ce stade, vous pouvez pousser la branche sur votre dépôt, créer une merge request, et quand les jobs de la CI sont passés, la merger.

Normalement, une fois l’étape deploy passée, vous pourrez constater que le message qu’on récupère via une requête à PostgreSQL s’affiche sur votre app.

A SUIVRE. Je n’ai pas eu le temps de tout écrire 😑 mais j’ai une feuille de route que je vous partagerai au fur et à mesure.