déployer des applications avec kapp-controller
Publié le :
kapp-controller est une extension de
kapp qui
permet d’installer et de mettre à jour vos applications et packages sur vos
clusters Kubernetes. Il fait partie de la suite Carvel.
kapp-controller vous laisse le choix de vos outils de configuration et de vos
sources de configuration : configurations YAML simples, graphiques Helm, modèles
ytt, modèles jsonnet, … chargé depuis un référentiel Git, une archive sur
HTTP, un référentiel Helm, etc
Fonctionnement de kapp-controller
On retrouve le même principe de fonctionnement que les systèmes de packages
Linux (apt, yum, …). En effet, kapp-controller décompose l’installation d’un
package d’une application en trois étapes :
- Fetch : récupération des définitions des sources de l’application depuis le package repository.
- Template: applications des valeurs fournies par l’utilisateur pour personnaliser le logiciel en utilisant des templates ytt, helm, …
- Deploy : création ou mise à jour des ressources sur le cluster
kapp-controller utilise des images crées avec l’outil de la suite Carvel
répondant au nom de imgpkg. Nous en verrons l’utilisation plus bas dans le
tutoriel.
Installation de kapp-controller sur votre cluster
Pour notre tutoriel je vais avoir besoin d’une registry docker. Je la crée en locale :
docker run -d -p 5000:5000 --restart=always --name registry registry:2export REPO_HOST="`ifconfig | grep -A1 docker | grep inet | cut -f10 -d' '`:5000"echo $REPO_HOST172.17.0.1:5000L’installation de kapp-controller se fait simplement avec kapp. Pour ce
tutoriel j’ai utilisé un cluster créé avec
kind.
kapp deploy -a kc -f https://github.com/vmware-tanzu/carvel-kapp-controller/releases/latest/download/release.ymlTarget cluster 'https://127.0.0.1:38717' (nodes: master1, 1+)
Apps in all namespaces
Namespace  Name  Namespaces                             Lcs   Lcadefault    kc    (cluster),kapp-controller,kube-system  true  15m
Lcs: Last Change SuccessfulLca: Last Change Age
1 appsUn pti coup d’inspect :
kapp inspect -a kc --treeTarget cluster 'https://127.0.0.1:38717' (nodes: master1, 1+)
Resources in app 'kc'
Namespace        Name                                                    Kind                      Owner    Conds.  Rs  Ri  Age(cluster)        pkg-apiserver:system:auth-delegator                     ClusterRoleBinding        kapp     -       ok  -   17m(cluster)        kapp-controller-cluster-role                            ClusterRole               kapp     -       ok  -   17m(cluster)        kapp-controller-cluster-role-binding                    ClusterRoleBinding        kapp     -       ok  -   17m(cluster)        internalpackagemetadatas.internal.packaging.carvel.dev  CustomResourceDefinition  kapp     2/2 t   ok  -   17mkube-system      pkgserver-auth-reader                                   RoleBinding               kapp     -       ok  -   17mkapp-controller  packaging-api                                           Service                   kapp     -       ok  -   17mkapp-controller   L packaging-api                                        Endpoints                 cluster  -       ok  -   17mkapp-controller   L packaging-api-rmftl                                  EndpointSlice             cluster  -       ok  -   17m(cluster)        internalpackages.internal.packaging.carvel.dev          CustomResourceDefinition  kapp     2/2 t   ok  -   17m(cluster)        packageinstalls.packaging.carvel.dev                    CustomResourceDefinition  kapp     2/2 t   ok  -   17m(cluster)        v1alpha1.data.packaging.carvel.dev                      APIService                kapp     1/1 t   ok  -   17m(cluster)        packagerepositories.packaging.carvel.dev                CustomResourceDefinition  kapp     2/2 t   ok  -   17m(cluster)        kapp-controller-packaging-global                        Namespace                 kapp     -       ok  -   17mkapp-controller  kapp-controller-sa                                      ServiceAccount            kapp     -       ok  -   17m(cluster)        apps.kappctrl.k14s.io                                   CustomResourceDefinition  kapp     2/2 t   ok  -   17m(cluster)        kapp-controller                                         Namespace                 kapp     -       ok  -   17mkapp-controller  kapp-controller                                         Deployment                kapp     2/2 t   ok  -   17mkapp-controller   L kapp-controller-79bd7d495                            ReplicaSet                cluster  -       ok  -   17mkapp-controller   L.. kapp-controller-79bd7d495-c4jbh                    Pod                       cluster  4/4 t   ok  -   17mkapp-controller   L kapp-controller-79bd7d495-c4jbh                      PodMetrics                cluster  -       ok  -   0s
Rs: Reconcile stateRi: Reconcile information
20 resourcesOn voit tout ce qui a été déployé pour kapp-controller :
- des CRD : internalpackagemetadatas.internal.packaging.carvel.dev, internalpackages.internal.packaging.carvel.dev, packageinstalls.packaging.carvel.dev, apps.kappctrl.k14s.io et packagerepositories.packaging.carvel.dev
- une API : v1alpha1.data.packaging.carvel.dev
- un namespace : kapp-controller
- un Service Account : kapp-controller-sa
- un deployment : kapp-controller
Déployer une application avec kapp-controller
Je reprends les mêmes exemples fournis sur le projet de kapp ↗.
cd /tmpgit clone https://github.com/vmware-tanzu/carvel-simple-app-on-kubernetescd config-step-2-templateNotre exemple se trouve dans le répertoire config-step-2-template où on y
retrouve deux fichiers au format ytt, c’est-à-dire un template :
- config.yaml : qui définit un service et un deployment
- values.yml : qui contient les valeurs qui seront appliquées par ytt
Pour ceux qui ne connaissent pas le langage de template ytt je vous renvoie à
mon billet.
Création du package de l’application avec imgpkg
Nous allons utiliser imgpgk pour construire notre package d’application et
pour le pousser dans la registry locale.
Nous allons créer la structure qui contiendra notre package d’application et y déposer les fichiers nécessaires :
mkdir -p package-contents/config/cp config.yml package-contents/config/config.ymlcp values.yml package-contents/config/values.ymlmkdir -p package-contents/.imgpkgimgpkg utilise d’un répertoire .imgpkg pour y déposer ses définitions. On
peut y retrouver deux fichiers : bundle.yml qui contient des métadatas
(optionnel) et images.yml qui référence les images utilisées dans le package.
Pour construire ces images, nous allons utiliser kbld qui offre cette
fonctionnalité. Pour rappel,
kbld scrute les metadatas d’une
ressource kubernetes pour y retrouver les images utilisées par les pods pour les
rendre immutable en lui accolant un SHA. Ici ce SHA existe déjà, regardez le
contenu du fichier config.yml. Supprimez le avant et vous verrez il l’ajoutera.
kbld -f package-contents/config/ --imgpkg-lock-output package-contents/.imgpkg/images.yml
resolve | final: docker.io/dkalinin/k8s-simple-app -> index.docker.io/dkalinin/k8s-simple-app@sha256:4c8b96d4fffdfae29258d94a22ae4ad1fe36139d47288b8960d9958d1e63a9d0---simple-app: ""---apiVersion: v1kind: Servicemetadata:  name: simple-app  namespace: defaultspec:  ports:  - port: null    targetPort: null  selector: null---apiVersion: apps/v1kind: Deploymentmetadata:  annotations:    kbld.k14s.io/images: |      - origins:        - resolved:            tag: latest            url: docker.io/dkalinin/k8s-simple-app        url: index.docker.io/dkalinin/k8s-simple-app@sha256:4c8b96d4fffdfae29258d94a22ae4ad1fe36139d47288b8960d9958d1e63a9d0  name: simple-app  namespace: defaultspec:  selector:    matchLabels: null  template:    metadata:      labels: null    spec:      containers:      - env:        - name: HELLO_MSG          value: null        image: index.docker.io/dkalinin/k8s-simple-app@sha256:4c8b96d4fffdfae29258d94a22ae4ad1fe36139d47288b8960d9958d1e63a9d0        name: simple-app---app_port: 80hello_msg: strangersvc_port: 80Que contient ce fichier images.yml :
---apiVersion: imgpkg.carvel.dev/v1alpha1images:- annotations:    kbld.carvel.dev/id: docker.io/dkalinin/k8s-simple-app    kbld.carvel.dev/origins: |      - resolved:          tag: latest          url: docker.io/dkalinin/k8s-simple-app  image: index.docker.io/dkalinin/k8s-simple-app@sha256:4c8b96d4fffdfae29258d94a22ae4ad1fe36139d47288b8960d9958d1e63a9d0kind: ImagesLockMaintenant poussons l’image dans la registry avec imgpkg :
imgpkg push -b ${REPO_HOST}/packages/simple-app:1.0.0 -f package-contents/dir: .dir: .imgpkgfile: .imgpkg/images.ymldir: configfile: config/config.ymlfile: config/values.ymlPushed '172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145'SucceededVérifions ce que contient notre registry locale :
curl ${REPO_HOST}/v2/_catalog{"repositories":["packages/simple-app"]}On a bien notre image.
Création des définitions de notre application
Pour déployer notre application avec kapp-controller nous devons définir deux
objets : un Package et un PackageMetadata.
Commençons par le PackageMetadata :
cat > metadata.yml << EOFapiVersion: data.packaging.carvel.dev/v1alpha1kind: PackageMetadatametadata:  # This will be the name of our package  name: simple-app.corp.comspec:  displayName: "Simple App"  longDescription: "Simple app consisting of a k8s deployment and service"  shortDescription: "Simple app for demoing"  categories:  - demoEOFIl ne contient comme attendu que des metadatas qui sera utilisé par
kapp-controller lors du fetch.
Ensuite créons la déclaration du Package. Nous allons utiliser ytt comme système de template et donc nous allons devoir créer une ressource de type schema au format openAPI :
ytt -f values.yml --data-values-schema-inspect -o openapi-v3 > schema-openapi.ymlVérifions ce qu’il contient :
openapi: 3.0.0info:  version: 0.1.0  title: Schema for data values, generated by yttpaths: {}components:  schemas:    dataValues:      nullable: true      default: nullCréons maintenant le package :
cat > package-template.yml << EOF#@ load("@ytt:data", "data")  # for reading data values (generated via ytt's data-values-schema-inspect mode).#@ load("@ytt:yaml", "yaml")  # for dynamically decoding the output of ytt's data-values-schema-inspect---apiVersion: data.packaging.carvel.dev/v1alpha1kind: Packagemetadata:  name: #@ "simple-app.corp.com." + data.values.versionspec:  refName: simple-app.corp.com  version: #@ data.values.version  releaseNotes: |        Initial release of the simple app package  valuesSchema:    openAPIv3: #@ yaml.decode(data.values.openapi)["components"]["schemas"]["dataValues"]  template:    spec:      fetch:      - imgpkgBundle:          image: #@ "${REPO_HOST}/packages/simple-app:" + data.values.version      template:      - ytt:          paths:          - "config/"      - kbld:          paths:          - "-"          - ".imgpkg/images.yml"      deploy:      - kapp: {}EOFVous allez me dire “outch” bien difficile à comprendre. Attendez ça va s’éclaircir par la suite. Ça me rappelle la première fois ou j’ai du créer un package yum.
Création du Package Repository
Pour rappel, Un Package Repository est l’endroit où nous allons déposer nos applications et ses metadatas et dans lequel kapp-controller vient puiser pour les déployer.
Commençons par créer les répertoires nécessaires :
lltotal 16K-rw-r--r--. 1 vagrant vagrant 639 Jan 28 08:45 config.yml-rw-r--r--. 1 vagrant vagrant 325 Jan 28 09:09 metadata.yml-rw-r--r--. 1 vagrant vagrant 178 Jan 28 09:03 schema-openapi.yml-rw-r--r--. 1 vagrant vagrant  64 Jan 27 06:48 values.yml
mkdir -p my-pkg-repo/.imgpkg my-pkg-repo/packages/simple-app.corp.comLançons la création du package avec ytt en indiquant la version que nous voulons créer :
ytt -f package-template.yml --data-value-file openapi=schema-openapi.yml -v version="1.0.0" > my-pkg-repo/packages/simple-app.corp.com/1.0.0.ymlcat my-pkg-repo/packages/simple-app.corp.com/1.0.0.ymlapiVersion: data.packaging.carvel.dev/v1alpha1kind: Packagemetadata:  name: simple-app.corp.com.1.0.0spec:  refName: simple-app.corp.com  version: 1.0.0  releaseNotes: |    Initial release of the simple app package  valuesSchema:    openAPIv3:      nullable: true      default: null  template:    spec:      fetch:      - imgpkgBundle:          image: 172.17.0.1:5000/packages/simple-app:1.0.0      template:      - ytt:          paths:          - config/      - kbld:          paths:          - '-'          - .imgpkg/images.yml      deploy:      - kapp: {}Un petit coup de kbld pour créer notre image de repo :
kbld -f my-pkg-repo/packages/ --imgpkg-lock-output my-pkg-repo/.imgpkg/images.yml
resolve | final: 172.17.0.1:5000/packages/simple-app:1.0.0 -> 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145---apiVersion: data.packaging.carvel.dev/v1alpha1kind: Packagemetadata:  annotations:    kbld.k14s.io/images: |      - origins:        - resolved:            tag: 1.0.0            url: 172.17.0.1:5000/packages/simple-app:1.0.0        url: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145  name: simple-app.corp.com.1.0.0spec:  refName: simple-app.corp.com  releaseNotes: |    Initial release of the simple app package  template:    spec:      deploy:      - kapp: {}      fetch:      - imgpkgBundle:          image: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145      template:      - ytt:          paths:          - config/      - kbld:          paths:          - '-'          - .imgpkg/images.yml  valuesSchema:    openAPIv3:      default: null      nullable: true  version: 1.0.0---apiVersion: data.packaging.carvel.dev/v1alpha1kind: PackageMetadatametadata:  name: simple-app.corp.comspec:  categories:  - demo  displayName: Simple App  longDescription: Simple app consisting of a k8s deployment and service  shortDescription: Simple app for demoing
SucceededNormalement on doit retrouver le fichier images.yml dans le répertoire .imgpkg
cat  my-pkg-repo/.imgpkg/images.yml---apiVersion: imgpkg.carvel.dev/v1alpha1images:- annotations:    kbld.carvel.dev/id: 172.17.0.1:5000/packages/simple-app:1.0.0    kbld.carvel.dev/origins: |      - resolved:          tag: 1.0.0          url: 172.17.0.1:5000/packages/simple-app:1.0.0  image: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145kind: ImagesLockPoussons notre repository dans la registry :
imgpkg push -b ${REPO_HOST}/packages/my-pkg-repo:1.0.0 -f my-pkg-repodir: .dir: .imgpkgfile: .imgpkg/images.ymldir: packagesdir: packages/simple-app.corp.comfile: packages/simple-app.corp.com/1.0.0.ymlfile: packages/simple-app.corp.com/metadata.ymlPushed '172.17.0.1:5000/packages/my-pkg-repo@sha256:55104b57562a354cb9b5c5ed537fdc221a2c94f575b18dea6b60fa7951b2dee0'Succeededcurl ${REPO_HOST}/v2/_catalog
{"repositories":["packages/my-pkg-repo","packages/simple-app"]}Déploiement du repository sur le cluster
Pour le moment nous avons préparé le terrain en créant toutes les images : celle
du package de l’application et celle du repository. Maintenant il faut créer le
repository physiquement au sein du cluster. Pour cela il faut utiliser la CRD
PackageRepository :
cat > repo.yml << EOF---apiVersion: packaging.carvel.dev/v1alpha1kind: PackageRepositorymetadata:  name: simple-package-repositoryspec:  fetch:    imgpkgBundle:      image: ${REPO_HOST}/packages/my-pkg-repo:1.0.0EOFPour le déployer nous allons utiliser kapp qui va nous permettre de voir ce qui
se passe :
kapp deploy -a repo -f repo.yml -y
Target cluster 'https://127.0.0.1:38717' (nodes: kind-control-plane)
Changes
Namespace  Name                       Kind               Conds.  Age  Op      Op st.  Wait to    Rs  Ridefault    simple-package-repository  PackageRepository  -       -    create  -       reconcile  -   -
Op:      1 create, 0 delete, 0 update, 0 noop, 0 existsWait to: 1 reconcile, 0 delete, 0 noop
9:38:20AM: ---- applying 1 changes [0/1 done] ----9:38:20AM: create packagerepository/simple-package-repository (packaging.carvel.dev/v1alpha1) namespace: default9:38:20AM: ---- waiting on 1 changes [0/1 done] ----9:38:20AM: ongoing: reconcile packagerepository/simple-package-repository (packaging.carvel.dev/v1alpha1) namespace: default9:38:20AM:  ^ Waiting for generation 1 to be observed9:38:21AM: ongoing: reconcile packagerepository/simple-package-repository (packaging.carvel.dev/v1alpha1) namespace: default9:38:21AM:  ^ Reconciling9:38:22AM: ok: reconcile packagerepository/simple-package-repository (packaging.carvel.dev/v1alpha1) namespace: default9:38:22AM: ---- applying complete [1/1 done] ----9:38:22AM: ---- waiting complete [1/1 done] ----
SucceededOn en sort avec succès. Mais que se passe-t-il ? Nous application est-elle déjà déployée ? Non, car nous avons juste créé le repository.
Utilisons la commande kubectl pour voir ce que nous affiche la ressource
PackageRepository que nous venons de créer :
kubectl get packagerepositoryNAME                        AGE     DESCRIPTIONsimple-package-repository   6m17s   Reconcile succeededMaintenant vérifions ce que contient notre repository :
kubectl get packagemetadatasNAME                  DISPLAY NAME   CATEGORIES   SHORT DESCRIPTION        AGEsimple-app.corp.com   Simple App     demo         Simple app for demoing   9m41sOn y trouve notre application. Maintenant quelle version de l’application disposons-nous ?
kubectl get packages --field-selector spec.refName=simple-app.corp.comNAME                        PACKAGEMETADATA NAME   VERSION   AGEsimple-app.corp.com.1.0.0   simple-app.corp.com    1.0.0     11m17sNotre version 1.0.0. Mais que contient-elle ?
kubectl get package simple-app.corp.com.1.0.0 -o yamlapiVersion: data.packaging.carvel.dev/v1alpha1kind: Packagemetadata:  annotations:    kapp.k14s.io/disable-original: ""    kapp.k14s.io/disable-wait: ""    kapp.k14s.io/identity: v1;default/data.packaging.carvel.dev/Package/simple-app.corp.com.1.0.0;data.packaging.carvel.dev/v1alpha1    kbld.k14s.io/images: |      - origins:        - resolved:            tag: 1.0.0            url: 172.17.0.1:5000/packages/simple-app:1.0.0        - preresolved:            url: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145        url: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145  creationTimestamp: "2022-01-28T10:41:22Z"  generation: 1  labels:    kapp.k14s.io/app: "1643366480557105100"    kapp.k14s.io/association: v1.fc22fec9fc12e7d2a6c2562942b75674  name: simple-app.corp.com.1.0.0  namespace: default  resourceVersion: "7383"  uid: 3375ab8b-adb5-45b3-aec2-f9be67c1a7ccspec:  refName: simple-app.corp.com  releaseNotes: |    Initial release of the simple app package  releasedAt: null  template:    spec:      deploy:      - kapp: {}      fetch:      - imgpkgBundle:          image: 172.17.0.1:5000/packages/simple-app@sha256:fe88086f7737e1bf834786856edb2bab5e153dc470479b114e58174763472145      template:      - ytt:          paths:          - config/      - kbld:          paths:          - '-'          - .imgpkg/images.yml  valuesSchema:    openAPIv3:      default: null      nullable: true  version: 1.0.0Tout simplement toutes les informations nécessaires à son déploiement : les phases : fetch, template et deploy.
Déploiement de l’application sur le cluster
Maintenant que nous avons notre repository d’applications contenant notre application, voyons comment la déployer. Comme pour les autres phases un yaml est nécessaire (IAC bien) :
cat > pkginstall.yml << EOF---apiVersion: packaging.carvel.dev/v1alpha1kind: PackageInstallmetadata:  name: pkg-demospec:  serviceAccountName: default-ns-sa  packageRef:    refName: simple-app.corp.com    versionSelection:      constraints: 1.0.0  values:  - secretRef:      name: pkg-demo-values---apiVersion: v1kind: Secretmetadata:  name: pkg-demo-valuesstringData:  values.yml: |    ---    hello_msg: "to all my katacoda friends"EOFOn y retrouve l’appel à la référence CRD packageinstalls qui utilise une spec de type packageRef qui indique le nom du package (refname) et sa version à utiliser :
  packageRef:    refName: simple-app.corp.com    versionSelection:      constraints: 1.0.0Vous avez certainement remarqué la création d’un secret qui vient définir le fichier “values.yml” utilisé par le package et d’un service account qu’il faut déployer avant.
kapp deploy -a default-ns-rbac -f https://raw.githubusercontent.com/vmware-tanzu/carvel-kapp-controller/develop/examples/rbac/default-ns.yml -yDéployons maintenant notre application :
kapp deploy -a pkg-demo -f pkginstall.yml -ySi comme moi la première fois vous avez des erreurs la commande kapp inspect
avec l’option --status vous indiquera la cause :
kapp inspect -a pkg-demo --statusTarget cluster 'https://127.0.0.1:38717' (nodes: kind-control-plane)
Resources in app 'pkg-demo'
Namespace  defaultName       pkg-demoKind       PackageInstallStatus     conditions:           - message: Error (see .status.usefulErrorMessage for details)             status: "True"             type: ReconcileFailed           friendlyDescription: 'Reconcile failed: Error (see .status.usefulErrorMessage for             details)'           lastAttemptedVersion: 1.0.0           observedGeneration: 2           usefulErrorMessage: |             ytt: Error: Checking file '/etc/kappctrl-mem-tmp/kapp-controller-fetch-template-deploy3698766503/0/config': lstat /etc/kappctrl-mem-tmp/kapp-controller-fetch-template-deploy3698766503/0/config: no such file or directory           version: 1.0.0
Namespace  defaultName       pkg-demo-valuesKind       SecretStatus     -
2 resources
SucceededUne fois corrigé, dans mon cas, j’avais oublié le fichier de config dans le package de l’application on peut vérifier que notre application est bien déployé.
Target cluster 'https://127.0.0.1:38717' (nodes: kind-control-plane)
Resources in app 'pkg-demo'
Namespace  Name             Kind            Owner  Conds.  Rs  Ri  Agedefault    pkg-demo         PackageInstall  kapp   1/1 t   ok  -   6m^          pkg-demo-values  Secret          kapp   -       ok  -   6m
Rs: Reconcile stateRi: Reconcile information
2 resources
Succeeded
kubectl get deployments.apps simple-appNAME         READY   UP-TO-DATE   AVAILABLE   AGEsimple-app   1/1     1            1           7m31s
kubectl get poddefault              simple-app-58fb574f5c-55nl4                  1/1     Running   0          7m3s
kubectl get serviceNAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGEkubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   75msimple-app   ClusterIP   10.96.0.46   <none>        80/TCP    8m32s
docker exec -it kind-control-plane bashroot@kind-control-plane:/# curl http://10.96.0.46<h1>Hello to all my katacoda friends!</h1>Notre application est bien déployée et fonctionne.
Conclusion
Franchement j’en sors content, car j’ai bien compris le principe. Mais je me pose quelques questions comme :
- Est-il possible d’utiliser des repos externes ? Car tout mettre dans le même cluster n’est pas dans mes principes.
- Le debug de la partie déploiement est-il facile ? Là, je m’en suis sorti, mais dans des cas plus complexes ???
- Comment industrialiser tout ça ?
- Comment l’utiliser avec une démarche GitOps ? Car pour le moment je n’ai pas vu trace d’utilisation de repository git.
Voilà ça demande encore un peu de travail pour l’adopter comme gestionnaire d’applications. Et vous donnez moi en commentaires vos réflexions.
Dans le repo git de la version community edition de Tangzu ↗ on retrouve une vingtaine d’applications utilisant kapp-controller.
