1. Avant Propos
Premier article sur d'une série portant sur les sujets de "multi tenants".
Kubernetes est une plateforme puissante, souple et évolutive d'orchestration de conteneurs qui peut être utilisée pour déployer et gérer des applications à grande échelle. Cependant, Kubernetes peut être coûteux et difficile à gérer, en particulier lorsque plusieurs équipes ou organisations l'utilisent en même temps.
Une des solution est d'utiliser Capsule
Capsule est un framework multi-tenant basé sur Kubernetes qui facilite la gestion de clusters Kubernetes partagés.
2. Table des matières
- 1. Avant Propos
- 2. Table des matières
- 3. Avantages de Capsule
- 4. Comment ca marche ?
- 5. Utilisation avec un service managé Azure Kubernetes Service (AKS)
- 6. POC
- 6.1. Création d'un cluster AKS avec l'intégration AAD.
- 6.1.1. Création de différents groupe dans Entra (Préparation)
- 6.1.1.1. Création du groupe Capsule Admin
- 6.1.1.2. Création du user qui sera "cluster admin"
- 6.1.2. Création du groupe capsuleDevGroup
- 6.1.3. Création d'un user dans le groupe capsuleDevGroup
- 6.1.4. Création du groupe capsuleStagingGroup
- 6.1.5. Création d'un user dans le groupe capsuleStagingGroup
- 6.1.6. Création du groupe capsuleGroup
- 6.1.6.1. Users
- 6.1.6.2. Groupes
- 6.1.6.3. Memberships
- 6.1.7. Création d'un cluster AKS
- 6.1.8. Récupération du
kubeconfig
permettant l'administration du cluster kubernetes
- 6.2. Ajout des rôles au niveau de IAM de la souscription Azure
- 6.3. Installation de Capsule
- 6.4. Création des Tenants
- 6.5. Utilisation des Tenants
- Gestion des règles au niveau d'un tenant
3. Avantages de Capsule
-
Le Multi-Tenancy Capsule permet de mettre en place une architecture multi-tenant dans un cluster Kubernetes. On peut ainsi créer des "partitions" logiques regroupant un ensemble d'utilisateurs, de namespaces, de droits associés et quota. Très pratique pour faire coexister au sein d'un même cluster des populations ou environnement différents. Cela évite ainsi de provisionner des clusters dédiés.
-
Les "Policy" Elles sont basées sur des règles pour gérer les ressources, les accès, les quotas, limites etc sur les clusters.
-
Le mode déclaratif Parfait pour gérer l'ensemble via du GitOps.
-
La légereté : L'architecture en mode microservices minimalistes.
-
Un projet CNCF Il est au niveau SandBox : https://www.cncf.io/projects/capsule/
4. Comment ca marche ?
- Un contrôleur (Capsule Controler) va gérer une Custom Resource de type Tenant.
Dans chaque tenant, les utilisateurs rattachés pourront créer leurs propres namespace et pourront partager toutes les resources assignées.
- Un moteur règles (Capsule Policy Engine) va se charger de l'isolation des tenants les uns des autres.
Les Network et Security Policies, Resource Quota, Limit Ranges, RBAC, et autres other policies définies au niveau Tenant sont héritées par tous les
namespaces
du tenant.
Un fois déployé le contrôleur vous pourrez faire un kubectl explain tenant.spec
pour connaitre l'ensemble des paramètres que vous pourrez mettre à un tenant.
On a ainsi un mode de fonctionnement qui permet d'avoir des utilisateurs qui peuvent en toute autonomie gérer leur espace sans intervention d'un administrateur du cluster.
5. Utilisation avec un service managé Azure Kubernetes Service (AKS)
Nous allons utiliser un service managé AKS avec l'intégration d'Azure Active Directory (Microsoft Entra ID) pour notre POC.
6. POC
6.1. Création d'un cluster AKS avec l'intégration AAD.
6.1.1. Création de différents groupe dans Entra (Préparation)
Nous allons utiliser la CLI d'azure pour aller au plus simple (tout peut être fait avec opentofu )
6.1.1.1. Création du groupe Capsule Admin
CAPSULE_ADMIN_GROUP_ID=$(az ad group create \
--display-name capsuleAdminGroup \
--mail-nickname capsuleAdminGroup \
--query id \
--output tsv)
# Check
echo $CAPSULE_ADMIN_GROUP_ID
03c9ccac-xxx-xxx-xx-xxxxx
6.1.1.2. Création du user qui sera "cluster admin"
💡Note : Il faudra bien sûr adapter le domaine avec vos données (ici @microsoftalterway.onmicrosoft.com) :
CAPSULE_ADMIN_USER_NAME="capsule-admin@microsoftalterway.onmicrosoft.com"
CAPSULE_ADMIN_USER_PASSWORD="@#Temporary:Password#@"
CAPSULE_ADMIN_USER_ID=$(az ad user create \
--display-name ${CAPSULE_ADMIN_USER_NAME} \
--user-principal-name ${CAPSULE_ADMIN_USER_NAME} \
--password ${CAPSULE_ADMIN_USER_PASSWORD} \
--query id -o tsv)
az ad group member add \
--group capsuleAdminGroup \
--member-id $CAPSULE_ADMIN_USER_ID
6.1.2. Création du groupe capsuleDevGroup
CAPSULE_DEV_GROUP_ID=$(az ad group create \
--display-name capsuleDevGroup \
--mail-nickname capsuleDevGroup \
--query id \
--output tsv)
6.1.3. Création d'un user dans le groupe capsuleDevGroup
Cet utilisateur sera considéré comme le propriétaire du tenant DEV
CAPSULE_DEV_USER_NAME="capsule-user-dev@microsoftalterway.onmicrosoft.com"
CAPSULE_DEV_USER_PASSWORD="@#Temporary:Password#@"
CAPSULE_DEV_USER_ID=$(az ad user create \
--display-name ${CAPSULE_DEV_USER_NAME} \
--user-principal-name ${CAPSULE_DEV_USER_NAME} \
--password ${CAPSULE_DEV_USER_PASSWORD} \
--query id -o tsv)
az ad group member add \
--group capsuleDevGroup \
--member-id $CAPSULE_DEV_USER_ID
6.1.4. Création du groupe capsuleStagingGroup
CAPSULE_STAGING_GROUP_ID=$(az ad group create \
--display-name capsuleStagingGroup \
--mail-nickname capsuleStagingGroup \
--query id \
--output tsv)
6.1.5. Création d'un user dans le groupe capsuleStagingGroup
Cet utilisateur sera considéré comme le propriétaire du tenant STAGING
CAPSULE_STAGING_USER_NAME="capsule-user-staging@microsoftalterway.onmicrosoft.com"
CAPSULE_STAGING_USER_PASSWORD="@#Temporary:Password#@"
CAPSULE_STAGING_USER_ID=$(az ad user create \
--display-name ${CAPSULE_STAGING_USER_NAME} \
--user-principal-name ${CAPSULE_STAGING_USER_NAME} \
--password ${CAPSULE_STAGING_USER_PASSWORD} \
--query id -o tsv)
az ad group member add \
--group capsuleStagingGroup \
--member-id $CAPSULE_STAGING_USER_ID
6.1.6. Création du groupe capsuleGroup
💡 Note : Tous les utilisateurs et groupes étant propriétaires de tenants doivent être dans ce groupe !
CAPSULE_GROUP_ID=$(az ad group create \
--display-name capsuleGroup \
--mail-nickname capsuleGroup \
--query id \
--output tsv)
# Dev Group Assignation
az ad group member add \
--group capsuleGroup \
--member-id $CAPSULE_DEV_USER_ID
# Staging Group Assignation
az ad group member add \
--group capsuleGroup \
--member-id $CAPSULE_STAGING_USER_ID
6.1.6.1. Users
6.1.6.2. Groupes
6.1.6.3. Memberships
6.1.7. Création d'un cluster AKS
Nous allons créer un Cluster AKS avec l'intégration d'AAD, le RBAC activé, et une API publique
Ici nous ne cherchons pas à faire un cluster AKS dans les règles de l'art, nous laissons Azure mettre les valeurs pour un grand nombre de resources (vnet, subnet, taille vm ...)
Nous allons dire au moment de la création quels sont les groupes d'utilisateurs ayant les privilèges de `cluster admin``.
💡 Note : Il faudra bien sûr adapter le domaine avec vos données (ici @microsoftalterway.onmicrosoft.com) :
❗️ Rappelez vous de l'ID ($CAPSULE_ADMIN_GROUP_ID
)
# resource-group
az group create --name aw-capsule --location francecentral
# aks cluster
az aks create \
--resource-group aw-capsule \
--node-resource-group aw-capsule-vm \
--name aw-capsule \
--enable-aad \
--enable-azure-rbac \
--aad-admin-group-object-ids $CAPSULE_ADMIN_GROUP_ID \
--network-plugin azure \
--network-policy calico
💡 Note : il faut installer kubelogin pour pouvoir vous authentifier sur ce cluster.
6.1.8. Récupération du kubeconfig
permettant l'administration du cluster kubernetes
# 1: Si vous souhaitez le mettre dans votre fichier ~/.kube/config
az aks get-credentials --resource-group aw-capsule --name aw-capsule
# 2: Si vous souhaitez le mettre dans un fichier séparé (il faudra utiliser le flag --kubeconfig-file ou la variable KUBEFONFIG pour pointer sur le cluster)
az aks get-credentials --resource-group aw-capsule --name aw-capsule --file ~/.kube/aw-capsule-config
Je vais pour le POC utiliser la deuxième solution (2:)
export KUBECONFIG=~/.kube/aw-capsule-config
# kubectl get nodes
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXX to authenticate.
- Utiliser le compte
capsule-admin@microsoftalterway.onmicrosoft.com
et son mot de passe (@#Temporary:Password#@)
vous devriez avoir dans la fenêtre de votre navigateur :
Azure Kubernetes Service AAD Client
Vous vous êtes connecté à l'application Azure Kubernetes Service AAD Client sur votre appareil. Vous pouvez maintenant fermer cette fenêtre.
et la commande devrait fonctionner 😀
❯ kubectl get nodes
NAME STATUS ROLES AGE VERSION
aks-nodepool1-12585846-vmss000000 Ready agent 6m v1.26.6
aks-nodepool1-12585846-vmss000001 Ready agent 6m v1.26.6
aks-nodepool1-12585846-vmss000002 Ready agent 6m v1.26.6
6.2. Ajout des rôles au niveau de IAM de la souscription Azure
Il faut ajouter pour les différents groupes (dev et staging) les droits suivants
"Reader
" et "Azure Kubernetes Service Cluster User Role
" afin de pouvoir télécharger les kubeconfig
avec la commande az aks get-credential...
6.3. Installation de Capsule
6.3.1. Utiliser le kubeconfig
de l'admin du cluster
❗️Pour rappel :
az aks get-credentials --resource-group aw-capsule --name aw-capsule --file ~/.kube/aw-capsule-config
export KUBECONFIG=~/.kube/aw-capsule-config
6.3.2. Installation du chart helm de l'operateur Capsule
6.3.2.1. Référence du repo
helm repo add clastix https://clastix.github.io/charts
# Si déja installé
helm repo update
6.3.2.2. Déploiement du chart
Il vous faudra l'id du groupe capsuleGroup.
# Pour Rappel
CAPSULE_GROUP_ID=$(az ad group create \
--display-name capsuleGroup \
--mail-nickname capsuleGroup \
--query id \
--output tsv)
Capsule doit connaître les groupes autorisés avec lesquels il travaillera.
Nous devons enregistrer l'ID d'objet du groupe Azure AD capsuleGroup en tant que groupe d'utilisateurs Capsule sous CapsuleConfiguration
helm upgrade --install capsule clastix/capsule \
-n capsule-system \
--create-namespace \
--set manager.options.forceTenantPrefix=true \
--set "manager.options.capsuleUserGroups[0]=$CAPSULE_GROUP_ID"
👀 Contrôle :
❯ kubectl get deploy -n capsule-system
NAME READY UP-TO-DATE AVAILABLE AGE
capsule-controller-manager 1/1 1 1 78s
6.3.3. Installation du chart Helm Capsule Proxy
Capsule Proxy est un module complémentaire pour Capsule Operator qui résout certains problèmes de RBAC lors de l'activation de la multi-location dans Kubernetes, car les utilisateurs ne peuvent pas lister les ressources au niveau cluster.
Kubernetes RBAC ne peut pas répertorier uniquement les ressources détenues à l'échelle du cluster, car il n'existe aucune API filtrée par ACL. Par exemple:
kubectl get namespaces
échoue même si l'utilisateur a les droits.
Comment fonctionne Capsule Proxy
+-----------+ +-----------+ +-----------+
kubectl ------>|:443 |--------->|:9001 |-------->|:6443 |
+-----------+ +-----------+ +-----------+
ingress-controller capsule-proxy kube-apiserver
load-balancer
nodeport
HostPort...
On peut exposer capsule-proxy de différentes manières :
- Ingress
- NodePort Service
- LoadBalance Service
- HostPort
- HostNetwork
Dans notre cas nous allons utiliser un loadbalancer. Sur azure nous aurons une ip publique. Nous allons aussi utiliser une fonctionnalité qui nous permet grace a une annotations de créer un fqdn du type [name].francecentral.cloudapp.azure.com qui nous permettra d'acceder directement au service capsule-proxy par une url distincte.
❗️Pour rappel :
CAPSULE_ADMIN_GROUP_ID=$(az ad group create \
--display-name capsuleAdminGroup \
--mail-nickname capsuleAdminGroup \
--query id \
--output tsv)
helm upgrade --install capsule-proxy clastix/capsule-proxy \
-n capsule-system \
--set service.type=LoadBalancer \
--set service.port=443 \
--set options.oidcUsernameClaim=unique_name \
--set "options.ignoredUserGroups[0]=$CAPSULE_ADMIN_GROUP_ID" \
--set "options.additionalSANs[0]=capsule-proxy.francecentral.cloudapp.azure.com" \
--set service.annotations."service\.beta\.kubernetes\.io/azure-dns-label-name"=capsule-proxy
💡 Note : Vous devrez adapter le SAN : capsule-proxy.francecentral.cloudapp.azure.com
et l'annotation
👀 Contrôle :
❯ kubectl get po,secrets,svc -n capsule-system
NAME READY STATUS RESTARTS AGE
pod/capsule-controller-manager-c98c8fb88-7xzhm 1/1 Running 0 4m11s
pod/capsule-proxy-945bc469d-lc9jz 1/1 Running 0 2m
NAME TYPE DATA AGE
secret/capsule-proxy Opaque 3 116s
secret/capsule-tls Opaque 3 4m11s
secret/sh.helm.release.v1.capsule-proxy.v1 helm.sh/release.v1 1 2m
secret/sh.helm.release.v1.capsule.v1 helm.sh/release.v1 1 4m11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/capsule-controller-manager-metrics-service ClusterIP 10.0.213.12 <none> 8080/TCP 4m11s
service/capsule-proxy LoadBalancer 10.0.2.95 20.199.4.73 443:30350/TCP 2m
service/capsule-proxy-metrics-service ClusterIP 10.0.163.163 <none> 8080/TCP 2m
service/capsule-webhook-service ClusterIP 10.0.9.76 <none> 443/TCP 4m11s
curl -k https://capsule-proxy.francecentral.cloudapp.azure.com
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"cannot retrieve user and group: unauthenticated users not supported","reason":"Forbidden","code":403}%
6.4. Création des Tenants
6.4.1. DEV
CAPSULE_DEV_USER_ID=$(az ad user list --upn capsule-user-dev@microsoftalterway.onmicrosoft.com -o tsv --query "[0].id")
echo $CAPSULE_DEV_USER_ID
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: dev
spec:
owners:
- name: ${CAPSULE_DEV_GROUP_ID}
kind: Group
EOF
✅ tenant.capsule.clastix.io/dev created
6.4.2. Staging
CAPSULE_STAGING_USER_ID=$(az ad user list --upn capsule-user-staging@microsoftalterway.onmicrosoft.com -o tsv --query "[0].id")
echo $CAPSULE_STAGING_USER_ID
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: staging
spec:
owners:
- name: ${CAPSULE_STAGING_GROUP_ID}
kind: Group
EOF
✅ tenant.capsule.clastix.io/staging created
👀 Contrôle :
kubectl get tenants.capsule.clastix.io
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
dev Active 0 1m39s
staging Active 0 5s
6.5. Utilisation des Tenants
Nous allons maintenant accéder au cluster avec l'utilisateur capsule-user-dev
qui est attaché au tenant dev
via le groupe capsuleDevGroup
Je vous conseille de faire la manipulation suivante avant de commencer, pour être sur le bon utilisateur si vous utiliser le même compte utilisateur sur votre machine. Vous n'avez pas à le faire si vous vous connectez avec un autre utilisateur sur votre machine locale.
az logout
kubelogin remove-tokens
6.5.1. Récupération du kubeconfig
pour les utilisateurs dev
az login
az aks get-credentials --resource-group aw-capsule --name aw-capsule --file ~/.kube/dev-capsule-config
✅ Vous allez devoir vous connecter avec le user capsule-user-dev
export KUBECONFIG=~/.kube/dev-capsule-config
👀 Contrôle :
❯ az logout
❯ kubelogin remove-tokens
❯ az login
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
CloudName HomeTenantId IsDefault Name State TenantId
----------- ------------------------------------ ----------- ----------------------- ------- ------------------------------------
AzureCloud xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx True Conso Interne Alter Way Enabled xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
❯ az aks get-credentials --resource-group aw-capsule --name aw-capsule --file ~/.kube/dev-capsule-config
Merged "aw-capsule" as current context in /Users/hleclerc/.kube/dev-capsule-config
6.5.2. Utilisation de l'utilisateur capsule-user-dev
Si vous tapez une commande kubectl ...
, vous allez devoir vous identifier pour pouvoir utiliser le cluster kubernetes via l'api publique de celui-ci.
❯ export KUBECONFIG=~/.kube/dev-capsule-config
❯ kubectl get po
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code G7RCDSJW8 to authenticate.
Essayez une commande toute simple
❯ kubectl get po
Error from server (Forbidden): pods is forbidden: User "capsule-user-dev@microsoftalterway.onmicrosoft.com" cannot list resource "pods" in API group "" in the namespace "default": User does not have access to the resource in Azure. Update role assignment to allow access.
Cette erreur est normale car dans le tenant dev
vous n'avez pas accès au namespace default
car celui-ci n'appartient pas au tenant.
par contre si vous faîtes la commande :
❯ kubectl create ns dev-demo
namespace/dev-demo created
Il n'y a pas d'erreur car vous êtes dans le tenant dev
vous êtes le propriétaire de ce tanant et vous venez de créer un namespace dev-demo
💡 Note : Capsule vous force à préfixer tous les noms de namespace par dev-
Si vous tapez la commande :
❯ kubectl get ns
Error from server (Forbidden): namespaces is forbidden: User "capsule-user-dev@microsoftalterway.onmicrosoft.com" cannot list resource "namespaces" in API group "" at the cluster scope: User does not have access to the resource in Azure. Update role assignment to allow access.
C'est pour cela que nous allons utiliser le capsule-proxy pour pouvoir lister les objets qui n'ont pas de namespace.
Nous allons manipuler le fichier kubeconfig
pour changer le fqdn du serveur et pointer sur le proxy : aw-capsule-proxy.francecentral.cloudapp.azure.com par exemple https://capsule-proxy.caas.fr:443
# Récupérer le nom du cluster
❯ kubectl config get-clusters
# Modification de l'url de l'api
❯ kubectl config set-cluster aw-capsule --server=https://capsule-proxy.caas.fr:443
# Recupérer le nom du user
❯ kubectl config get-users
# Modification du user si besoin dans le context (clusterUser_aw-capsule_aw-capsule)
kubectl config set-context aw-capsule --cluster=aw-capsule --user=clusterUser_aw-capsule_aw-capsule
Si vous tapez la commande par exemple
❯ kubectl get ns
Vous allez certainement avoir une erreur Unable to connect to the server: tls: failed to verify certificate: x509: certificate signed by unknown authority
.
Pour éviter ca utilisez le flag --insecure-skip-tls-verify
.
❯ kubectl get ns --insecure-skip-tls-verify
I0928 09:10:45.534157 3741 versioner.go:58] Get https://capsule-proxy.francecentral.cloudapp.azure.com:443/version?timeout=5s: x509: certificate signed by unknown authority
NAME STATUS AGE
dev-demo Active 17m
Et voila 😀😏💪 !
Bravo ! Vous avez votre premier namespace dans le tenant dev
.
Vous ne voyez que les composants qui appartiennent à votre tenant.
💡 Note :
J'ai utilisé certbot
pour rapidement générer les certificats pour le fqdn capsule-proxy.caas.fr
.
certbot -d capsule-proxy.caas.fr --manual --preferred-challenges dns certonly
Si vous ne voulez pas avoir l'erreur X509 il vous faudra remplacer certificate-authority-data
par la valeur du tls.cert
que vous avez utilisé (en base64)
Moi j'ai pris la fullchain.pem que certbot m'a généré
ensuite il faut mettre à jour
- Le secret de capsule-proxy dans le ns capsule-system
❯ kubectl delete secret capsule-proxy
❯ kubectl -n capsule-system create secret tls capsule-proxy --cert=./tls.crt --key=./tls.key
- capsule proxy :
helm upgrade --install capsule-proxy clastix/capsule-proxy \
-n capsule-system \
--set service.type=LoadBalancer \
--set service.port=443 \
--set options.oidcUsernameClaim=unique_name \
--set "options.ignoredUserGroups[0]=$CAPSULE_ADMIN_GROUP_ID" \
--set "options.additionalSANs[0]=capsule-proxy.caas.fr" \
--set service.annotations."service\.beta\.kubernetes\.io/azure-dns-label-name"=capsule-proxy \
--set options.generateCertificates=false
CA devrait être bon plus d'erreur de certicat
Gestion des règles au niveau d'un tenant
Network policy
Si on défini des network policy au niveau du tenant elles seront propagées à tous les namespaces du tenant.
un exemple :
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: dev
spec:
ingressOptions:
hostnameCollisionScope: Disabled
networkPolicies:
items:
- egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ingress:
- from:
- namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: dev
podSelector:
matchLabels:
policyTypes:
- Ingress
- Egress
owners:
- clusterRoles:
- admin
- capsule-namespace-deleter
kind: Group
name: 0fd5e5bb-39bb-468c-b343-dcdfb01e92d0
resourceQuotas:
scope: Tenant
Si on applique les modifications au tenant dev alors
- Seuls les pods des namespaces du tenant pourrons discuter entre et consommer d'autre "services" interne ou externe au cluster
- Aucun pod ne pourras accéder au pods du tenant dev
On voit bien l'ajout des network policy automatiquement par le contrôleur capsule.
❯ kubectl get netpol -A
NAMESPACE NAME POD-SELECTOR AGE
dev-app capsule-dev-0 <none> 7m59s
dev-demo capsule-dev-0 <none> 7m45s
kube-system konnectivity-agent app=konnectivity-agent 8h
Vous pouvez voir ici une vidéo de démonstration sur l'exemple des network policy
Références
- Site Web de Capsule: https://clastix.io/capsule/
- Documentation de Capsule: https://capsule.clastix.io/docs/
- Dépôt GitHub de Capsule: https://github.com/clastix/capsule
Découvrez les derniers articles d'alter way
- : Prowler : L'outil de sécurité multi-cloud indispensable pour renforcer votre infrastructure
- : Kubernetes : plateforme "star" de l'IT et levier d'innovation des entreprises
- AI_dev2024
- : DirectPV : Avoir du stockage bloc distribué facilement dans kubernetes
- : Simple comme GitOps : kluctl
- Conférence Wax 2024 @thecamp