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 :
main
/master
ou de push d’un
tag vX.Y.Z
.vX.Y.Z
.Gestion du stockage dans Kubernetes via des volumes persistants
❗️ 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.
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.
- Repo à forker : https://gitlab.com/bhubert/ipi-cicd-image-gallery
Après avoir forké :
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 !).
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
:
IMAGE_NAME_TAG
à
partir du nom du registre ACR, du nom de la branche, et du
“SHA1” commit,L’étape deploy
permet de déployer l’app
sur un cluster Kubernetes managé par Azure (AKS) :
kubectl
pour contrôler le
cluster,IMAGE_NAME_TAG
que lors de l’étape précédente,server
qu’il doit changer son image pour utiliser
le nom + tag définis par IMAGE_NAME_TAG
,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 :
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.
À 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"
}
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
ressourcesAZURE_ACR_NAME
→ nom de l’ACRAZURE_AKS_NAME
→ nom de l’AKSLes 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
Dans un monde idéal - sans contraintes de temps - on pourrait dès maintenant travailler sur des branches, suivant ce workflow :
main
git pull
localement pour récupérer la
dernière versionNous 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.
install
, lint
et
build
seront exécutées. Les autres ne le seront
que lorsque vous mergerez cette branche via une merge
request.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 :
acrbenhub240205
.ipi-cicd-image-gallery
.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
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
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é.
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 :
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
>
) :
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}')"
}
}
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 :
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.
À 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.
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 :
aks-config.yml
,
pour le passer en tant que variable d’environnement
PG_CONNECTION_STRING
au pod.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'
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 :
pg-conn-string
que nous
avons créé,url
,PG_CONNECTION_STRING
Vous pouvez ensuite :
kubectl apply -f aks-config.yml
,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.