Aller au contenu
Infrastructure as Code medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

Pulumi - preview, tests et CI sur un runner KVM

10 min de lecture

logo pulumi

Pour industrialiser un projet Pulumi local, commencez par trois controles simples : compiler le code Python, lancer un petit test unitaire, puis rejouer pulumi preview --diff --non-interactive. Sur un projet KVM/libvirt, la CI qui execute vraiment le preview ne peut pas tourner sur un runner GitHub heberge, car il ne voit pas qemu:///system. Elle doit tourner sur un runner self-hosted Linux qui a acces a l’hyperviseur.

Le flux de ce guide a ete rejoue le 1 avril 2026 sur la stack dev du lab local avec python -m compileall ., python -m unittest discover, pulumi preview --stack dev --diff --non-interactive, puis validation du workflow GitHub Actions avec actionlint et yamllint.

L’objectif n’est pas encore de construire un pipeline gigantesque. Vous allez mettre en place une chaine courte, mais utile :

  • une verification de syntaxe Python ;
  • un test unitaire minimal sur le chargement de la configuration ;
  • un preview Pulumi non interactif ;
  • un workflow GitHub Actions qui rejoue ces etapes sur un runner adapte ;
  • un lint du YAML du workflow avant envoi.

Pourquoi preview doit rester dans la chaine de qualite

Section intitulée « Pourquoi preview doit rester dans la chaine de qualite »

Dans un projet Pulumi, le preview n’est pas un simple confort visuel. Il sert a verifier l’intention du changement avant l’application reelle.

Sur le lab restructure de cette section, il doit encore annoncer :

  • le composant lab:kvm:LibvirtVmStack ;
  • le provider libvirt ;
  • le reseau pulumi-config-net ;
  • le disque clone ;
  • le disque cloud-init avec userData: [secret] ;
  • la VM pulumi-config-vm.

Si cette lecture disparait du workflow de qualite, vous perdez tres vite la vision du changement avant up.

Le premier test utile ici ne porte pas sur libvirt lui-meme. Il porte sur le contrat de configuration de la stack. Creez un fichier tests/test_config.py avec un faux objet de configuration, puis verifiez les valeurs par defaut et les valeurs surchargees.

from __future__ import annotations
import unittest
from unittest.mock import patch
import pulumi
from pulumi_kvm_local.config import load_stack_settings
class FakeConfig:
def __init__(self, values: dict[str, object] | None = None):
self._values = values or {}
def get(self, key: str):
value = self._values.get(key)
if isinstance(value, int):
return str(value)
return value
def get_int(self, key: str):
value = self._values.get(key)
if value is None:
return None
return int(value)
def require_secret(self, key: str):
value = self._values[key]
return pulumi.Output.secret(str(value))
class LoadStackSettingsTests(unittest.TestCase):
def test_load_stack_settings_uses_defaults(self):
with patch("pulumi_kvm_local.config.pulumi.Config", return_value=FakeConfig({"adminPassword": "secret"})):
settings = load_stack_settings()
self.assertEqual(settings.network_name, "pulumi-lab-net")
self.assertEqual(settings.vm_name, "pulumi-lab-vm")
self.assertEqual(settings.vm_memory_mib, 2048)
self.assertEqual(settings.vm_vcpu, 2)
self.assertEqual(settings.admin_user, "devops")
self.assertIsInstance(settings.admin_password, pulumi.Output)
def test_load_stack_settings_reads_stack_values(self):
fake_config = FakeConfig(
{
"networkName": "pulumi-config-net",
"vmName": "pulumi-config-vm",
"vmMemoryMiB": 3072,
"vmVcpu": 2,
"adminUser": "devops",
"adminPassword": "Pulumi-Devops-2026!",
}
)
with patch("pulumi_kvm_local.config.pulumi.Config", return_value=fake_config):
settings = load_stack_settings()
self.assertEqual(settings.network_name, "pulumi-config-net")
self.assertEqual(settings.vm_name, "pulumi-config-vm")
self.assertEqual(settings.vm_memory_mib, 3072)
self.assertEqual(settings.vm_vcpu, 2)
self.assertEqual(settings.admin_user, "devops")
self.assertIsInstance(settings.admin_password, pulumi.Output)
if __name__ == "__main__":
unittest.main()

Ce test reste volontairement simple : il verifie que votre projet lit bien la configuration attendue sans avoir besoin de lancer une vraie ressource.

Avant d’ecrire la CI, rejouez la chaine localement sur le projet :

Fenêtre de terminal
source venv/bin/activate
python -m compileall .
python -m unittest discover -s tests -p 'test_*.py'
pulumi stack select dev
pulumi preview --stack dev --diff --non-interactive

Dans le lab valide, unittest retourne :

  • Ran 2 tests ;
  • OK.

Le preview non interactif annonce encore 7 ressources a creer et masque correctement userData comme secret. C’est exactement le signal que vous voulez conserver avant une fusion.

Le point important n’est pas seulement que la commande passe. Il faut savoir la lire vite.

Dans le lab rejoue, le preview montre notamment :

  • lab:kvm:LibvirtVmStack ;
  • pulumi:providers:libvirt ;
  • name: "pulumi-config-net" pour le reseau ;
  • name: "pulumi-config-vm" pour la VM ;
  • memory: 3072 et vcpu: 2 ;
  • userData: [secret] pour le disque cloud-init.

Cette lecture donne deja une tres bonne revue automatique du changement, sans executer pulumi up.

Etape 4 - Ecrire un workflow GitHub Actions self-hosted

Section intitulée « Etape 4 - Ecrire un workflow GitHub Actions self-hosted »

Le workflow suivant est volontairement sobre. Il rejoue la meme chaine que vos verifications locales et garde le preview sur un runner self-hosted Linux.

---
name: pulumi-preview
"on":
pull_request:
paths:
- "**/*.py"
- "Pulumi.yaml"
- "Pulumi.dev.yaml"
- "requirements.txt"
- ".github/workflows/pulumi-preview.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
validate-preview:
runs-on:
- self-hosted
- linux
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m venv venv
. venv/bin/activate
pip install -r requirements.txt
- name: Compile Python sources
run: |
. venv/bin/activate
python -m compileall .
- name: Run unit tests
run: |
. venv/bin/activate
python -m unittest discover -s tests -p 'test_*.py'
- name: Pulumi preview
env:
PULUMI_CONFIG_PASSPHRASE: ""
run: |
. venv/bin/activate
pulumi login --local
pulumi stack select dev
pulumi preview --stack dev --diff --non-interactive

Trois points meritent votre attention :

  • runs-on: [self-hosted, linux] parce que le projet vise un hyperviseur local ;
  • PULUMI_CONFIG_PASSPHRASE: "" pour un backend local sans passphrase ;
  • l’absence de up dans la pull request, car le but ici est de relire le changement, pas d’appliquer la stack.

Un fichier GitHub Actions peut etre invalide alors meme que vos commandes Pulumi sont bonnes. Linter le YAML localement evite ce faux signal.

Fenêtre de terminal
actionlint .github/workflows/pulumi-preview.yml
yamllint .github/workflows/pulumi-preview.yml

Dans le lab rejoue, ces deux commandes passent sans erreur.

Chaque couche repond a une question differente :

  • compileall verifie que le code Python reste syntaxiquement acceptable ;
  • unittest verifie un petit comportement local et portable ;
  • pulumi preview relit l’intention IaC avant application ;
  • actionlint et yamllint verifient que la CI elle-meme n’est pas casse.

Vous obtenez ainsi une chaine progressive, legere et bien adaptee a un projet Pulumi encore local.

SymptomeCause probableSolution
Le workflow tourne sur ubuntu-latest mais le preview echoueLe runner ne voit pas qemu:///systemGardez la partie preview sur un runner self-hosted Linux avec acces libvirt
pulumi preview demande une interactionLa commande reste dans son mode interactifAjoutez --non-interactive --diff
Les tests essaient de lire une vraie stackLe test importe pulumi.Config() sans isolationMockez pulumi.Config() comme dans l’exemple
Le YAML est refuse avant meme l’execution des jobsLe workflow est mal formeRejouez actionlint et yamllint avant le push
  • Une bonne CI Pulumi commence souvent par compileall, un test unitaire et un preview non interactif.
  • Sur libvirt, le preview reel doit tourner sur un runner self-hosted qui voit l’hyperviseur.
  • Le preview reste une lecture de qualite essentielle avant up.
  • Linter le workflow lui-meme evite de casser la chaine de qualite pour une simple erreur YAML.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn