From e820ca466f784eb349a97232171769a32dc73650 Mon Sep 17 00:00:00 2001 From: Ludovic Piot Date: Tue, 21 Oct 2025 13:21:16 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=95=20Add=20Flux=20(M5B/M6)=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- slides/flux/add-cluster.md | 126 ++++++++++ slides/flux/app1-rocky-test.md | 417 +++++++++++++++++++++++++++++++++ slides/flux/app2-movy-test.md | 320 +++++++++++++++++++++++++ slides/flux/bootstrap.md | 410 ++++++++++++++++++++++++++++++++ slides/flux/ingress.md | 284 ++++++++++++++++++++++ slides/flux/kyverno.md | 241 +++++++++++++++++++ slides/flux/observability.md | 251 ++++++++++++++++++++ slides/flux/openebs.md | 129 ++++++++++ slides/flux/scenario.md | 354 ++++++++++++++++++++++++++++ slides/flux/tenants.md | 200 ++++++++++++++++ slides/images/konnectivity.png | Bin 0 -> 72821 bytes slides/k8s/k0s.md | 390 ++++++++++++++++++++++++++++++ 12 files changed, 3122 insertions(+) create mode 100644 slides/flux/add-cluster.md create mode 100644 slides/flux/app1-rocky-test.md create mode 100644 slides/flux/app2-movy-test.md create mode 100644 slides/flux/bootstrap.md create mode 100644 slides/flux/ingress.md create mode 100644 slides/flux/kyverno.md create mode 100644 slides/flux/observability.md create mode 100644 slides/flux/openebs.md create mode 100644 slides/flux/scenario.md create mode 100644 slides/flux/tenants.md create mode 100644 slides/images/konnectivity.png create mode 100644 slides/k8s/k0s.md diff --git a/slides/flux/add-cluster.md b/slides/flux/add-cluster.md new file mode 100644 index 00000000..e101fa8e --- /dev/null +++ b/slides/flux/add-cluster.md @@ -0,0 +1,126 @@ + +## Flux install + +We'll install `Flux`. +And replay the all scenario a 2nd time. +Let's face it: we don't have that much time. 😅 + +Since all our install and configuration is `GitOps`-based, we might just leverage on copy-paste and code configuration… +Maybe. + +Let's copy the 📂 `./clusters/CLOUDY` folder and rename it 📂 `./clusters/METAL`. + +--- + +### Modifying Flux config 📄 files + +- In 📄 file `./clusters/METAL/flux-system/gotk-sync.yaml` +
change the `Kustomization` value `spec.path: ./clusters/METAL` + - ⚠️ We'll have to adapt the `Flux` _CLI_ command line + +- And that's pretty much it! + - We'll see if anything goes wrong on that new cluster + +--- + +### Connecting to our dedicated `Github` repo to host Flux config + +.lab[ + +- let's replace `GITHUB_TOKEN` and `GITHUB_REPO` values +- don't forget to change the patch to `clusters/METAL` + +```bash +k8s@shpod:~$ export GITHUB_TOKEN="my-token" && \ + export GITHUB_USER="container-training-fleet" && \ + export GITHUB_REPO="fleet-config-using-flux-XXXXX" + +k8s@shpod:~$ flux bootstrap github \ + --owner=${GITHUB_USER} \ + --repository=${GITHUB_REPO} \ + --team=OPS \ + --team=ROCKY --team=MOVY \ + --path=clusters/METAL +``` +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### Flux deployed our complete stack + +Everything seems to be here but… + +- one database is in `Pending` state + +- our `ingresses` don't work well + +```bash +k8s@shpod ~$ curl --header 'Host: rocky.test.enixdomain.com' http://${myIngressControllerSvcIP} +curl: (52) Empty reply from server +``` + +--- + +### Fixing the Ingress + +The current `ingress-nginx` configuration leverages on specific annotations used by Scaleway to bind a _IaaS_ load-balancer to the `ingress-controller`. +We don't have such kind of things here.😕 + +- We could bind our `ingress-controller` to a `NodePort`. +`ingress-nginx` install manifests propose it here: +
https://github.com/kubernetes/ingress-nginx/deploy/static/provider/baremetal + +- In the 📄file `./clusters/METAL/ingress-nginx/sync.yaml`, +
change the `Kustomization` value `spec.path: ./deploy/static/provider/baremetal` + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### Troubleshooting the database + +One of our `db-0` pod is in `Pending` state. + +```bash +k8s@shpod ~$ k get pods db-0 -n *-test -oyaml +(…) +status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2025-06-11T11:15:42Z" + message: '0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims. + preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.' + reason: Unschedulable + status: "False" + type: PodScheduled + phase: Pending + qosClass: Burstable +``` + +--- + +### Troubleshooting the PersistentVolumeClaims + +```bash +k8s@shpod ~$ k get pvc postgresql-data-db-0 -n *-test -o yaml +(…) + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal FailedBinding 9s (x182 over 45m) persistentvolume-controller no persistent volumes available for this claim and no storage class is set +``` + +No `storage class` is available on this cluster. +We hadn't the problem on our managed cluster since a default storage class was configured and then associated to our `PersistentVolumeClaim`. + +Why is there no problem with the other database? + diff --git a/slides/flux/app1-rocky-test.md b/slides/flux/app1-rocky-test.md new file mode 100644 index 00000000..22b5d950 --- /dev/null +++ b/slides/flux/app1-rocky-test.md @@ -0,0 +1,417 @@ +# R01- Configuring **_🎸ROCKY_** deployment with Flux + +The **_⚙️OPS_** team manages 2 distinct envs: **_⚗️TEST_** et _**🚜PROD**_ + +Thanks to _Kustomize_ + 1. it creates a **_base_** common config + 2. this common config is overwritten with a **_⚗️TEST_** _tenant_-specific configuration + 3. the same applies with a _**🚜PROD**_-specific configuration + +> 💡 This seems complex, but no worries: Flux's CLI handles most of it. + +--- + +## Creating the **_🎸ROCKY_**-dedicated _tenant_ in **_⚗️TEST_** env + +- Using the `flux` _CLI_, we create the file configuring the **_🎸ROCKY_** team's dedicated _tenant_… +- … this file takes place in the `base` common configuration for both envs + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + mkdir -p ./tenants/base/rocky && \ + flux create tenant rocky \ + --with-namespace=rocky-test \ + --cluster-role=rocky-full-access \ + --export > ./tenants/base/rocky/rbac.yaml +``` + +] + +--- + +class: extra-details + +### 📂 ./tenants/base/rocky/rbac.yaml + +Let's see our file… + +3 resources are created: `Namespace`, `ServiceAccount`, and `ClusterRoleBinding` + +`Flux` **impersonates** as this `ServiceAccount` when it applies any resources found in this _tenant_-dedicated source(s) + +- By default, the `ServiceAccount` is bound to the `cluster-admin` `ClusterRole` +- The team maintaining the sourced `Github` repository is almighty at cluster scope + +A not that much isolated _tenant_! 😕 + +That's why the **_⚙️OPS_** team enforces specific `ClusterRoles` with restricted permissions + +Let's create these permissions! + +--- + +## _namespace_ isolation for **_🎸ROCKY_** + +.lab[ + +- Here are the restricted permissions to use in the `rocky-test` `Namespace` + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cp ~/container.training/k8s/M6-rocky-cluster-role.yaml ./tenants/base/rocky/ +``` + +] + +> 💡 Note that some resources are managed at cluster scope (like `PersistentVolumes`). +> We need specific permissions, then… + +--- + +## Creating `Github` source in Flux for **_🎸ROCKY_** app repository + +A specific _branch_ of the `Github` repository is monitored by the `Flux` source + +.lab[ + +- ⚠️ you may change the **repository URL** to the one of your own clone + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create source git rocky-app \ + --namespace=rocky-test \ + --url=https://github.com/Musk8teers/container.training-spring-music/ \ + --branch=rocky --export > ./tenants/base/rocky/sync.yaml +``` + +] + +--- + +## Creating `kustomization` in Flux for **_🎸ROCKY_** app repository + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create kustomization rocky \ + --namespace=rocky-test \ + --service-account=rocky \ + --source=GitRepository/rocky-app \ + --path="./k8s/" --export >> ./tenants/base/rocky/sync.yaml + +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cd ./tenants/base/rocky/ && \ + kustomize create --autodetect && \ + cd - +``` + +] + +--- + +class: extra-details + +### 📂 Flux config files + +Let's review our `Flux` configuration files + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cat ./tenants/base/rocky/sync.yaml && \ + cat ./tenants/base/rocky/kustomization.yaml +``` + +] + +--- + +## Adding a kustomize patch for **_⚗️TEST_** cluster deployment + +💡 Remember the DRY strategy! + +- The `Flux` tenant-dedicated configuration is looking for this file: `.tenants/test/rocky/kustomization.yaml` +- It has been configured here: `clusters/CLOUDY/tenants.yaml` + +- All the files we just created are located in `.tenants/base/rocky` +- So we have to create a specific kustomization in the right location + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + mkdir -p ./tenants/test/rocky && \ + cp ~/container.training/k8s/M6-rocky-test-patch.yaml ./tenants/test/rocky/ && \ + cp ~/container.training/k8s/M6-rocky-test-kustomization.yaml ./tenants/test/rocky/kustomization.yaml +``` + +--- + +### Synchronizing Flux config with its Github repo + +Locally, our `Flux` config repo is ready +The **_⚙️OPS_** team has to push it to `Github` for `Flux` controllers to watch and catch it! + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + git add . && \ + git commit -m':wrench: :construction_worker: add ROCKY tenant configuration' && \ + git push +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +class: pic + +![rocky config files](images/M6-R01-config-files.png) + +--- + +class: extra-details + +### Flux resources for ROCKY tenant 1/2 + +.lab[ + +```bash +k8s@shpod:~$ flux get all -A +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +flux-system gitrepository/flux-system main@sha1:8ffd72cf False + True stored artifact for revision 'main@sha1:8ffd72cf' +rocky-test gitrepository/rocky-app rocky@sha1:ffe9f3fe False + True stored artifact for revision 'rocky@sha1:ffe9f3fe' +(…) +``` + +] + +--- + +class: extra-details + +### Flux resources for ROCKY _tenant_ 2/2 + +.lab[ + +```bash +k8s@shpod:~$ flux get all -A +(…) +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +flux-system kustomization/flux-system main@sha1:8ffd72cf False + True Applied revision: main@sha1:8ffd72cf +flux-system kustomization/tenant-prod False + False kustomization path not found: stat /tmp/kustomization-1164119282/tenants/prod: no such file or directory +flux-system kustomization/tenant-test main@sha1:8ffd72cf False + True Applied revision: main@sha1:8ffd72cf +rocky-test kustomization/rocky False + False StatefulSet/db dry-run failed (Forbidden): statefulsets.apps "db" is forbidden: User "system:serviceaccount:rocky-test:rocky" cannot patch resource "statefulsets" in API group "apps" at the cluster scope +``` + +] + +And here is our 2nd Flux error(s)! 😅 + +--- + +class: extra-details + +### Flux Kustomization, mutability, … + +🔍 Notice that none of the expected resources is created: +the whole kustomization is rejected, even if the `StatefulSet` is this only resource that fails! + +🔍 Flux Kustomization uses the dry-run feature to templatize the resources and then applying patches onto them +Good but some resources are not completely mutable, such as `StatefulSets` + +We have to fix the mutation by applying the change without having to patch the resource. + +🔍 Simply add the `spec.targetNamespace: rocky-test` to the `Kustomization` named `rocky` + +--- + +class: extra-details + +## And then it's deployed 1/2 + +You should see the following resources in the `rocky-test` namespace + +.lab[ + +```bash +k8s@shpod-578d64468-tp7r2 ~/$ k get pods,svc,deployments -n rocky-test +NAME READY STATUS RESTARTS AGE +pod/db-0 1/1 Running 0 47s +pod/web-6c677bf97f-c7pkv 0/1 Running 1 (22s ago) 47s +pod/web-6c677bf97f-p7b4r 0/1 Running 1 (19s ago) 47s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/db ClusterIP 10.32.6.128 5432/TCP 48s +service/web ClusterIP 10.32.2.202 80/TCP 48s + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/web 0/2 2 0 47s +``` + +] + +--- + +class: extra-details + +## And then it's deployed 2/2 + +You should see the following resources in the `rocky-test` namespace + +.lab[ + +```bash +k8s@shpod-578d64468-tp7r2 ~/$ k get statefulsets,pvc,pv -n rocky-test +NAME READY AGE +statefulset.apps/db 1/1 47s + +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE +persistentvolumeclaim/postgresql-data-db-0 Bound pvc-c1963a2b-4fc9-4c74-9c5a-b0870b23e59a 1Gi RWO sbs-default 47s + +NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE +persistentvolume/postgresql-data 1Gi RWO,RWX Retain Available 47s +persistentvolume/pvc-150fcef5-ebba-458e-951f-68a7e214c635 1G RWO Delete Bound shpod/shpod sbs-default 4h46m +persistentvolume/pvc-c1963a2b-4fc9-4c74-9c5a-b0870b23e59a 1Gi RWO Delete Bound rocky-test/postgresql-data-db-0 sbs-default 47s +``` + +] + +--- + +class: extra-details + +### PersistentVolumes are using a default `StorageClass` + +💡 This managed cluster comes with custom `StorageClasses` leveraging on Cloud _IaaS_ capabilities (i.e. block devices) + +![Flux configuration waterfall](images/M6-persistentvolumes.png) + +- a default `StorageClass` is applied if none is specified (like here) +- for **_🏭PROD_** purpose, ops team might enforce a more performant `StorageClass` +- on a bare-metal cluster, **_🏭PROD_** team has to configure and provide `StorageClasses` on its own + +--- + +class: pic + +![Flux configuration waterfall](images/M6-flux-config-dependencies.png) + +--- + + +## Upgrading ROCKY app + +The Git source named `rocky-app` is pointing at +- a Github repository named [Musk8teers/container.training-spring-music](https://github.com/Musk8teers/container.training-spring-music/) +- on its branch named `rocky` + +This branch deploy the v1.0.0 of the _Web_ app: +`spec.template.spec.containers.image: ghcr.io/musk8teers/container.training-spring-music:1.0.0` + +What happens if the **_🎸ROCKY_** team upgrades its branch to deploy `v1.0.1` of the _Web_ app? + +--- + +## _tenant_ **_🏭PROD_** + +💡 **_🏭PROD_** _tenant_ is still waiting for its `Flux` configuration, but don't bother for it right now. + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:3
+    branch MOVY order:4
+    branch YouRHere order:5
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
diff --git a/slides/flux/app2-movy-test.md b/slides/flux/app2-movy-test.md new file mode 100644 index 00000000..1b2bfcf5 --- /dev/null +++ b/slides/flux/app2-movy-test.md @@ -0,0 +1,320 @@ +# M01- Configuring **_🎬MOVY_** deployment with Flux + +**_🎸ROCKY_** _tenant_ is now fully usable in **_⚗️TEST_** env, let's do the same for another _dev_ team: **_🎬MOVY_** + +😈 We could do it by using `Flux` _CLI_, +but let's see if we can succeed by just adding manifests in our `Flux` configuration repository. + +--- + +class: pic + +![Flux configuration waterfall](images/M6-flux-config-dependencies.png) + +--- + +## Impact study + +In our `Flux` configuration repository: + +- Creation of the following 📂 folders: `./tenants/[base|test]/MOVY` + +- Modification of the following 📄 file: `./clusters/CLOUDY/tenants.yaml`? + - Well, we don't need to: the watched path include the whole `./tenants/[test]/*` folder + +In the app repository: + +- Creation of a `movy` branch to deploy another version of the app dedicated to movie soundtracks + +--- + +### Creation of the 📂 folders + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cp -pr tenants/base/rocky tenants/base/movy + cp -pr tenants/test/rocky tenants/test/movy +``` + +] + +--- + +### Modification of tenants/[base|test]/movy/* 📄 files + +- For 📄`M6-rocky-*.yaml`, change the file names… + - and update the 📄`kustomization.yaml` file as a result + +- In any file, replace any `rocky` entry by `movy` + +- In 📄 `sync.yaml` be aware of what repository and what branch you want `Flux` to watch for **_🎬MOVY_** app deployment. + - for this demo, let's assume we create a `movy` branch + +--- + +class: extra-details + +### What about reusing rocky-cluster-roles? + +💡 In 📄`M6-movy-cluster-role.yaml` and 📄`rbac.yaml`, we could have reused the already existing `ClusterRoles`: `rocky-full-access`, and `rocky-pv-access` + +A `ClusterRole` is cluster wide. It is not dedicated to a namespace. +- Its permissions are restrained to a specific namespace by being bound to a `ServiceAccount` by a `RoleBinding`. +- Whereas a `ClusterRoleBinding` extends the permissions to the whole cluster scope. + +But a _tenant_ is a **_tenant_** and permissions might evolved separately for **_🎸ROCKY_** and **_🎬MOVY_**. + +So [we got to keep'em separated](https://www.youtube.com/watch?v=GHUql3OC_uU). + +--- + +### Let-su-go! + +The **_⚙️OPS_** team push this new tenant configuration to `Github` for `Flux` controllers to watch and catch it! + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + git add . && \ + git commit -m':wrench: :construction_worker: add MOVY tenant configuration' && \ + git push +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +class: extra-details + +### Another Flux error? + +.lab[ + +- It seems that our `movy` branch is not present in the app repository + +```bash +k8s@shpod:~$ flux get kustomization -A +NAMESPACE NAME REVISION SUSPENDED MESSAGE +(…) +flux-system tenant-prod False False kustomization path not found: stat /tmp/kustomization-113582828/tenants/prod: no such file or directory +(…) +movy-test movy False False Source artifact not found, retrying in 30s +``` + +] + +--- + +### Creating the `movy` branch + +- Let's create this new `movy` branch from `rocky` branch + +.lab[ + +- You can force immediate reconciliation by typing this command: + +```bash +k8s@shpod:~$ flux reconcile source git movy-app -n movy-test +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### New branch detected + +You now have a second app responding on [http://movy.test.mybestdomain.com] +But as of now, it's just the same as the **_🎸ROCKY_** one. + +We want a specific (pink-colored) version with a dataset full of movie soundtracks. + +--- + +## New version of the **_🎬MOVY_** app + +In our branch `movy`… +Let's modify our `deployment.yaml` file with 2 modifications. + +- in `spec.template.spec.containers.image` change the container image tag to `1.0.3` + +- and… let's introduce some evil enthropy by changing this line… 😈😈😈 + +```yaml + value: jdbc:postgresql://db/music +``` + +by this one + +```yaml + value: jdbc:postgresql://db.rocky-test/music +``` + +And push the modifications… + +--- + +class: pic + +![MOVY app has an incorrect dataset](images/M6-incorrect-dataset-in-MOVY-app.png) + +--- + +class: pic + +![ROCKY app has an incorrect dataset](images/M6-incorrect-dataset-in-ROCKY-app.png) + +--- + +### MOVY app is connected to ROCKY database + +How evil have we been! 😈 +We connected the **_🎬MOVY_** app to the **_🎸ROCKY_** database. + +Even if our tenants are isolated in how they manage their Kubernetes resources… +pod network is still full mesh and any connection is authorized. + +> The **_⚙️OPS_** team should fix this! + +--- + +class: extra-details + +## Adding NetworkPolicies to **_🎸ROCKY_** and **_🎬MOVY_** namespaces + +`Network policies` may be seen as the firewall feature in the pod network. +They rules ingress and egress network connections considering a described subset of pods. + +Please, refer to the [`Network policies` chapter in the High Five M4 module](./4.yml.html#toc-network-policies) + +- In our case, we just add the file `~/container.training/k8s/M6-network-policies.yaml` +
in our `./tenants/base/movy` folder + +- without forgetting to update our `kustomization.yaml` file + +- and without forgetting to commit 😁 + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:3
+    branch MOVY order:4
+    branch YouRHere order:5
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'k0s install on METAL cluster' tag:'K01'
+    commit id:'Flux config. for METAL cluster' tag:'K02'
+    branch METAL_TEST-PROD order:3
+    commit id:'ROCKY/MOVY tenants on METAL' type: HIGHLIGHT
+    checkout OPS
+    commit id:'Flux config. for OpenEBS' tag:'K03'
+    checkout METAL_TEST-PROD
+    merge OPS id:'openEBS on METAL' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Prometheus install'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Kyverno install'
+    commit id:'Kyverno rules'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
diff --git a/slides/flux/bootstrap.md b/slides/flux/bootstrap.md new file mode 100644 index 00000000..b53b30b3 --- /dev/null +++ b/slides/flux/bootstrap.md @@ -0,0 +1,410 @@ +# T02- creating **_⚗️TEST_** env on our **_☁️CLOUDY_** cluster + +Let's take a look at our **_☁️CLOUDY_** cluster! + +**_☁️CLOUDY_** is a Kubernetes cluster created with [Scaleway Kapsule](https://www.scaleway.com/en/kubernetes-kapsule/) managed service + +This managed cluster comes preinstalled with specific features: +- Kubernetes dashboard +- specific _Storage Classes_ based on Scaleway _IaaS_ block storage offerings +- a `Cilium` _CNI_ stack already set up + +--- + +## Accessing the managed Kubernetes cluster + +To access our cluster, we'll connect via [`shpod`](https://github.com/jpetazzo/shpod) + +.lab[ + +- If you already have a kubectl on your desktop computer +```bash +kubectl -n shpod run shpod --image=jpetazzo/shpod +kubectl -n shpod exec -it shpod -- bash +``` +- or directly via ssh +```bash +ssh -p myPort k8s@mySHPODSvcIpAddress +``` + +] + +--- + +## Flux installation + +Once `Flux` is installed, +the **_⚙️OPS_** team exclusively operates its clusters by updating a code base in a `Github` repository + +_GitOps_ and `Flux` enable the **_⚙️OPS_** team to rely on the _first-class citizen pattern_ in Kubernetes' world through these steps: + +- describe the **desired target state** +- and let the **automated convergence** happens + +--- + +### Checking prerequisites + +The `Flux` _CLI_ is available in our `shpod` pod + +Before installation, we need to check that: + - `Flux` _CLI_ is correctly installed + - it can connect to the `API server` + - our versions of `Flux` and Kubernetes are compatible + +.lab[ + +```bash +k8s@shpod:~$ flux --version +flux version 2.5.1 + +k8s@shpod:~$ flux check --pre +► checking prerequisites +✔ Kubernetes 1.32.3 >=1.30.0-0 +✔ prerequisites checks passed +``` + +] + +--- + +### Git repository for Flux configuration + +The **_⚙️OPS_** team uses `Flux` _CLI_ +- to create a `git` repository named `fleet-config-using-flux-XXXXX` (⚠ replace `XXXXX` by a personnal suffix) +- in our `Github` organization named `container-training-fleet` + +Prerequisites are: + - `Flux` _CLI_ needs a `Github` personal access token (_PAT_) + - to create and/or access the `Github` repository + - to give permissions to existing teams in our `Github` organization + - The PAT needs _CRUD_ permissions on our `Github` organization + - repositories + - admin:public_key + - users + +- As **_⚙️OPS_** team, let's creates a `Github` personal access token… + +--- + +class: pic + +![Generating a Github personal access token](images/M6-github-add-token.jpg) + +--- + +### Creating dedicated `Github` repo to host Flux config + +.lab[ + +- let's replace the `GITHUB_TOKEN` value by our _Personal Access Token_ +- and the `GITHUB_REPO` value by our specific repository name + +```bash +k8s@shpod:~$ export GITHUB_TOKEN="my-token" && \ + export GITHUB_USER="container-training-fleet" && \ + export GITHUB_REPO="fleet-config-using-flux-XXXXX" + +k8s@shpod:~$ flux bootstrap github \ + --owner=${GITHUB_USER} \ + --repository=${GITHUB_REPO} \ + --team=OPS \ + --team=ROCKY --team=MOVY \ + --path=clusters/CLOUDY +``` +] + +--- + +class: extra-details + +Here is the result + +```bash +✔ repository "https://github.com/container-training-fleet/fleet-config-using-flux-XXXXX" created +► reconciling repository permissions +✔ granted "maintain" permissions to "OPS" +✔ granted "maintain" permissions to "ROCKY" +✔ granted "maintain" permissions to "MOVY" +► reconciling repository permissions +✔ reconciled repository permissions +► cloning branch "main" from Git repository "https://github.com/container-training-fleet/fleet-config-using-flux-XXXXX.git" +✔ cloned repository +► generating component manifests +✔ generated component manifests +✔ committed component manifests to "main" ("7c97bdeb5b932040fd8d8a65fe1dc84c66664cbf") +► pushing component manifests to "https://github.com/container-training-fleet/fleet-config-using-flux-XXXXX.git" +✔ component manifests are up to date +► installing components in "flux-system" namespace +✔ installed components +✔ reconciled components +► determining if source secret "flux-system/flux-system" exists +► generating source secret +✔ public key: ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBFqaT8B8SezU92qoE+bhnv9xONv9oIGuy7yVAznAZfyoWWEVkgP2dYDye5lMbgl6MorG/yjfkyo75ETieAE49/m9D2xvL4esnSx9zsOLdnfS9W99XSfFpC2n6soL+Exodw== +✔ configured deploy key "flux-system-main-flux-system-./clusters/CLOUDY" for "https://github.com/container-training-fleet/fleet-config-using-flux-XXXXX" +► applying source secret "flux-system/flux-system" +✔ reconciled source secret +► generating sync manifests +✔ generated sync manifests +✔ committed sync manifests to "main" ("11035e19cabd9fd2c7c94f6e93707f22d69a5ff2") +► pushing sync manifests to "https://github.com/container-training-fleet/fleet-config-using-flux-XXXXX.git" +► applying sync manifests +✔ reconciled sync configuration +◎ waiting for GitRepository "flux-system/flux-system" to be reconciled +✔ GitRepository reconciled successfully +◎ waiting for Kustomization "flux-system/flux-system" to be reconciled +✔ Kustomization reconciled successfully +► confirming components are healthy +✔ helm-controller: deployment ready +✔ kustomize-controller: deployment ready +✔ notification-controller: deployment ready +✔ source-controller: deployment ready +✔ all components are healthy +``` + +--- + +### Flux configures Github repository access for teams + +- `Flux` sets up permissions that allow teams within our organization to **access** the `Github` repository as maintainers +- Teams need to exist before `Flux` proceeds to this configuration + +![Teams in Github](images/M6-github-teams.png) + +--- + +### ⚠️ Disclaimer + +- In this lab, adding these teams as maintainers was merely a demonstration of how `Flux` _CLI_ sets up permissions in Github + +- But there is no need for dev teams to have access to this `Github` repository + +- One advantage of _GitOps_ lies in its ability to easily set up 💪🏼 **Separation of concerns** by using multiple `Flux` sources + +--- + +### 📂 Flux config files + +`Flux` has been successfully installed onto our **_☁️CLOUDY_** Kubernetes cluster! + +Its configuration is managed through a _Gitops_ workflow sourced directly from our `Github` repository + +Let's review our `Flux` configuration files we've created and pushed into the `Github` repository… +… as well as the corresponding components running in our Kubernetes cluster + +![Flux config files](images/M6-flux-config-files.png) + +--- + +class: pic + +![Flux architecture](images/M6-flux-controllers.png) + +--- + +class: extra-details + +### Flux resources 1/2 + +.lab[ + +```bash +k8s@shpod:~$ kubectl get all --namespace flux-system +NAME READY STATUS RESTARTS AGE +pod/helm-controller-b6767d66-h6qhk 1/1 Running 0 5m +pod/kustomize-controller-57c7ff5596-94rnd 1/1 Running 0 5m +pod/notification-controller-58ffd586f7-zxfvk 1/1 Running 0 5m +pod/source-controller-6ff87cb475-g6gn6 1/1 Running 0 5m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/notification-controller ClusterIP 10.104.139.156 80/TCP 5m1s +service/source-controller ClusterIP 10.106.120.137 80/TCP 5m +service/webhook-receiver ClusterIP 10.96.28.236 80/TCP 5m +(…) +``` + +] + +--- + +class: extra-details + +### Flux resources 2/2 + +.lab[ + +```bash +k8s@shpod:~$ kubectl get all --namespace flux-system +(…) +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/helm-controller 1/1 1 1 5m +deployment.apps/kustomize-controller 1/1 1 1 5m +deployment.apps/notification-controller 1/1 1 1 5m +deployment.apps/source-controller 1/1 1 1 5m + +NAME DESIRED CURRENT READY AGE +replicaset.apps/helm-controller-b6767d66 1 1 1 5m +replicaset.apps/kustomize-controller-57c7ff5596 1 1 1 5m +replicaset.apps/notification-controller-58ffd586f7 1 1 1 5m +replicaset.apps/source-controller-6ff87cb475 1 1 1 5m +``` + +] + +--- + +### Flux components + +- the `source controller` monitors `Git` repositories to apply Kubernetes resources on the cluster + +- the `Helm controller` checks for new `Helm` _charts_ releases in `Helm` repositories and installs updates as needed + +- _CRDs_ store `Flux` configuration within the Kubernetes control plane + +--- + +class: extra-details + +### Flux resources that have been created + +.lab[ + +```bash +k8s@shpod:~$ flux get all --all-namespaces +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +flux-system gitrepository/flux-system main@sha1:d48291a8 False + True stored artifact for revision 'main@sha1:d48291a8' + +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +flux-system kustomization/flux-system main@sha1:d48291a8 False + True Applied revision: main@sha1:d48291a8 +``` + +] + +--- + +### Flux CLI + +`Flux` Command-Line Interface fulfills 3 primary functions: + +1. It installs and configures first mandatory `Flux` resources in a _Gitops_ `git` repository + - ensuring proper access and permissions + +2. It locally generates `YAML` files for desired `Flux` resources so that we just need to `git push` them + - _tenants_ + - sources + - … + +3. It requests the API server to manage `Flux`-related resources + - _operators_ + - _CRDs_ + - logs + +--- + +class: extra-details + +### Flux -- for more info + +Please, refer to the [`Flux` chapter in the High Five M3 module](./3.yml.html#toc-helm-chart-format) + +--- + +### Flux relies on Kustomize + +The `Flux` component named `kustomize controller` look for `Kustomize` resources in `Flux` code-based sources + +1. `Kustomize` look for `YAML` manifests listed in the `kustomization.yaml` file + +2. and aggregates, hydrates and patches them following the `kustomization` configuration + +--- + +class: extra-details + +### 2 different kustomization resources + +⚠️ `Flux` uses 2 distinct resources with `kind: kustomization` + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: kustomization +``` + +describes how Kustomize (the _CLI_ tool) appends and transforms `YAML` manifests into a single bunch of `YAML` described resources + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 group +kind: Kustomization +``` + +describes where `Flux kustomize-controller` looks for a `kustomization.yaml` file in a given `Flux` code-based source + +--- + +class: extra-details + +### Kustomize -- for more info + +Please, refer to the [`Kustomize` chapter in the High Five M3 module](./3.yml.html#toc-kustomize) + +--- + +class: extra-details + +### Group / Version / Kind -- for more info + +For more info about how Kubernetes resource natures are identified by their `Group / Version / Kind` triplet… +… please, refer to the [`Kubernetes API` chapter in the High Five M5 module](./5.yml.html#toc-the-kubernetes-api) + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:3
+    branch MOVY order:4
+    branch YouRHere order:5
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
diff --git a/slides/flux/ingress.md b/slides/flux/ingress.md new file mode 100644 index 00000000..0abb4bdd --- /dev/null +++ b/slides/flux/ingress.md @@ -0,0 +1,284 @@ +# T05- Configuring ingress for **_🎸ROCKY_** app + +🍾 **_🎸ROCKY_** team has just deployed its `v1.0.0` + +We would like to reach it from our workstations +The regular way to do it in Kubernetes is to configure an `Ingress` resource. + +- `Ingress` is an abstract resource that manages how services are exposed outside of the Kubernetes cluster (Layer 7). +- It relies on `ingress-controller`(s) that are technical solutions to handle all the rules related to ingress. + +- Available features vary, depending on the `ingress-controller`: load-balancing, networking, firewalling, API management, throttling, TLS encryption, etc. +- `ingress-controller` may provision Cloud _IaaS_ network resources such as load-balancer, persistent IPs, etc. + +--- + +class: extra-details + +## Ingress -- for more info + +Please, refer to the [`Ingress` chapter in the High Five M2 module](./2.yml.html#toc-exposing-http-services-with-ingress-resources) + +--- + +## Installing `ingress-nginx` as our `ingress-controller` + +We'll use `ingress-nginx` (relying on `NGinX`), quite a popular choice. + +- It is able to provision IaaS load-balancer in ScaleWay Cloud services +- As a reverse-proxy, it is able to balance HTTP connections on an on-premises cluster + +The **_⚙️OPS_** Team add this new install to its `Flux` config. repo + +--- + +### Creating a `Github` source in Flux for `ingress-nginx` + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + mkdir -p ./clusters/CLOUDY/ingress-nginx && \ + flux create source git ingress-nginx \ + --namespace=ingress-nginx \ + --url=https://github.com/kubernetes/ingress-nginx/ \ + --branch=release-1.12 \ + --export > ./clusters/CLOUDY/ingress-nginx/sync.yaml +``` + +] + +--- + +### Creating `kustomization` in Flux for `ingress-nginx` + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create kustomization ingress-nginx \ + --namespace=ingress-nginx \ + --source=GitRepository/ingress-nginx \ + --path="./deploy/static/provider/scw/" \ + --export >> ./clusters/CLOUDY/ingress-nginx/sync.yaml + +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cp -p ~/container.training/k8s/M6-ingress-nginx-kustomization.yaml \ + ./clusters/CLOUDY/ingress-nginx/kustomization.yaml && \ + cp -p ~/container.training/k8s/M6-ingress-nginx-components.yaml \ + ~/container.training/k8s/M6-ingress-nginx-*-patch.yaml \ + ./clusters/CLOUDY/ingress-nginx/ +``` + +] + +--- + +### Applying the new config + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + git add ./clusters/CLOUDY/ingress-nginx && \ + git commit -m':wrench: :rocket: add Ingress-controller' && \ + git push +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +class: pic + +![Ingress-nginx provisionned a IaaS load-balancer in Scaleway Cloud services](images/M6-ingress-nginx-scaleway-lb.png) + +--- + +class: extra-details + +### Using external Git source + +💡 Note that you can directly use pubilc `Github` repository (not maintained by your company). + +- If you have to alter the configuration, `Kustomize` patching capabilities might help. + +- Depending on the _gitflow_ this repository uses, updates will be deployed automatically to your cluster (here we're using a `release` branch). + +- This repo exposes a `kustomization.yaml`. Well done! + +--- + +## Adding the `ingress` resource to ROCKY app + +.lab[ + +- Add the new manifest to our kustomization bunch + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + cp -pr ~/container.training/k8s/M6-rocky-ingress.yaml ./tenants/base/rocky && \ + echo '- M6-rocky-ingress.yaml' >> ./tenants/base/rocky/kustomization.yaml +``` + +- Commit and its done + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + git add . && \ + git commit -m':wrench: :rocket: add Ingress' && \ + git push +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### Here is the result + +After Flux reconciled the whole bunch of sources and kustomizations, you should see + +- `Ingress-NGinX` controller components in `ingress-nginx` namespace +- A new `Ingress` in `rocky-test` namespace + +.lab[ + +```bash +k8s@shpod:~$ kubectl get all -n ingress-nginx && \ + kubectl get ingress -n rocky-test + +k8s@shpod:~$ \ + PublicIP=$(kubectl get ingress rocky -n rocky-test \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + +k8s@shpod:~$ \ + curl --header 'rocky.test.mybestdomain.com' http://$PublicIP/ +``` + +] + +--- + +class: pic + +![Rocky application screenshot](images/M6-rocky-app-screenshot.png) + +--- + +## Upgrading **_🎸ROCKY_** app + +**_🎸ROCKY_** team is now fully able to upgrade and deploy its app autonomously. + +Just give it a try! +- In the `deployment.yaml` file +- in the app repo ([https://github.com/Musk8teers/container.training-spring-music/]) +- you can change the `spec.template.spec.containers.image` to `1.0.1` and then to `1.0.2` + +Dont' forget which branch is watched by `Flux` Git source named `rocky` + +Don't forget to commit! + +--- + +## Few considerations + +- The **_⚙️OPS_** team has to decide how to manage name resolution for public IPs + - Scaleway propose to expose a wildcard domain for its Kubernetes clusters + +- Here, we chose that `Ingress-controller` (that makes sense) but `Ingress` as well were managed by the **_⚙️OPS_** team. + - It might have been done in many different ways! + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:3
+    branch MOVY order:4
+    branch YouRHere order:5
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
diff --git a/slides/flux/kyverno.md b/slides/flux/kyverno.md new file mode 100644 index 00000000..2044ba5a --- /dev/null +++ b/slides/flux/kyverno.md @@ -0,0 +1,241 @@ +## introducing Kyverno + +Kyverno is a tool to extend Kubernetes permission management to express complex policies… +
… and override manifests delivered by client teams. + +--- + +class: extra-details + +### Kyverno -- for more info + +Please, refer to the [`Setting up Kubernetes` chapter in the High Five M4 module](./4.yml.html#toc-policy-management-with-kyverno) for more infos about `Kyverno`. + +--- + +## Creating an `Helm` source in Flux for OpenEBS Helm chart + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + mkdir -p clusters/CLOUDY/kyverno && \ + cp -pr ~/container.training/k8s/ + +k8s@shpod ~$ flux create source helm kyverno \ + --namespace=kyverno \ + --url=https://kyverno.github.io/kyverno/ \ + --interval=3m \ + --export > ./clusters/CLOUDY/kyverno/sync2.yaml +``` + +] + +--- + +## Creating the `HelmRelease` in Flux + +.lab[ + +```bash +k8s@shpod ~$ flux create helmrelease kyverno \ + --namespace=kyverno \ + --source=HelmRepository/kyverno.flux-system \ + --target-namespace=kyverno \ + --create-target-namespace=true \ + --chart-version=">=3.4.2" \ + --chart=kyverno \ + --export >> ./clusters/CLOUDY/kyverno/sync.yaml +``` + +] + +--- + +## Add Kyverno policy + +This polivy is just an example. +It enforces the use of a `Service Account` in `Flux` configurations + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + mkdir -p clusters/CLOUDY/kyverno-policies && \ + cp -pr ~/container.training/k8s/M6-kyverno-enforce-service-account.yaml \ + ./clusters/CLOUDY/kyverno-policies/ + +--- + +### Creating `kustomization` in Flux for Kyverno policies + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + flux create kustomization kyverno-policies \ + --namespace=kyverno \ + --source=GitRepository/flux-system \ + --path="./clusters/CLOUDY/kyverno-policies/" \ + --prune true --interval 5m \ + --depends-on kyverno \ + --export >> ./clusters/CLOUDY/kyverno-policies/sync.yaml +``` + +] + + +## Apply Kyverno policy +```bash +flux create kustomization + +--path +--source GitRepository/ +--export > ./clusters/CLOUDY/kyverno-policies/sync.yaml +``` + +--- + +## Add Kyverno dependency for **_⚗️TEST_** cluster + +- Now that we've got `Kyverno` policies, +- ops team will enforce any upgrade from any kustomization in our dev team tenants +- to wait for the `kyverno` policies to be reconciled (in a `Flux` perspective) + +- upgrade file `./clusters/CLOUDY/tenants.yaml`, +- by adding this property: `spec.dependsOn.{name: kyverno-policies}` + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### Debugging + +`Kyverno-policies` `Kustomization` failed because `spec.dependsOn` property can only target a resource from the same `Kind`. + +- Let's suppress the `spec.dependsOn` property. + +Now `Kustomizations` for **_🎸ROCKY_** and **_🎬MOVY_** tenants failed because of our policies. + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:4
+    branch MOVY order:5
+    branch YouRHere order:6
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT tag:'T07'
+
+    checkout OPS
+    commit id:'k0s install on METAL cluster' tag:'K01'
+    commit id:'Flux config. for METAL cluster' tag:'K02'
+    branch METAL_TEST-PROD order:3
+    commit id:'ROCKY/MOVY tenants on METAL' type: HIGHLIGHT
+    checkout OPS
+    commit id:'Flux config. for OpenEBS' tag:'K03'
+    checkout METAL_TEST-PROD
+    merge OPS id:'openEBS on METAL' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Prometheus install'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Kyverno install'
+    commit id:'Kyverno rules'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'Flux config. for PROD tenant' tag:'P01'
+    branch PROD-env order:2
+    commit id:'ROCKY tenant on PROD'
+    checkout OPS
+    commit id:'ROCKY patch for PROD' tag:'R04'
+    checkout PROD-env
+    merge OPS id:'PROD ready to deploy ROCKY' type: HIGHLIGHT
+    checkout PROD-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout MOVY
+    commit id:'MOVY HELM chart' tag:'M03'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0'
+
diff --git a/slides/flux/observability.md b/slides/flux/observability.md new file mode 100644 index 00000000..57772513 --- /dev/null +++ b/slides/flux/observability.md @@ -0,0 +1,251 @@ +# Install monitoring stack + +The **_⚙️OPS_** team wants to have a real monitoring stack for its clusters. +Let's deploy `Prometheus` and `Grafana` onto the clusters. + +Note: + +--- + +## Creating `Github` source in Flux for monitoring components install repository + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ mkdir -p clusters/CLOUDY/kube-prometheus-stack + +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create source git monitoring \ + --namespace=monitoring \ + --url=https://github.com/fluxcd/flux2-monitoring-example.git \ + --branch=main --export > ./clusters/CLOUDY/kube-prometheus-stack/sync.yaml +``` + +] + +--- + +### Creating `kustomization` in Flux for monitoring stack + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create kustomization monitoring \ + --namespace=monitoring \ + --source=GitRepository/monitoring \ + --path="./monitoring/controllers/kube-prometheus-stack/" \ + --export >> ./clusters/CLOUDY/kube-prometheus-stack/sync.yaml +``` + +] + +--- + +### Install Flux Grafana dashboards + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux create kustomization dashboards \ + --namespace=monitoring \ + --source=GitRepository/monitoring \ + --path="./monitoring/configs/" \ + --export >> ./clusters/CLOUDY/kube-prometheus-stack/sync.yaml +``` + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +## Flux repository synchro is broken😅 + +It seems that `Flux` on **_☁️CLOUDY_** cluster is not able to authenticate with `ssh` on its `Github` config repository! + +What happened? +When we install `Flux` on **_🤘METAL_** cluster, it generates a new `ssh` keypair and override the one used by **_☁️CLOUDY_** among the "deployment keys" of the `Github` repository. + +⚠️ Beware of flux bootstrap command! + +We have to +- generate a new keypair (or reuse an already existing one) +- add the private key to the Flux-dedicated secrets in **_☁️CLOUDY_** cluster +- add it to the "deployment keys" of the `Github` repository + +--- + +### the command + +.lab[ + +- `Flux` _CLI_ helps to recreate the secret holding the `ssh` **private** key. + +```bash +k8s@shpod:~$ flux create secret git flux-system \ + --url=ssh://git@github.com/container-training-fleet/fleet-config-using-flux-XXXXX \ + --private-key-file=/home/k8s/.ssh/id_ed25519 +``` + +- copy the **public** key into the deployment keys of the `Github` repository + +] + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +## Access the Grafana dashboard + +.lab[ + +- Get the `Host` and `IP` address to request + +```bash +k8s@shpod:~$ kubectl -n monitoring get ingress +NAME CLASS HOSTS ADDRESS PORTS AGE +grafana nginx grafana.test.metal.mybestdomain.com 62.210.39.83 80 6m30s +``` + +- Get the `Grafana` admin password + +```bash +k8s@shpod:~$ k get secret kube-prometheus-stack-grafana -n monitoring \ + -o jsonpath='{.data.admin-password}' | base64 -d +``` + +] + +## And browse… + +class: pic + +![Grafana dashboard screenshot](images/M6-grafana-dashboard.png) + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:4
+    branch MOVY order:5
+    branch YouRHere order:6
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT tag:'T07'
+
+    checkout OPS
+    commit id:'k0s install on METAL cluster' tag:'K01'
+    commit id:'Flux config. for METAL cluster' tag:'K02'
+    branch METAL_TEST-PROD order:3
+    commit id:'ROCKY/MOVY tenants on METAL' type: HIGHLIGHT
+    checkout OPS
+    commit id:'Flux config. for OpenEBS' tag:'K03'
+    checkout METAL_TEST-PROD
+    merge OPS id:'openEBS on METAL' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Prometheus install'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'Kyverno install'
+    commit id:'Kyverno rules'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for PROD tenant' tag:'P01'
+    branch PROD-env order:2
+    commit id:'ROCKY tenant on PROD'
+    checkout OPS
+    commit id:'ROCKY patch for PROD' tag:'R04'
+    checkout PROD-env
+    merge OPS id:'PROD ready to deploy ROCKY' type: HIGHLIGHT
+    checkout PROD-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout MOVY
+    commit id:'MOVY HELM chart' tag:'M03'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0'
+
diff --git a/slides/flux/openebs.md b/slides/flux/openebs.md new file mode 100644 index 00000000..0bb25132 --- /dev/null +++ b/slides/flux/openebs.md @@ -0,0 +1,129 @@ +# K03- Installing OpenEBS as our CSI + +`OpenEBS` is a _CSI_ solution capable of hyperconvergence, synchronous replication and other extra features. +It installs with `Helm` charts. + +- `Flux` is able to watch `Helm` repositories and install `HelmReleases` +- To inject its configuration into the `Helm chart` , `Flux` relies on a `ConfigMap` including the `values.yaml` file + +.lab[ + +```bash +k8s@shpod ~$ mkdir -p ./clusters/METAL/openebs/ && \ + cp -pr ~/container.training/k8s/M6-openebs-*.yaml \ + ./clusters/METAL/openebs/ && \ + cd ./clusters/METAL/openebs/ && \ + mv M6-openebs-kustomization.yaml kustomization.yaml && \ + cd - +``` + +] + +--- + +## Creating an `Helm` source in Flux for OpenEBS Helm chart + +.lab[ + +```bash +k8s@shpod ~$ flux create source helm openebs \ + --url=https://openebs.github.io/openebs \ + --interval=3m \ + --export > ./clusters/METAL/openebs/sync.yaml +``` + +] + +--- + +## Creating the `HelmRelease` in Flux + +.lab[ + +```bash +k8s@shpod ~$ flux create helmrelease openebs \ + --namespace=openebs \ + --source=HelmRepository/openebs.flux-system \ + --chart=openebs \ + --values-from=ConfigMap/openebs-values \ + --export >> ./clusters/METAL/openebs/sync.yaml +``` + +] + +--- + +## 📂 Let's review the files + +- `M6-openebs-components.yaml` +
To include the `Flux` resources in the same _namespace_ where `Flux` installs the `OpenEBS` resources, we need to create the _namespace_ **before** the installation occurs + +- `sync.yaml` +
The resources `Flux` uses to watch and get the `Helm chart` + +- `M6-openebs-values.yaml` +
the `values.yaml` file that will be injected into the `Helm chart` + +- `kustomization.yaml` +
This one is a bit special: it includes a [ConfigMap generator](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/) + +- `M6-openebs-kustomizeconfig.yaml` +

This one is tricky: in order for `Flux` to trigger an upgrade of the `Helm Release` when the `ConfigMap` is altered, you need to explain to the `Kustomize ConfigMap generator` how the resources are relating with each others. 🤯 + +And here we go! + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +## And the result + +Now, we have a _cluster_ featuring `openEBS`. +But still… The PersistentVolumeClaim remains in `Pending` state!😭 + +```bash +k8s@shpod ~$ kubectl get storageclass +NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE +openebs-hostpath openebs.io/local Delete WaitForFirstConsumer false 82m +``` +We still don't have a default `StorageClass`!😤 + +--- + +### Manually enforcing the default `StorageClass` + +Even if Flux is constantly reconciling our resources, we still are able to test evolutions by hand. + +.lab[ + +```bash +k8s@shpod ~$ flux suspend helmrelease openebs -n openebs +► suspending helmrelease openebs in openebs namespace +✔ helmrelease suspended +k8s@shpod ~$ kubectl patch storageclass openebs-hostpath \ + -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' + +k8s@shpod ~$ k get storageclass +NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE +openebs-hostpath (default) openebs.io/local Delete WaitForFirstConsumer false 82m +``` + +] + +--- + +### Now the database is OK + +```bash +k8s@shpod ~$ get pvc,pods -n movy-test +NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE +persistentvolumeclaim/postgresql-data-db-0 Bound pvc-ede1634f-2478-42cd-8ee3-7547cd7cdde2 1Gi RWO openebs-hostpath 20m + +NAME READY STATUS RESTARTS AGE +pod/db-0 1/1 Running 0 5h43m +(…) +``` diff --git a/slides/flux/scenario.md b/slides/flux/scenario.md new file mode 100644 index 00000000..e1aca12b --- /dev/null +++ b/slides/flux/scenario.md @@ -0,0 +1,354 @@ +# Kubernetes in production —
an end-to-end example + +- Previous training modules focused on individual topics + + (e.g. RBAC, network policies, CRDs, Helm...) + +- We will now show how to put everything together to deploy apps in production + + (dealing with typical challenges like: multiple apps, multiple teams, multiple clusters...) + +- Our first challenge will be to pick and choose which components to use + + (among the vast [Cloud Native Landscape](https://landscape.cncf.io/)) + +- We'll start with a basic Kubernetes cluster (on cloud or on premises) + +- We'll and enhance it by adding features one at a time + +--- + +## The cast + +There are 3 teams in our company: + +- **_⚙️OPS_** is the platform engineering team + + - they're responsible for building and configuring Kubernetes clusters + +- the **_🎸ROCKY_** team develops and manages the **_🎸ROCKY_** app + + - that app manages a collection of _rock & pop_ albums + + - it's deployed with plain YAML manifests + +- the **_🎬MOVY_** team develops and manages the **_🎬MOVY_** app + + - that app manages a collection of _movie soundtrack_ albums + + - it's deployed with Helm charts + +--- + +## Code and team organization + +- **_🎸ROCKY_** and **_🎬MOVY_** reside in separate git repositories + +- Each team can write code, build package, and deploy their applications: + + - independently +
(= without having to worry about what's happening in the other repo) + + - autonomously +
(= without having to synchronize or obtain privileges from another team) + +--- + +## Cluster organization + +The **_⚙️OPS_** team manages 2 Kubernetes clusters: + +- **_☁️CLOUDY_**: managed cluster from a public cloud provider + +- **_🤘METAL_**: custom-built cluster installed on bare Linux servers + +Let's see the differences between these clusters. + +--- + +## **_☁️CLOUDY_** cluster + +- Managed cluster from a public cloud provider ("Kubernetes-as-a-Service") + +- HA control plane deployed and managed by the cloud provider + +- Two worker nodes (potentially with cluster autoscaling) + +- Usually comes pre-installed with some basic features + + (e.g. metrics-server, CNI, CSI, sometimes an ingress controller) + +- Requires extra components to be production-ready + + (e.g. Flux or other gitops pipeline, observability...) + +- Example: [Scaleway Kapsule][kapsule] (but many other KaaS options are available) + +[kapsule]: https://www.scaleway.com/en/kubernetes-kapsule/ + +--- + +## **_🤘METAL_** cluster + +- Custom-built cluster installed on bare Linux servers + +- HA control plane deployed and managed by the **_⚙️OPS_** team + +- 3 nodes + + - in our example, the nodes will run both the control plane and our apps + + - it is more typical to use dedicated control plane nodes +
(example: 3 control plane nodes + at least 3 worker nodes) + +- Comes with even less pre-installed components than **_☁️CLOUDY_** + + (requiring more work from our **_⚙️OPS_** team) + +- Example: we'll use [k0s] (but many other distros are available) + +[k0s]: https://k0sproject.io/ + +--- + +## **_⚗️TEST_** and **_🏭PROD_** + +- The **_⚙️OPS_** team creates 2 environments for each dev team + + (**_⚗️TEST_** and **_🏭PROD_**) + +- These environments exist on both clusters + + (meaning 2 apps × 2 clusters × 2 envs = 8 envs total) + +- The setup for each env and cluster should follow DRY principles + + (to ensure configurations are consistent and minimize maintenance) + +- Each cluster and each env has its own lifecycle + + (= it should be possible to deploy, add an extra components/feature... +
on one env without impacting the other) + +--- + +### Multi-tenancy + +Both **_🎸ROCKY_** and **_🎬MOVY_** teams should use **dedicated _"tenants"_** on each cluster/env + +- the **_🎸ROCKY_** team should be able to deploy, upgrade and configure its app within its dedicated **namespace** without anybody else involved + +- and the same for **_🎬MOVY_** + +- neither team's deployments might interfere with the other, maintaining a clean and conflict-free environment + +--- + +## Application overview + +- Both dev teams are working on an app to manage music albums + +- This app is mostly based on a `Spring` framework demo called spring-music + +- This lab uses a dedicated fork [container.training-spring-music](https://github.com/Musk8teers/container.training-spring-music): + - with 2 branches dedicated to the **_🎸ROCKY_** and **_🎬MOVY_** teams + +- The app architecture consists of 2 tiers: + - a `Java/Spring` Web app + - a `PostgreSQL` database + +--- + +### 📂 specific file: application.yaml + +This is where we configure the application to connect to the `PostgreSQL` database. + +.lab[ + +🔍 Location: [/src/main/resources/application.yml](https://github.com/Musk8teers/container.training-spring-music/blob/main/src/main/resources/application.yml) + +] + +`PROFILE=postgres` env var is set in [docker-compose.yaml](https://github.com/Musk8teers/container.training-spring-music/blob/main/docker-compose.yml) file, for example… + +--- + +### 📂 specific file: AlbumRepositoryPopulator.java + + +This is where the album collection is initially loaded from the file [`album.json`](https://github.com/Musk8teers/container.training-spring-music/blob/main/src/main/resources/albums.json) + +.lab[ + +🔍 Location: [`/src/main/java/org/cloudfoundry/samples/music/repositories/AlbumRepositoryPopulator.java`](https://github.com/Musk8teers/container.training-spring-music/blob/main/src/main/java/org/cloudfoundry/samples/music/repositories/AlbumRepositoryPopulator.java) + +] + +--- + +## 🚚 How to deploy? + +The **_⚙️OPS_** team offers 2 deployment strategies that dev teams can use autonomously: + +- **_🎸ROCKY_** uses a `Flux` _GitOps_ workflow based on regular Kubernetes `YAML` resources + +- **_🎬MOVY_** uses a `Flux` _GitOps_ workflow based on `Helm` charts + +--- + +## 🍱 What features? + + +The **_⚙️OPS_** team aims to provide clusters offering the following features to its users: + +- a network stack with efficient workload isolation + +- ingress and load-balancing capabilites + +- an enterprise-grade monitoring solution for real-time insights + +- automated policy rule enforcement to control Kubernetes resources requested by dev teams + + + + + +--- + +## 🌰 In a nutshell + +- 3 teams: **_⚙️OPS_**, **_🎸ROCKY_**, **_🎬MOVY_** + +- 2 clusters: **_☁️CLOUDY_**, **_🤘METAL_** + +- 2 envs per cluster and per dev team: **_⚗️TEST_**, **_🏭PROD_** + +- 2 Web apps Java/Spring + PostgreSQL: one for pop and rock albums, another for movie soundtrack albums + +- 2 deployment strategies: regular `YAML` resources + `Kustomize`, `Helm` charts + + +> 💻 `Flux` is used both +> - to operate the clusters +> - and to manage the _GitOps_ deployment workflows + +--- + +### What our scenario might look like… + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:4
+    branch MOVY order:5
+    branch YouRHere order:6
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
+    checkout OPS
+    commit id:'Ingress-controller config.' tag:'T05'
+    checkout TEST-env
+    merge OPS id:'Ingress-controller install' type: HIGHLIGHT tag:'T06'
+
+    checkout OPS
+    commit id:'ROCKY patch for ingress config.' tag:'R03'
+    checkout TEST-env
+    merge OPS id:'ingress config. for ROCKY app'
+
+    checkout ROCKY
+    commit id:'blue color' tag:'v1.0.1'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.1'
+
+    checkout ROCKY
+    commit id:'pink color' tag:'v1.0.2'
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout OPS
+    commit id:'FLUX config for MOVY deployment' tag:'M01'
+    checkout TEST-env
+    merge OPS id:'FLUX ready to deploy MOVY' type: HIGHLIGHT tag:'M02'
+
+    checkout MOVY
+    commit id:'MOVY' tag:'v1.0.3'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0.3' type: REVERSE
+
+    checkout OPS
+    commit id:'Network policies'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT tag:'T07'
+
+    checkout OPS
+    commit id:'k0s install on METAL cluster' tag:'K01'
+    commit id:'Flux config. for METAL cluster' tag:'K02'
+    branch METAL_TEST-PROD order:3
+    commit id:'ROCKY/MOVY tenants on METAL' type: HIGHLIGHT
+    checkout OPS
+    commit id:'Flux config. for OpenEBS' tag:'K03'
+    checkout METAL_TEST-PROD
+    merge OPS id:'openEBS on METAL' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Prometheus install'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Kyverno install'
+    commit id:'Kyverno rules'
+    checkout TEST-env
+    merge OPS type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for PROD tenant' tag:'P01'
+    branch PROD-env order:2
+    commit id:'ROCKY tenant on PROD'
+    checkout OPS
+    commit id:'ROCKY patch for PROD' tag:'R04'
+    checkout PROD-env
+    merge OPS id:'PROD ready to deploy ROCKY' type: HIGHLIGHT
+    checkout PROD-env
+    merge ROCKY tag:'ROCKY v1.0.2'
+
+    checkout MOVY
+    commit id:'MOVY HELM chart' tag:'M03'
+    checkout TEST-env
+    merge MOVY tag:'MOVY v1.0'
+
diff --git a/slides/flux/tenants.md b/slides/flux/tenants.md new file mode 100644 index 00000000..336b450e --- /dev/null +++ b/slides/flux/tenants.md @@ -0,0 +1,200 @@ +# Multi-tenants management with Flux + +💡 Thanks to `Flux`, we can manage Kubernetes resources from inside the clusters. + +The **_⚙️OPS_** team uses `Flux` with a _GitOps_ code base to: +- configure the clusters +- deploy tools and components to extend the clusters capabilites +- configure _GitOps_ workflow for dev teams in **dedicated and isolated _tenants_** + +The **_🎸ROCKY_** team uses `Flux` to deploy every new release of its app, by detecting every new `git push` events happening in its app `Github` repository + + +The **_🎬MOVY_** team uses `Flux` to deploy every new release of its app, packaged and published in a new `Helm` chart release + +--- + +## Creating _tenants_ with Flux + +While basic `Flux` behavior is to use a single configuration directory applied by a cluster-wide role… + +… it can also enable _multi-tenant_ configuration by: +- creating dedicated directories for each _tenant_ in its configuration code base +- and using a dedicated `ServiceAccount` with limited permissions to operate in each _tenant_ + +Several _tenants_ are created +- per env + - for **_⚗️TEST_** + - and **_🏭PROD_** +- per team + - for **_🎸ROCKY_** + - and **_🎬MOVY_** + +--- + +class: pic + +![Multi-tenants clusters](images/M6-cluster-multi-tenants.png ) + +--- + +### Flux CLI works locally + +First, we have to **locally** clone your `Flux` configuration `Github` repository + +- create an ssh key pair +- add the **public** key to your `Github` repository (**with write access**) +- and git clone the repository + +--- + +### The command line 1/2 + +Creating the **_⚗️TEST_** _tenant_ + +.lab[ + +- ⚠️ Think about renaming the repo with your own suffix +```bash +k8s@shpod:~$ cd fleet-config-using-flux-XXXXX/ +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + flux create kustomization tenant-test \ + --namespace=flux-system \ + --source=GitRepository/flux-system \ + --path ./tenants/test \ + --interval=1m \ + --prune --export >> clusters/CLOUDY/tenants.yaml +``` + +] + +--- + +### The command line 2/2 + +Then we create the **_🏭PROD_** _tenant_ + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ \ + flux create kustomization tenant-prod \ + --namespace=flux-system \ + --source=GitRepository/flux-system \ + --path ./tenants/prod \ + --interval=3m \ + --prune --export >> clusters/CLOUDY/tenants.yaml +``` + +] + +--- + +### 📂 Flux tenants.yaml files + +Let's review the `fleet-config-using-flux-XXXXX/clusters/CLOUDY/tenants.yaml` file + + + + +⚠️ The last command we type in `Flux` _CLI_ creates the `YAML` manifest **locally** + +> ☝🏻 Don't forget to `git commit` and `git push` to `Github`! + +--- + +class: pic + +![Running Mario](images/M6-running-Mario.gif) + +--- + +### Our 1st Flux error + +.lab[ + +```bash +k8s@shpod:~/fleet-config-using-flux-XXXXX$ flux get all +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +flux-system gitrepository/flux-system main@sha1:0466652e False + True stored artifact for revision 'main@sha1:0466652e' + +NAMESPACE NAME REVISION SUSPENDED + READY MESSAGE +kustomization/flux-system main@sha1:0466652e False True + Applied revision: main@sha1:0466652e +kustomization/tenant-prod False False + kustomization path not found: stat /tmp/kustomization-417981261/tenants/prod: no such file or directory +kustomization/tenant-test False False + kustomization path not found: stat /tmp/kustomization-2532810750/tenants/test: no such file or directory +``` + +] + +> Our configuration may be incomplete 😅 + +--- + +## Configuring Flux for the **_🎸ROCKY_** team + +What the **_⚙️OPS_** team has to do: + +- 🔧 Create a dedicated `rocky` _tenant_ for **_⚗️TEST_** and **_🏭PROD_** envs on the cluster + +- 🔧 Create the `Flux` source pointing to the `Github` repository embedding the **_🎸ROCKY_** app source code + +- 🔧 Add a `kustomize` _patch_ into the global `Flux` config to include this specific `Flux` config. dedicated to the deployment of the **_🎸ROCKY_** app + +What the **_🎸ROCKY_** team has to do: + +- 👨‍💻 Create the `kustomization.yaml` file in the **_🎸ROCKY_** app source code repository on `Github` + +--- + +### 🗺️ Where are we in our scenario? + +
+%%{init:
+    {
+      "theme": "default",
+      "gitGraph": {
+        "mainBranchName": "OPS",
+        "mainBranchOrder": 0
+      }
+    }
+}%%
+gitGraph
+    commit id:"0" tag:"start"
+    branch ROCKY order:3
+    branch MOVY order:4
+    branch YouRHere order:5
+
+    checkout OPS
+    commit id:'Flux install on CLOUDY cluster' tag:'T01'
+    branch TEST-env order:1
+    commit id:'FLUX install on TEST' tag:'T02' type: HIGHLIGHT
+
+    checkout OPS
+    commit id:'Flux config. for TEST tenant' tag:'T03'
+    commit id:'namespace isolation by RBAC'
+    checkout TEST-env
+    merge OPS id:'ROCKY tenant creation' tag:'T04'
+
+    checkout YouRHere
+    commit id:'x'
+    checkout OPS
+    merge YouRHere id:'YOU ARE HERE'
+
+    checkout OPS
+    commit id:'ROCKY deploy. config.' tag:'R01'
+
+    checkout TEST-env
+    merge OPS id:'TEST ready to deploy ROCKY' type: HIGHLIGHT tag:'R02'
+
+    checkout ROCKY
+    commit id:'ROCKY' tag:'v1.0.0'
+
+    checkout TEST-env
+    merge ROCKY tag:'ROCKY v1.0.0'
+
diff --git a/slides/images/konnectivity.png b/slides/images/konnectivity.png new file mode 100644 index 0000000000000000000000000000000000000000..07ff272adf8fe7da16edad45e0b15d471cb3d1c8 GIT binary patch literal 72821 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¬vvE6*V21w7Y8=S0Y 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 /usr/local/bin/k0sctl + chmod +x /usr/local/bin/k0sctl + ``` + +] + +--- + +## `k0sctl` configuration file + +.lab[ + +- Create a default configuration file: + ```bash + k0sctl init \ + --controller-count 3 \ + --user docker \ + --k0s m621 m622 m623 > k0sctl.yaml + ``` + +- Edit the following field so that controller nodes also run kubelet: + + `spec.hosts[*].role: controller+worker` + +- Add the following fields so that controller nodes can run normal workloads: + + `spec.hosts[*].noTaints: true` + +] + +--- + +## Deploy the cluster + +- `k0sctl` will connect to all our nodes using SSH + +- It will copy `k0s` to the nodes + +- ...And invoke it with the correct parameters + +- ✨️ Magic! ✨️ + +.lab[ + +- Let's do this! + ```bash + k0sctl apply --config k0sctl.yaml + ``` + +] + +--- + +## Check the results + +- `k0s` has multiple troubleshooting commands to check cluster health + +.lab[ + +- Check cluster status: + ```bash + sudo k0s status + ``` + +] + +- The result should look like this: + ``` + Version: v1.33.1+k0s.1 + Process ID: 60183 + Role: controller + Workloads: true + SingleNode: false + Kube-api probing successful: true + Kube-api probing last error: + ``` + +--- + +## Checking etcd status + +- We can also check the status of our etcd cluster + +.lab[ + +- Check that the etcd cluster has 3 members: + ```bash + sudo k0s etcd member-list + ``` +] + +- The result should look like this: + ``` + {"members":{"m621":"https://10.10.3.190:2380","m622":"https://10.10.2.92:2380", + "m623":"https://10.10.2.110:2380"}} + ``` + +--- + +## Running `kubectl `commands + +- `k0s` embeds `kubectl` as well + +.lab[ + +- Check that our nodes are all `Ready`: + ```bash + sudo k0s kubectl get nodes + ``` + +] + +- The result should look like this: + ``` + NAME STATUS ROLES AGE VERSION + m621 Ready control-plane 66m v1.33.1+k0s + m622 Ready control-plane 66m v1.33.1+k0s + m623 Ready control-plane 66m v1.33.1+k0s + ``` + +--- + +class: extra-details + +## Single node install (FYI!) + +Just in case you need to quickly get a single-node cluster with `k0s`... + +Download `k0s`: +```bash +curl -sSLf https://get.k0s.sh | sudo sh +``` + +Set up the control plane and other components: +```bash +sudo k0s install controller --single +``` + +Start it: +```bash +sudo k0s start +``` + +--- + +class: extra-details + +## Single node uninstall + +To stop the running cluster: +```bash +sudo k0s start +``` + +Reset and wipe its state: +```bash +sudo k0s reset +``` + +] + +--- + +## Deploying shpod + +- Our machines might be very barebones + +- Let's get ourselves an environment with completion, colors, Helm, etc. + +.lab[ + +- Run shpod: + ```bash + curl https://shpod.in | sh + ``` + +]