├── 00-Introduction-and-Installation ├── manifests │ ├── variables │ ├── pod-httpd.yml │ ├── svc-web-8080.yml │ ├── svc-clusterip.yml │ ├── nginx-config.conf │ ├── pvc.yml │ ├── pod-busybox.yml │ ├── pv.yml │ ├── svc-nodeport.yml │ ├── pod-env.yml │ ├── pod-node-selector.yml │ ├── pv-nfs-dns.yml │ ├── pv-nfs-ip.yml │ ├── pod-busybox-ns.yml │ ├── pod-secret-as-var.yml │ ├── pod-multi-vol.yml │ ├── deploy-nginx.yml │ ├── pod-emtydir-vol.yml │ ├── pod-busybox-ready.yml │ ├── pod-secret.yml │ ├── ingress-nginx.yml │ ├── pod-mount-nginx.yml │ ├── ingress.yml │ ├── pod-pv.yml │ ├── pod-pvc.yml │ ├── pod-probes.yml │ ├── deploy-redis.yml │ ├── multi-resource-manifest.yml │ ├── pod-multi-container-vol.yml │ ├── pod-wordpress-mysql.yml │ ├── pv-multiple-nfs.yml │ └── ingress-multi-service.yml ├── Dockerfile ├── ghost-blog-on-k8s-figure1.png ├── ghost-blog-on-k8s-figure2.png ├── index.html ├── initializing-k8s.sh ├── sidecar-deploy.sh ├── Running-applications-in-Kubernetes.md ├── nginx-create-image.sh ├── README.md ├── nginx-run-custom-image.sh ├── sidecar-pod.yml ├── The-why-and-how-of-Kubernetes.md ├── Making-your-own-image-to-run-in-kubernetes.md ├── containderd-k8s-install.sh ├── bootstrap-cluster-with-kubeadm-on-any-cloud.md └── ghost-with-ingress-and-cert-manager.md ├── 01-GitOps-and-Observability ├── namespace.yaml ├── service.yaml ├── pvc.yaml ├── clusterissuer.yaml ├── Dockerfile ├── servicemon-otel.yaml ├── app-of-apps.yaml ├── ghost-app.yaml ├── ingress-nginx-app.yaml ├── README.md ├── cert-manager-app.yaml ├── ingress.yaml ├── tempo-app.yaml ├── tempo-values.yaml ├── loki-app.yaml ├── deployment.yaml ├── otel-collector-app.yaml ├── setup-repo.sh ├── prometheus-app.yaml ├── kps-values.yaml ├── ghost-deploy-with-otel.yaml ├── otel-values.yaml ├── loki-values.yaml ├── visualize-with-grafana.md ├── observability-prometheus-and-loki.md ├── metrics-logs-traces-with-opentelemetry.md └── apps-with-gitops-and-argocd.md ├── week4 ├── K8s-Network-Policies.png ├── manifests │ ├── netpolicy-deny-all.yml │ ├── pvc.yml │ ├── sc.yml │ ├── pod.yml │ ├── netpolicy-db-5984.yml │ ├── project1-deploy-svc-all-in-one.yml │ ├── netpolicy-web-80.yml │ ├── netpolicy-api-3000.yml │ ├── deploy-app-routing.yml │ └── deploy-policy-all-db-api-web.yml ├── deploy-aci.yml └── main.tf ├── kubernetes-from-scratch-cover.png ├── week5 ├── manifests │ ├── cluster-role.yml │ ├── csr.yml │ ├── pod-with-sa.yml │ ├── role.yml │ └── deploy-with-sa.yml ├── run.sh └── main.bicep ├── 02-Extending-K8s-with-Operators-and-Custom-Resources ├── my-podset.yaml ├── .dockerignore ├── README.md ├── Dockerfile ├── podset_types.go ├── build-simple-opeartor.md ├── what-are-crds.md ├── registries.conf ├── podset_controller.go ├── operator-pattern.md └── Makefile ├── week7 ├── manifests │ ├── svc-mysql.yml │ ├── svc-wordpress.yml │ ├── pvc-mysql.yml │ ├── pvc-wordpress.yml │ ├── pod-tolerate.yml │ ├── deploy-stress.yml │ ├── deploy-mysql.yml │ └── deploy-wordpress.yml ├── run.sh └── main.bicep ├── week6 ├── manifests │ ├── cert.yml │ ├── deploy.yml │ ├── cluster-issuer.yml │ ├── ingress-clippy.yml │ ├── deploy-clippy.yml │ └── ingress.yml ├── run.sh └── main.bicep ├── week8 └── manifests │ ├── deploy-prometheus.yml │ ├── deploy-stress-test.yml │ └── configmap-prometheus.yml ├── awesome-k8s-resources.md ├── kustomization-examples.yaml ├── networkpolicy-rbac-examples.yaml ├── networkpolicy-rbac-variations.yaml ├── helm-values-examples.yaml ├── kyverno-falco-policies.yaml ├── .gitignore ├── kustomization-patches.yaml ├── custom-resource-definitions.yaml ├── README.md └── combined_output.yaml /00-Introduction-and-Installation/manifests/variables: -------------------------------------------------------------------------------- 1 | VAR1=Hello 2 | VAR2=World 3 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: ghost -------------------------------------------------------------------------------- /00-Introduction-and-Installation/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY ./index.html /usr/share/nginx/html/index.html -------------------------------------------------------------------------------- /week4/K8s-Network-Policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/HEAD/week4/K8s-Network-Policies.png -------------------------------------------------------------------------------- /kubernetes-from-scratch-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/HEAD/kubernetes-from-scratch-cover.png -------------------------------------------------------------------------------- /00-Introduction-and-Installation/ghost-blog-on-k8s-figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/HEAD/00-Introduction-and-Installation/ghost-blog-on-k8s-figure1.png -------------------------------------------------------------------------------- /00-Introduction-and-Installation/ghost-blog-on-k8s-figure2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/HEAD/00-Introduction-and-Installation/ghost-blog-on-k8s-figure2.png -------------------------------------------------------------------------------- /week4/manifests/netpolicy-deny-all.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: deny-all 5 | spec: 6 | podSelector: {} 7 | policyTypes: 8 | - Ingress 9 | - Egress -------------------------------------------------------------------------------- /week5/manifests/cluster-role.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: pod-reader 5 | rules: 6 | - apiGroups: [""] 7 | resources: ["pods"] 8 | verbs: ["get", "watch", "list"] -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/my-podset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps.example.com/v1 2 | kind: PodSet 3 | metadata: 4 | name: test-podset 5 | namespace: default 6 | spec: 7 | replicas: 3 8 | image: nginx:latest -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-httpd.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: newhttpd 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: httpd 9 | image: httpd 10 | 11 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/svc-web-8080.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web 5 | spec: 6 | selector: 7 | run: nginx 8 | ports: 9 | - protocol: "TCP" 10 | port: 8080 11 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/svc-clusterip.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx 5 | spec: 6 | selector: 7 | run: nginx 8 | ports: 9 | - protocol: "TCP" 10 | port: 80 11 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/nginx-config.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8888; 3 | server_name localhost; 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pvc.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: pvc 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 1Gi 11 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ghost 5 | namespace: ghost 6 | spec: 7 | selector: 8 | app: ghost 9 | ports: 10 | - port: 2368 11 | targetPort: 2368 12 | name: http -------------------------------------------------------------------------------- /week4/manifests/pvc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: fastfilepvc 5 | spec: 6 | accessModes: 7 | - ReadWriteMany 8 | storageClassName: fastfilesc 9 | resources: 10 | requests: 11 | storage: 100Gi -------------------------------------------------------------------------------- /week7/manifests/svc-mysql.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: wordpress-mysql 5 | labels: 6 | app: wordpress 7 | spec: 8 | ports: 9 | - port: 3306 10 | selector: 11 | app: wordpress 12 | tier: mysql 13 | clusterIP: None -------------------------------------------------------------------------------- /week7/manifests/svc-wordpress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: wordpress 5 | labels: 6 | app: wordpress 7 | spec: 8 | ports: 9 | - port: 80 10 | selector: 11 | app: wordpress 12 | tier: frontend 13 | type: LoadBalancer -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-busybox.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: busybox2 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: busy 9 | image: busybox 10 | command: 11 | - sleep 12 | - "3600" 13 | -------------------------------------------------------------------------------- /week6/manifests/cert.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: Certificate 3 | metadata: 4 | name: app-tls 5 | spec: 6 | secretName: app-tls 7 | dnsNames: 8 | - dotnetapp.southcentralus.cloudapp.azure.com 9 | issuerRef: 10 | name: app-tls 11 | kind: ClusterIssuer -------------------------------------------------------------------------------- /00-Introduction-and-Installation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Custom Nginx 6 | 7 | 8 |

Congrats!! You've created a custom container image and deployed it to Kubernetes!!

9 | 10 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pv.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: pv-volume 5 | labels: 6 | type: local 7 | spec: 8 | capacity: 9 | storage: 2Gi 10 | accessModes: 11 | - ReadWriteOnce 12 | hostPath: 13 | path: "/mydata" 14 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/svc-nodeport.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webserver 5 | spec: 6 | selector: 7 | run: nginx 8 | ports: 9 | - port: 80 10 | name: "http" 11 | targetPort: 80 12 | nodePort: 30000 13 | type: NodePort 14 | -------------------------------------------------------------------------------- /week5/manifests/csr.yml: -------------------------------------------------------------------------------- 1 | apiVersion: certificates.k8s.io/v1 2 | kind: CertificateSigningRequest 3 | metadata: 4 | name: myuser 5 | spec: 6 | groups: 7 | - system:authenticated 8 | request: 9 | signerName: kubernetes.io/kube-apiserver-client 10 | usages: 11 | - client auth -------------------------------------------------------------------------------- /week7/manifests/pvc-mysql.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: mysql-pv-claim 5 | labels: 6 | app: wordpress 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | storageClassName: default 11 | resources: 12 | requests: 13 | storage: 20Gi -------------------------------------------------------------------------------- /week7/manifests/pvc-wordpress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: wp-pv-claim 5 | labels: 6 | app: wordpress 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | storageClassName: default 11 | resources: 12 | requests: 13 | storage: 20Gi -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-env.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: test1 5 | spec: 6 | containers: 7 | - name: test1 8 | image: cirros 9 | command: ["/bin/sh", "-c", "env"] 10 | envFrom: 11 | - configMapRef: 12 | name: variables 13 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: ghost-pvc 5 | namespace: ghost 6 | spec: 7 | accessModes: 8 | - ReadWriteOnce 9 | storageClassName: linode-block-storage-retain 10 | resources: 11 | requests: 12 | storage: 8Gi -------------------------------------------------------------------------------- /week5/manifests/pod-with-sa.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | run: nginx 7 | name: nginx 8 | namespace: web 9 | spec: 10 | serviceAccountName: view-sa 11 | containers: 12 | - image: nginx 13 | name: nginx 14 | restartPolicy: Always -------------------------------------------------------------------------------- /week4/manifests/sc.yml: -------------------------------------------------------------------------------- 1 | kind: StorageClass 2 | apiVersion: storage.k8s.io/v1 3 | metadata: 4 | name: fastfilesc 5 | provisioner: kubernetes.io/azure-file 6 | mountOptions: 7 | - dir_mode=0777 8 | - file_mode=0777 9 | - uid=0 10 | - gid=0 11 | - mfsymlinks 12 | - cache=strict 13 | parameters: 14 | skuName: Premium_LRS -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-node-selector.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: busybox3 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: busybox 9 | image: busybox 10 | command: 11 | - sleep 12 | - "3600" 13 | nodeSelector: 14 | kubernetes.io/os: linux 15 | -------------------------------------------------------------------------------- /week5/manifests/role.yml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: view-serviceaccount-rolebinding 5 | namespace: web 6 | roleRef: 7 | apiGroup: rbac.authorization.k8s.io 8 | kind: ClusterRole 9 | name: view 10 | subjects: 11 | - kind: ServiceAccount 12 | name: view-sa 13 | namespace: web -------------------------------------------------------------------------------- /00-Introduction-and-Installation/initializing-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --cri-socket=unix:///run/containerd/containerd.sock 4 | 5 | # sleep 6 | sleep 300 7 | 8 | # apply calico manifest 9 | kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml 10 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pv-nfs-dns.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: pv-nfs 5 | spec: 6 | capacity: 7 | storage: 1Gi 8 | accessModes: 9 | - ReadWriteMany 10 | persistentVolumeReclaimPolicy: Retain 11 | nfs: 12 | path: /data 13 | server: myserver 14 | readOnly: false 15 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pv-nfs-ip.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: nfs-pv 5 | spec: 6 | capacity: 7 | storage: 2Gi 8 | accessModes: 9 | - ReadWriteMany 10 | persistentVolumeReclaimPolicy: Retain 11 | nfs: 12 | path: /data 13 | server: 192.168.99.1 14 | readOnly: false 15 | -------------------------------------------------------------------------------- /week4/manifests/pod.yml: -------------------------------------------------------------------------------- 1 | kind: Pod 2 | apiVersion: v1 3 | metadata: 4 | name: mypod 5 | spec: 6 | containers: 7 | - name: mypod 8 | image: nginx:1.15.5 9 | volumeMounts: 10 | - mountPath: "/mnt/azure" 11 | name: volume 12 | volumes: 13 | - name: volume 14 | persistentVolumeClaim: 15 | claimName: fastfilepvc 16 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-busybox-ns.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: secret 5 | --- 6 | apiVersion: v1 7 | kind: Pod 8 | metadata: 9 | name: busybox2 10 | namespace: secret 11 | spec: 12 | containers: 13 | - image: busybox 14 | name: busy 15 | command: 16 | - sleep 17 | - "3600" 18 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-secret-as-var.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: mymysql 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: mysql 9 | image: mysql:latest 10 | env: 11 | - name: MYSQL_ROOT_PASSWORD 12 | valueFrom: 13 | secretKeyRef: 14 | name: mysql 15 | key: password 16 | -------------------------------------------------------------------------------- /week4/manifests/netpolicy-db-5984.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: db-netpol 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app: db 9 | policyTypes: 10 | - Ingress 11 | ingress: 12 | - from: 13 | - podSelector: 14 | matchLabels: 15 | app: api 16 | ports: 17 | - port: 5984 18 | protocol: TCP -------------------------------------------------------------------------------- /01-GitOps-and-Observability/clusterissuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: ghost-tls 5 | spec: 6 | acme: 7 | server: https://acme-v02.api.letsencrypt.org/directory 8 | email: chad@kubeskills.com 9 | privateKeySecretRef: 10 | name: ghost-tls 11 | solvers: 12 | - http01: 13 | ingress: 14 | ingressClassName: nginx -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-multi-vol.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: busyboxab 5 | namespace: default 6 | spec: 7 | containers: 8 | - image: busybox 9 | name: busy1 10 | command: 11 | - sleep 12 | - "3600" 13 | volumeMounts: 14 | - mountPath: /dir 15 | name: myvolume 16 | volumes: 17 | - name: myvolume 18 | emptyDir: {} 19 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/deploy-nginx.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: nginx 6 | name: nginx 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - image: nginx 19 | name: nginx -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-emtydir-vol.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: vol2 5 | spec: 6 | containers: 7 | - name: centos2 8 | image: centos:7 9 | command: 10 | - sleep 11 | - "3600" 12 | volumeMounts: 13 | - mountPath: /test 14 | name: test 15 | restartPolicy: Always 16 | volumes: 17 | - name: test 18 | emptyDir: {} 19 | -------------------------------------------------------------------------------- /week7/manifests/pod-tolerate.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: mypod 5 | spec: 6 | containers: 7 | - image: nginx:1.15.11-alpine 8 | name: mypod 9 | resources: 10 | requests: 11 | cpu: 100m 12 | memory: 128Mi 13 | limits: 14 | cpu: 1 15 | memory: 2G 16 | tolerations: 17 | - key: "sku" 18 | operator: "Equal" 19 | value: "gpu" 20 | effect: "NoSchedule" -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-busybox-ready.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: busybox-ready 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: busy 9 | image: busybox 10 | command: 11 | - sleep 12 | - "3600" 13 | readinessProbe: 14 | periodSeconds: 10 15 | exec: 16 | command: 17 | - cat 18 | - /tmp/nothing 19 | resources: {} 20 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: secretbox2 5 | namespace: default 6 | spec: 7 | containers: 8 | - name: secretbox 9 | image: busybox 10 | command: 11 | - sleep 12 | - "3600" 13 | volumeMounts: 14 | - mountPath: /secretstuff 15 | name: secret 16 | volumes: 17 | - name: secret 18 | secret: 19 | secretName: secretstuff 20 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/ingress-nginx.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: nginx-ingress 5 | annotations: 6 | ingress.kubernetes.io/rewrite-target: / 7 | spec: 8 | rules: 9 | - http: 10 | paths: 11 | - path: /nginxserver 12 | pathType: Prefix 13 | backend: 14 | service: 15 | name: nginx 16 | port: 17 | number: 80 18 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghost:6-alpine 2 | 3 | # Switch to root to install packages 4 | USER root 5 | 6 | # Install minimal OpenTelemetry packages 7 | RUN cd /var/lib/ghost && \ 8 | npm install --save --production \ 9 | @opentelemetry/api@^1.9.0 \ 10 | @opentelemetry/auto-instrumentations-node@^0.50.0 && \ 11 | npm cache clean --force 12 | 13 | # Switch back to node user for security 14 | USER node 15 | 16 | EXPOSE 2368 17 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-mount-nginx.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx 5 | labels: 6 | role: web 7 | spec: 8 | containers: 9 | - name: nginx 10 | image: nginx 11 | volumeMounts: 12 | - name: conf 13 | mountPath: /etc/nginx/conf.d 14 | volumes: 15 | - name: conf 16 | configMap: 17 | name: nginx 18 | items: 19 | - key: nginx-custom-config.conf 20 | path: default.conf 21 | -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test artifacts 3 | *.test 4 | *.out 5 | coverage.txt 6 | *.swp 7 | *.swo 8 | *~ 9 | 10 | # Ignore git files 11 | .git 12 | .gitignore 13 | 14 | # Ignore documentation 15 | *.md 16 | LICENSE 17 | 18 | # Ignore IDE files 19 | .vscode 20 | .idea 21 | *.iml 22 | 23 | # Keep everything else (source code, go files, etc.) -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: example-ingress 5 | annotations: 6 | nginx.ingress.kubernetes.io/rewrite-target: /$1 7 | spec: 8 | rules: 9 | - host: hello-world.info 10 | http: 11 | paths: 12 | - path: /(.+) 13 | pathType: Prefix 14 | backend: 15 | service: 16 | name: web 17 | port: 18 | number: 80 19 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-pv.yml: -------------------------------------------------------------------------------- 1 | kind: Pod 2 | apiVersion: v1 3 | metadata: 4 | name: pv-pod 5 | spec: 6 | volumes: 7 | - name: pv-storage 8 | persistentVolumeClaim: 9 | claimName: pv-claim 10 | containers: 11 | - name: pv-container 12 | image: nginx 13 | ports: 14 | - containerPort: 80 15 | name: "http-server" 16 | volumeMounts: 17 | - mountPath: "/usr/share/nginx/html" 18 | name: pv-storage 19 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-pvc.yml: -------------------------------------------------------------------------------- 1 | kind: Pod 2 | apiVersion: v1 3 | metadata: 4 | name: pvc-pod 5 | spec: 6 | volumes: 7 | - name: pvc-storage 8 | persistentVolumeClaim: 9 | claimName: pvc 10 | containers: 11 | - name: pvc-container 12 | image: nginx 13 | ports: 14 | - containerPort: 80 15 | name: "http-server" 16 | volumeMounts: 17 | - mountPath: "/usr/share/nginx/html" 18 | name: pvc-storage 19 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-probes.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx-probes 5 | labels: 6 | role: web 7 | spec: 8 | containers: 9 | - name: nginx-probes 10 | image: nginx 11 | readinessProbe: 12 | tcpSocket: 13 | port: 80 14 | initialDelaySeconds: 5 15 | periodSeconds: 10 16 | livenessProbe: 17 | tcpSocket: 18 | port: 80 19 | initialDelaySeconds: 20 20 | periodSeconds: 20 21 | 22 | -------------------------------------------------------------------------------- /week5/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PREFIX="week5" 4 | export SUFFIX="rg" 5 | export RG_NAME=$PREFIX-$SUFFIX 6 | export RG_LOCATION="southcentralus" 7 | export BICEP_FILE="main.bicep" 8 | 9 | # Create the Resource Group to deploy the Webinar Environment 10 | az group create --name $RG_NAME --location $RG_LOCATION 11 | 12 | # Deploy AKS cluster using bicep template 13 | az deployment group create \ 14 | --name bicepk8sdeploy \ 15 | --resource-group $RG_NAME \ 16 | --template-file $BICEP_FILE -------------------------------------------------------------------------------- /week6/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PREFIX="week6" 4 | export SUFFIX="rg" 5 | export RG_NAME=$PREFIX-$SUFFIX 6 | export RG_LOCATION="southcentralus" 7 | export BICEP_FILE="main.bicep" 8 | 9 | # Create the Resource Group to deploy the Webinar Environment 10 | az group create --name $RG_NAME --location $RG_LOCATION 11 | 12 | # Deploy AKS cluster using bicep template 13 | az deployment group create \ 14 | --name bicepk8sdeploy \ 15 | --resource-group $RG_NAME \ 16 | --template-file $BICEP_FILE -------------------------------------------------------------------------------- /week7/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PREFIX="week7" 4 | export SUFFIX="rg" 5 | export RG_NAME=$PREFIX-$SUFFIX 6 | export RG_LOCATION="southcentralus" 7 | export BICEP_FILE="main.bicep" 8 | 9 | # Create the Resource Group to deploy the Webinar Environment 10 | az group create --name $RG_NAME --location $RG_LOCATION 11 | 12 | # Deploy AKS cluster using bicep template 13 | az deployment group create \ 14 | --name bicepk8sdeploy \ 15 | --resource-group $RG_NAME \ 16 | --template-file $BICEP_FILE -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/deploy-redis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis 6 | labels: 7 | app: redis 8 | spec: 9 | selector: 10 | matchLabels: 11 | app: redis 12 | replicas: 13 | template: 14 | metadata: 15 | labels: 16 | app: redis 17 | spec: 18 | containers: 19 | - name: redis 20 | image: redis:alpine 21 | ports: 22 | - containerPort: 6379 23 | name: redis 24 | -------------------------------------------------------------------------------- /week6/manifests/deploy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: dotnetapp 7 | name: dotnetapp 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: dotnetapp 13 | template: 14 | metadata: 15 | labels: 16 | app: dotnetapp 17 | spec: 18 | containers: 19 | - image: mcr.microsoft.com/dotnet/core/samples:aspnetapp 20 | name: samples 21 | ports: 22 | - containerPort: 80 -------------------------------------------------------------------------------- /00-Introduction-and-Installation/sidecar-deploy.sh: -------------------------------------------------------------------------------- 1 | # download the yml 2 | wget https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/main/week1/sidecar-pod.yml 3 | 4 | # (OPTIONAL)remove taint from controller node 5 | # kubectl taint no node1 node-role.kubernetes.io/master:NoSchedule- 6 | 7 | # deploy the pod 8 | kubectl create -f sidecar-pod.yml 9 | 10 | # access logs from the first container 11 | kubectl logs counter count-log-1 12 | 13 | # access logs from the second container 14 | kubectl logs counter count-log-2 15 | 16 | -------------------------------------------------------------------------------- /week6/manifests/cluster-issuer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: app-tls 5 | spec: 6 | acme: 7 | server: https://acme-v02.api.letsencrypt.org/directory 8 | email: chad@cmcrowell.com 9 | privateKeySecretRef: 10 | name: app-tls 11 | solvers: 12 | - http01: 13 | ingress: 14 | class: azure/application-gateway 15 | podTemplate: 16 | spec: 17 | nodeSelector: 18 | 'kubernetes.io/os': linux -------------------------------------------------------------------------------- /week6/manifests/ingress-clippy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: clippy-ing 5 | annotations: 6 | kubernetes.io/ingress.class: addon-http-application-routing 7 | spec: 8 | rules: 9 | - host: party-clippy.01645db0343d4b6b9675.southcentralus.aksapp.io 10 | http: 11 | paths: 12 | - path: / 13 | pathType: Prefix 14 | backend: 15 | service: 16 | name: clippy-svc 17 | port: 18 | number: 80 -------------------------------------------------------------------------------- /01-GitOps-and-Observability/servicemon-otel.yaml: -------------------------------------------------------------------------------- 1 | # servicemon-otel.yaml 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: otel-collector 6 | namespace: monitoring 7 | labels: 8 | release: prometheus-stack # Must match the release name in your Helm chart 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: opentelemetry-collector 13 | endpoints: 14 | - port: prometheus # Make sure this matches your collector service port name 15 | interval: 30s 16 | path: /metrics -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/multi-resource-manifest.yml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: myapp 5 | --- 6 | kind: Pod 7 | apiVersion: v1 8 | metadata: 9 | name: webserver 10 | namespace: myapp 11 | spec: 12 | containers: 13 | - image: httpd 14 | name: httpserver 15 | --- 16 | kind: Pod 17 | apiVersion: v1 18 | metadata: 19 | name: pod-b 20 | namespace: myapp 21 | spec: 22 | containers: 23 | - image: nginx 24 | name: nginxserver 25 | - image: nicolaka/netshoot 26 | name: netshoot 27 | -------------------------------------------------------------------------------- /week5/manifests/deploy-with-sa.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: alpine 5 | labels: 6 | app: alpine 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: alpine 12 | template: 13 | metadata: 14 | labels: 15 | app: alpine 16 | spec: 17 | serviceAccountName: app-sa 18 | containers: 19 | - name: alpine 20 | image: byrnedo/alpine-curl 21 | command: 22 | - "sh" 23 | - "-c" 24 | - "sleep 10000" 25 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-multi-container-vol.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: morevol2 5 | spec: 6 | containers: 7 | - name: centos 8 | image: centos:7 9 | command: 10 | - sleep 11 | - "3600" 12 | volumeMounts: 13 | - mountPath: /centos 14 | name: test 15 | - name: centos2 16 | image: centos:7 17 | command: 18 | - sleep 19 | - "3600" 20 | volumeMounts: 21 | - mountPath: /centos2 22 | name: test 23 | volumes: 24 | - name: test 25 | emptyDir: {} 26 | -------------------------------------------------------------------------------- /week6/manifests/deploy-clippy.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | app: clippy 7 | name: clippy 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: clippy 13 | template: 14 | metadata: 15 | creationTimestamp: null 16 | labels: 17 | app: clippy 18 | spec: 19 | containers: 20 | - image: r.j3ss.co/party-clippy 21 | name: party-clippy 22 | tty: true 23 | command: ["party-clippy"] 24 | ports: 25 | - containerPort: 8080 26 | -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/README.md: -------------------------------------------------------------------------------- 1 | # Finally Understand Kubernetes from Scratch (2025) | Hands-On in Any Cloud 2 | 3 | ![Kubernetes from Scratch Cover](../kubernetes-from-scratch-cover.png) 4 | 5 | ## SECTION 02: Extending Kubernetes with Operators and Custom Resources 6 | 7 | - LESSON 08: [What Are Custom Resource Definitions (CRDs)?](what-are-crds.md) 8 | - LESSON 09: [The Operator Pattern: Controllers That Watch and Act](operator-pattern.md) 9 | - LESSON 10: [Building a Simple Operator with Kubebuilder](build-simple-opeartor.md) 10 | 11 | --- 12 | 13 | [GO BACK](../README.md) -------------------------------------------------------------------------------- /01-GitOps-and-Observability/app-of-apps.yaml: -------------------------------------------------------------------------------- 1 | # app-of-apps.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: app-of-apps 6 | namespace: argocd 7 | spec: 8 | project: default 9 | source: 10 | repoURL: https://github.com/your-username/my-gitops-repo.git 11 | targetRevision: main 12 | path: argocd-apps 13 | directory: 14 | recurse: true # This finds all Application manifests in subdirectories 15 | destination: 16 | server: https://kubernetes.default.svc 17 | namespace: argocd 18 | syncPolicy: 19 | automated: 20 | prune: true 21 | selfHeal: true -------------------------------------------------------------------------------- /week7/manifests/deploy-stress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: stress 6 | name: stress 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: stress 12 | template: 13 | metadata: 14 | labels: 15 | app: stress 16 | spec: 17 | containers: 18 | - image: progrium/stress 19 | name: stress 20 | resources: 21 | requests: 22 | cpu: 250m 23 | limits: 24 | cpu: 500m 25 | command: 26 | - sleep 27 | - "3600" 28 | restartPolicy: Always 29 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pod-wordpress-mysql.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: frontend 5 | spec: 6 | containers: 7 | - name: db 8 | image: mysql 9 | env: 10 | - name: MYSQL_ROOT_PASSWORD 11 | value: "password" 12 | resources: 13 | requests: 14 | memory: "64Mi" 15 | cpu: "250m" 16 | limits: 17 | memory: "128Mi" 18 | cpu: "500m" 19 | - name: wp 20 | image: wordpress 21 | resources: 22 | requests: 23 | memory: "64Mi" 24 | cpu: "250m" 25 | limits: 26 | memory: "128Mi" 27 | cpu: "500m" 28 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/Running-applications-in-Kubernetes.md: -------------------------------------------------------------------------------- 1 | ## Running Applications in Kubernetes 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | ### Commands used in this lesson 13 | 14 | ```bash 15 | # clone the repo 16 | git clone https://github.com/chadmcrowell/k8s-from-scratch.git 17 | ``` 18 | 19 | --- 20 | 21 | [Next Lesson](Making-your-own-image-to-run-in-kubernetes.md) 22 | 23 | [Section 00 - Introduction and Installation](README.md) -------------------------------------------------------------------------------- /01-GitOps-and-Observability/ghost-app.yaml: -------------------------------------------------------------------------------- 1 | # ghost-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: ghost 6 | namespace: argocd 7 | annotations: 8 | argocd.argoproj.io/sync-wave: "3" # Deploy last, after infrastructure 9 | spec: 10 | project: default 11 | source: 12 | repoURL: https://github.com/your-username/your-repo.git 13 | targetRevision: main 14 | path: ghost 15 | destination: 16 | server: https://kubernetes.default.svc 17 | namespace: ghost 18 | syncPolicy: 19 | automated: 20 | prune: true 21 | selfHeal: true 22 | syncOptions: 23 | - CreateNamespace=true -------------------------------------------------------------------------------- /01-GitOps-and-Observability/ingress-nginx-app.yaml: -------------------------------------------------------------------------------- 1 | # ingress-nginx-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: ingress-nginx-app.yaml 6 | namespace: argocd 7 | annotations: 8 | argocd.argoproj.io/sync-wave: "2" # Deploy after cert-manager 9 | spec: 10 | project: default 11 | source: 12 | repoURL: https://github.com/your-username/your-repo.git 13 | targetRevision: main 14 | path: infrastructure/ingress-nginx 15 | destination: 16 | server: https://kubernetes.default.svc 17 | namespace: ingress-nginx 18 | syncPolicy: 19 | automated: 20 | prune: true 21 | selfHeal: true -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/pv-multiple-nfs.yml: -------------------------------------------------------------------------------- 1 | kind: Pod 2 | apiVersion: v1 3 | metadata: 4 | name: nfs-pv-pod 5 | spec: 6 | volumes: 7 | - name: nfs-pv 8 | persistentVolumeClaim: 9 | claimName: nfs-pv-claim 10 | containers: 11 | - name: nfs-client1 12 | image: centos:latest 13 | command: 14 | - sleep 15 | - "3600" 16 | volumeMounts: 17 | - mountPath: "/nfsshare" 18 | name: nfs-pv 19 | - name: nfs-client2 20 | image: centos:latest 21 | command: 22 | - sleep 23 | - "3600" 24 | volumeMounts: 25 | - mountPath: "/nfsshare" 26 | name: nfs-pv 27 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/README.md: -------------------------------------------------------------------------------- 1 | # Finally Understand Kubernetes from Scratch (2025) | Hands-On in Any Cloud 2 | 3 | ![Kubernetes from Scratch Cover](../kubernetes-from-scratch-cover.png) 4 | 5 | ## SECTION 01: GitOps and Observability 6 | 7 | - LESSON 05: [Observability in Kubernetes with Prometheus & Loki](observability-prometheus-and-loki.md) 8 | - LESSON 06: [Deploying Applications with GitOps and ArgoCD](apps-with-gitops-and-argocd.md) 9 | - LESSON 07: [Apps Emitting Metrics, Logs, and Traces with OpenTelemetry](metrics-logs-traces-with-opentelemetry.md) 10 | - LESSON 08: [Visualizing Observability Data with Grafana](visualize-with-grafana.md) 11 | 12 | --- 13 | 14 | [GO BACK](../README.md) 15 | -------------------------------------------------------------------------------- /week4/manifests/project1-deploy-svc-all-in-one.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: aks-app 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: aks-app 10 | template: 11 | metadata: 12 | labels: 13 | app: aks-app 14 | spec: 15 | nodeSelector: 16 | "beta.kubernetes.io/os": linux 17 | containers: 18 | - name: app 19 | image: chadmcrowell/nginx-custom:v1 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: aks-app 27 | spec: 28 | ports: 29 | - port: 80 30 | selector: 31 | app: aks-app 32 | type: LoadBalancer -------------------------------------------------------------------------------- /00-Introduction-and-Installation/nginx-create-image.sh: -------------------------------------------------------------------------------- 1 | # pull the nginx image 2 | docker pull nginx 3 | 4 | # create a basic index.html page 5 | 6 | 7 | 8 | 9 | Custom Nginx 10 | 11 | 12 |

Congrats!! You created a custom container image and deployed it to Kubernetes!!

13 | 14 | 15 | 16 | # create a Dockerfile 17 | FROM nginx:latest 18 | COPY ./index.html /usr/share/nginx/html/index.html 19 | 20 | # build and tag our new image 21 | docker build -t chadmcrowell/nginx-custom:v1 . 22 | 23 | # push the image to docker registry 24 | docker push chadmcrowell/nginx-custom:v1 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/cert-manager-app.yaml: -------------------------------------------------------------------------------- 1 | # cert-manager-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: cert-manager 6 | namespace: argocd 7 | annotations: 8 | argocd.argoproj.io/sync-wave: "1" # Deploy first 9 | spec: 10 | project: default 11 | source: 12 | repoURL: oci://quay.io/jetstack/charts 13 | chart: cert-manager 14 | targetRevision: v1.19.1 15 | helm: 16 | values: | 17 | crds: 18 | enabled: true 19 | destination: 20 | server: https://kubernetes.default.svc 21 | namespace: cert-manager 22 | syncPolicy: 23 | automated: 24 | prune: true 25 | selfHeal: true 26 | syncOptions: 27 | - CreateNamespace=true -------------------------------------------------------------------------------- /week6/manifests/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: app-ing 5 | annotations: 6 | kubernetes.io/ingress.class: azure/application-gateway 7 | certmanager.k8s.io/cluster-issuer: app-tls 8 | appgw.ingress.kubernetes.io/ssl-redirect: 'true' 9 | spec: 10 | tls: 11 | - hosts: 12 | - dotnetapp.southcentralus.cloudapp.azure.com 13 | secretName: app-tls 14 | rules: 15 | - host: dotnetapp.southcentralus.cloudapp.azure.com 16 | http: 17 | paths: 18 | - path: / 19 | pathType: Prefix 20 | backend: 21 | service: 22 | name: dotnetapp-svc 23 | port: 24 | number: 80 -------------------------------------------------------------------------------- /week8/manifests/deploy-prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: prometheus 5 | labels: 6 | app: prometheus 7 | spec: 8 | replicas: 4 9 | selector: 10 | matchLabels: 11 | app: prometheus 12 | template: 13 | metadata: 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/" 17 | prometheus.io/port: "8000" 18 | prometheus.io/scheme: "http" 19 | labels: 20 | app: prometheus 21 | spec: 22 | containers: 23 | - name: prometheus 24 | image: vishiy/tools:prommetricsv5 25 | imagePullPolicy: Always 26 | ports: 27 | - containerPort: 8000 28 | - containerPort: 8080 -------------------------------------------------------------------------------- /week4/manifests/netpolicy-web-80.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: web-netpol 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app: web 9 | policyTypes: 10 | - Ingress 11 | - Egress 12 | ingress: 13 | - from: [] 14 | ports: 15 | - port: 80 16 | protocol: TCP 17 | egress: 18 | - to: 19 | - podSelector: 20 | matchLabels: 21 | app: api 22 | ports: 23 | - port: 3000 24 | protocol: TCP 25 | - to: 26 | - namespaceSelector: 27 | matchLabels: 28 | name: kube-system 29 | podSelector: 30 | matchLabels: 31 | k8s-app: kube-dns 32 | ports: 33 | - port: 53 34 | protocol: UDP -------------------------------------------------------------------------------- /01-GitOps-and-Observability/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-ghost 5 | namespace: ghost 6 | annotations: 7 | nginx.ingress.kubernetes.io/backend-protocol: HTTP 8 | nginx.ingress.kubernetes.io/from-to-www-redirect: "true" 9 | nginx.ingress.kubernetes.io/proxy-body-size: 16m 10 | cert-manager.io/cluster-issuer: "ghost-tls" 11 | spec: 12 | ingressClassName: nginx 13 | tls: 14 | - hosts: 15 | - mycluster.ddns.net 16 | secretName: ghost-tls 17 | rules: 18 | - host: mycluster.ddns.net 19 | http: 20 | paths: 21 | - path: / 22 | pathType: Prefix 23 | backend: 24 | service: 25 | name: ghost 26 | port: 27 | number: 2368 -------------------------------------------------------------------------------- /00-Introduction-and-Installation/README.md: -------------------------------------------------------------------------------- 1 | # Finally Understand Kubernetes from Scratch (2025) | Hands-On in Any Cloud 2 | 3 | ![Kubernetes from Scratch Cover](../kubernetes-from-scratch-cover.png) 4 | 5 | ## SECTION 00: Introduction and Installation 6 | 7 | - LESSON 00: [The How and Why of Kubernetes](The-why-and-how-of-Kubernetes.md) 8 | - LESSON 01: [Running Applications in Kubernetes](Running-applications-in-Kubernetes.md) 9 | - LESSON 02: [Make Your Own Image to Run in Kubernetes](Making-your-own-image-to-run-in-kubernetes.md) 10 | - LESSON 03: [Bootstrap a Cluster with Kubeadm on Any Cloud](bootstrap-cluster-with-kubeadm-on-any-cloud.md) 11 | - LESSON 04: [Install Ghost with Ingress & Cert-manager](ghost-with-ingress-and-cert-manager.md) 12 | 13 | --- 14 | 15 | [GO BACK](../README.md) -------------------------------------------------------------------------------- /01-GitOps-and-Observability/tempo-app.yaml: -------------------------------------------------------------------------------- 1 | # tempo-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: tempo 6 | namespace: argocd 7 | spec: 8 | project: default 9 | sources: 10 | - repoURL: https://grafana.github.io/helm-charts 11 | chart: tempo 12 | targetRevision: 1.24.0 13 | helm: 14 | valueFiles: 15 | - $values/infrastructure/monitoring/tempo/tempo-values.yaml 16 | - repoURL: https://github.com/chadmcrowell/my-gitops-repo.git 17 | targetRevision: main 18 | ref: values 19 | destination: 20 | server: https://kubernetes.default.svc 21 | namespace: monitoring 22 | syncPolicy: 23 | automated: 24 | prune: true 25 | selfHeal: true 26 | syncOptions: 27 | - CreateNamespace=true -------------------------------------------------------------------------------- /01-GitOps-and-Observability/tempo-values.yaml: -------------------------------------------------------------------------------- 1 | # tempo-values.yaml 2 | tempo: 3 | searchEnabled: true 4 | storage: 5 | trace: 6 | backend: s3 7 | s3: 8 | bucket: traces 9 | endpoint: es-mad-1.linodeobjects.com 10 | access_key: $ACCESS_KEY 11 | secret_key: $SECRET_KEY 12 | insecure: true 13 | 14 | receivers: 15 | otlp: 16 | protocols: 17 | http: 18 | endpoint: 0.0.0.0:4318 19 | grpc: 20 | endpoint: 0.0.0.0:4317 21 | 22 | persistence: 23 | enabled: true 24 | size: 10Gi 25 | storageClassName: null # Use default storage class 26 | 27 | serviceMonitor: 28 | enabled: true 29 | 30 | resources: 31 | limits: 32 | memory: 1Gi 33 | cpu: 1000m 34 | requests: 35 | memory: 512Mi 36 | cpu: 250m -------------------------------------------------------------------------------- /00-Introduction-and-Installation/nginx-run-custom-image.sh: -------------------------------------------------------------------------------- 1 | # go to https://labs.play-with-k8s.com 2 | 3 | # create a new instance and follow the instructions to initialize the cluster 4 | 5 | # view the noSchedule taint 6 | kubectl describe node controplane | grep Taints 7 | 8 | # remove the noSchedule taint 9 | kubectl taint no controlplane node-role.kubernetes.io/master:NoSchedule- 10 | 11 | # create a deployment with your custom image 12 | kubectl create deploy custom --image chadmcrowell/nginx-custom:latest 13 | 14 | # describe why pod is not running 15 | kubectl describe po 16 | 17 | # create a service 18 | kubectl expose deploy custom --type=NodePort --port=80 --name=custom-service 19 | 20 | # get services 21 | kubectl get svc 22 | 23 | # curl the service address 24 | curl http:// 25 | 26 | -------------------------------------------------------------------------------- /week4/manifests/netpolicy-api-3000.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: NetworkPolicy 3 | metadata: 4 | name: api-netpol 5 | spec: 6 | podSelector: 7 | matchLabels: 8 | app: api 9 | policyTypes: 10 | - Ingress 11 | - Egress 12 | ingress: 13 | - from: 14 | - podSelector: 15 | matchLabels: 16 | app: web 17 | ports: 18 | - port: 3000 19 | protocol: TCP 20 | egress: 21 | - to: 22 | - podSelector: 23 | matchLabels: 24 | app: db 25 | ports: 26 | - port: 5984 27 | protocol: TCP 28 | - to: 29 | - namespaceSelector: 30 | matchLabels: 31 | name: kube-system 32 | podSelector: 33 | matchLabels: 34 | k8s-app: kube-dns 35 | ports: 36 | - port: 53 37 | protocol: UDP -------------------------------------------------------------------------------- /week8/manifests/deploy-stress-test.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: stress-test 5 | labels: 6 | app: stress 7 | spec: 8 | replicas: 10 9 | selector: 10 | matchLabels: 11 | app: stress 12 | template: 13 | metadata: 14 | annotations: 15 | prometheus.io/scrape: "true" 16 | prometheus.io/path: "/" 17 | prometheus.io/port: "8000" 18 | prometheus.io/scheme: "https" 19 | labels: 20 | app: stress 21 | spec: 22 | containers: 23 | - name: memory-demo-ctr 24 | image: polinux/stress 25 | resources: 26 | limits: 27 | memory: "200Mi" 28 | requests: 29 | memory: "100Mi" 30 | command: ["stress"] 31 | args: ["--vm", "1", "--vm-bytes", "150M", "--vm-hang", "1"] -------------------------------------------------------------------------------- /01-GitOps-and-Observability/loki-app.yaml: -------------------------------------------------------------------------------- 1 | # loki-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: loki 6 | namespace: argocd 7 | annotations: 8 | argocd.argoproj.io/sync-wave: "2" 9 | spec: 10 | project: default 11 | sources: 12 | - repoURL: https://grafana.github.io/helm-charts 13 | chart: loki 14 | targetRevision: "6.x.x" 15 | helm: 16 | releaseName: loki 17 | valueFiles: 18 | - $values/infrastructure/monitoring/loki/values.yaml 19 | - repoURL: https://github.com/your-username/your-repo.git 20 | targetRevision: main 21 | ref: values 22 | destination: 23 | server: https://kubernetes.default.svc 24 | namespace: monitoring 25 | syncPolicy: 26 | automated: 27 | prune: true 28 | selfHeal: true 29 | syncOptions: 30 | - CreateNamespace=true -------------------------------------------------------------------------------- /00-Introduction-and-Installation/manifests/ingress-multi-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: name-virtual-host-ingress 5 | spec: 6 | rules: 7 | - host: first.bar.com 8 | http: 9 | paths: 10 | - pathType: Prefix 11 | path: "/" 12 | backend: 13 | service: 14 | name: service1 15 | port: 16 | number: 80 17 | - host: second.foo.com 18 | http: 19 | paths: 20 | - pathType: Prefix 21 | path: "/" 22 | backend: 23 | service: 24 | name: service2 25 | port: 26 | number: 80 27 | - http: 28 | paths: 29 | - pathType: Prefix 30 | path: "/" 31 | backend: 32 | service: 33 | name: service3 34 | port: 35 | number: 80 36 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ghost 5 | namespace: ghost 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: ghost 11 | template: 12 | metadata: 13 | labels: 14 | app: ghost 15 | spec: 16 | containers: 17 | - name: ghost 18 | image: ghost:6-alpine 19 | ports: 20 | - containerPort: 2368 21 | env: 22 | - name: database__client 23 | value: sqlite3 24 | - name: database__connection__filename 25 | value: /var/lib/ghost/content/data/ghost.db 26 | - name: url 27 | value: https://mycluster.ddns.net 28 | volumeMounts: 29 | - name: ghost-content 30 | mountPath: /var/lib/ghost/content 31 | volumes: 32 | - name: ghost-content 33 | persistentVolumeClaim: 34 | claimName: ghost-pvc -------------------------------------------------------------------------------- /awesome-k8s-resources.md: -------------------------------------------------------------------------------- 1 | # Awesome Kubernetes Resources (Portable & On-Prem Friendly) 2 | 3 | A focused list of vendor-neutral tools you can use anywhere. 4 | 5 | ## GitOps 6 | - **FluxCD** – GitOps toolkit built on Kubernetes CRDs. 7 | - **Argo CD** – Declarative GitOps with Application CRD. 8 | 9 | ## Policy & Security 10 | - **Kyverno** – Kubernetes-native policy engine. 11 | - **Falco** – Runtime security rules for containers and hosts. 12 | - **kube-bench** – CIS Kubernetes benchmark tests. 13 | 14 | ## Networking 15 | - **Cilium** – eBPF-based CNI with observability and policy. 16 | - **Calico** – High-performance policy and routing. 17 | 18 | ## Observability 19 | - **Prometheus** / **Alertmanager** – Metrics + alerting. 20 | - **Grafana** – Dashboards. 21 | - **Loki** – Log aggregation without heavy indexing. 22 | 23 | ## Troubleshooting 24 | - **kubectl-debug** – Ephemeral debugging container. 25 | - **ksniff** – Remote tcpdump captures from pods. 26 | 27 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/sidecar-pod.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: counter 5 | spec: 6 | containers: 7 | - name: count 8 | image: busybox 9 | args: 10 | - /bin/sh 11 | - -c 12 | - > 13 | i=0; 14 | while true; 15 | do 16 | echo "$i: $(date)" >> /var/log/1.log; 17 | echo "$(date) INFO $i" >> /var/log/2.log; 18 | i=$((i+1)); 19 | sleep 1; 20 | done 21 | volumeMounts: 22 | - name: varlog 23 | mountPath: /var/log 24 | - name: count-log-1 25 | image: busybox 26 | args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log'] 27 | volumeMounts: 28 | - name: varlog 29 | mountPath: /var/log 30 | - name: count-log-2 31 | image: busybox 32 | args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log'] 33 | volumeMounts: 34 | - name: varlog 35 | mountPath: /var/log 36 | volumes: 37 | - name: varlog 38 | emptyDir: {} 39 | -------------------------------------------------------------------------------- /week7/manifests/deploy-mysql.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: wordpress-mysql 5 | labels: 6 | app: wordpress 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: wordpress 11 | tier: mysql 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: wordpress 18 | tier: mysql 19 | spec: 20 | containers: 21 | - image: mysql:5.6 22 | name: mysql 23 | env: 24 | - name: MYSQL_ROOT_PASSWORD 25 | valueFrom: 26 | secretKeyRef: 27 | name: mysql-pass 28 | key: password 29 | ports: 30 | - containerPort: 3306 31 | name: mysql 32 | volumeMounts: 33 | - name: mysql-persistent-storage 34 | mountPath: /var/lib/mysql 35 | volumes: 36 | - name: mysql-persistent-storage 37 | persistentVolumeClaim: 38 | claimName: mysql-pv-claim -------------------------------------------------------------------------------- /week4/deploy-aci.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: aci-helloworld 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: aci-helloworld 10 | template: 11 | metadata: 12 | labels: 13 | app: aci-helloworld 14 | spec: 15 | containers: 16 | - name: aci-helloworld 17 | image: mcr.microsoft.com/azuredocs/aci-helloworld 18 | ports: 19 | - containerPort: 80 20 | nodeSelector: 21 | kubernetes.io/role: agent 22 | beta.kubernetes.io/os: linux 23 | type: virtual-kubelet 24 | tolerations: 25 | - key: virtual-kubelet.io/provider 26 | operator: Exists 27 | --- 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | creationTimestamp: null 32 | name: aci-helloworld 33 | spec: 34 | type: LoadBalancer 35 | ports: 36 | - port: 80 37 | protocol: TCP 38 | targetPort: 80 39 | selector: 40 | app: aci-helloworld 41 | status: 42 | loadBalancer: {} -------------------------------------------------------------------------------- /01-GitOps-and-Observability/otel-collector-app.yaml: -------------------------------------------------------------------------------- 1 | # otel-collector-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: opentelemetry-collector 6 | namespace: argocd 7 | spec: 8 | project: default 9 | source: 10 | repoURL: https://open-telemetry.github.io/opentelemetry-helm-charts 11 | chart: opentelemetry-collector 12 | targetRevision: 0.110.0 13 | helm: 14 | valueFiles: 15 | - $values/infrastructure/monitoring/opentelemetry-collector/otel-values.yaml 16 | sources: 17 | - repoURL: https://open-telemetry.github.io/opentelemetry-helm-charts 18 | chart: opentelemetry-collector 19 | targetRevision: 0.110.0 20 | - repoURL: https://github.com/chadmcrowell/my-gitops-repo.git 21 | targetRevision: HEAD 22 | ref: values 23 | destination: 24 | server: https://kubernetes.default.svc 25 | namespace: monitoring 26 | syncPolicy: 27 | automated: 28 | prune: true 29 | selfHeal: true 30 | syncOptions: 31 | - CreateNamespace=true -------------------------------------------------------------------------------- /01-GitOps-and-Observability/setup-repo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create directory structure 4 | mkdir -p infrastructure/cert-manager \ 5 | infrastructure/ingress-nginx \ 6 | infrastructure/monitoring/prometheus \ 7 | infrastructure/monitoring/loki \ 8 | applications/ghost \ 9 | argocd-apps/infrastructure \ 10 | argocd-apps/applications 11 | 12 | # Create placeholder files 13 | touch infrastructure/cert-manager/{values.yaml,clusterissuer.yaml} 14 | touch infrastructure/ingress-nginx/deploy.yaml 15 | touch infrastructure/monitoring/prometheus/values.yaml 16 | touch infrastructure/monitoring/loki/values.yaml 17 | touch applications/ghost/{namespace.yaml,deployment.yaml,service.yaml,ingress.yaml,pvc.yaml} 18 | touch argocd-apps/infrastructure/{cert-manager-app.yaml,ingress-nginx-app.yaml,prometheus-app.yaml,loki-app.yaml} 19 | touch argocd-apps/applications/ghost-app.yaml 20 | touch argocd-apps/app-of-apps.yaml 21 | touch README.md 22 | 23 | echo "✅ GitOps repository structure created successfully!" 24 | tree -L 3 25 | -------------------------------------------------------------------------------- /week7/manifests/deploy-wordpress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: wordpress 5 | labels: 6 | app: wordpress 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: wordpress 11 | tier: frontend 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: wordpress 18 | tier: frontend 19 | spec: 20 | containers: 21 | - image: wordpress:4.8-apache 22 | name: wordpress 23 | env: 24 | - name: WORDPRESS_DB_HOST 25 | value: wordpress-mysql 26 | - name: WORDPRESS_DB_PASSWORD 27 | valueFrom: 28 | secretKeyRef: 29 | name: mysql-pass 30 | key: password 31 | ports: 32 | - containerPort: 80 33 | name: wordpress 34 | volumeMounts: 35 | - name: wordpress-persistent-storage 36 | mountPath: /var/www/html 37 | volumes: 38 | - name: wordpress-persistent-storage 39 | persistentVolumeClaim: 40 | claimName: wp-pv-claim -------------------------------------------------------------------------------- /01-GitOps-and-Observability/prometheus-app.yaml: -------------------------------------------------------------------------------- 1 | # prometheus-app.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: prometheus 6 | namespace: argocd 7 | annotations: 8 | argocd.argoproj.io/sync-wave: "2" 9 | spec: 10 | project: default 11 | sources: 12 | - repoURL: https://prometheus-community.github.io/helm-charts 13 | chart: kube-prometheus-stack 14 | targetRevision: "65.x.x" 15 | helm: 16 | releaseName: prometheus 17 | valueFiles: 18 | - $values/infrastructure/monitoring/prometheus/values.yaml 19 | - repoURL: https://github.com/your-username/your-repo.git 20 | targetRevision: main 21 | ref: values 22 | destination: 23 | server: https://kubernetes.default.svc 24 | namespace: monitoring 25 | syncPolicy: 26 | automated: 27 | prune: true 28 | selfHeal: true 29 | syncOptions: 30 | - CreateNamespace=true 31 | - ServerSideApply=true 32 | ignoreDifferences: 33 | - group: apps 34 | kind: Deployment 35 | jqPathExpressions: 36 | - .spec.template.metadata.annotations -------------------------------------------------------------------------------- /kustomization-examples.yaml: -------------------------------------------------------------------------------- 1 | # Three portable overlays (dev/stage/prod) demonstrating namePrefix, labels, and patches. 2 | # Directory suggestion: 3 | # base/ (deployment+service) 4 | # overlays/dev|stage|prod/ 5 | 6 | apiVersion: kustomize.config.k8s.io/v1beta1 7 | kind: Kustomization 8 | resources: 9 | - ./base 10 | namePrefix: dev- 11 | commonLabels: 12 | app.kubernetes.io/part-of: kfs 13 | app.kubernetes.io/environment: dev 14 | patchesStrategicMerge: 15 | - ./overlays/dev/deploy-patch.yaml 16 | --- 17 | apiVersion: kustomize.config.k8s.io/v1beta1 18 | kind: Kustomization 19 | resources: 20 | - ./base 21 | namePrefix: stage- 22 | commonLabels: 23 | app.kubernetes.io/part-of: kfs 24 | app.kubernetes.io/environment: stage 25 | patchesStrategicMerge: 26 | - ./overlays/stage/deploy-patch.yaml 27 | --- 28 | apiVersion: kustomize.config.k8s.io/v1beta1 29 | kind: Kustomization 30 | resources: 31 | - ./base 32 | namePrefix: prod- 33 | commonLabels: 34 | app.kubernetes.io/part-of: kfs 35 | app.kubernetes.io/environment: prod 36 | patchesStrategicMerge: 37 | - ./overlays/prod/deploy-patch.yaml 38 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/The-why-and-how-of-Kubernetes.md: -------------------------------------------------------------------------------- 1 | ## The Why and How of Kubernetes 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch/edit-lesson/sections/761976/lessons/2889184) 4 | 5 | --- 6 | 7 | ### Kubernetes Components 8 | - [https://kubernetes.io/docs/concepts/overview/components/](https://kubernetes.io/docs/concepts/overview/components/) 9 | 10 | ### Killercoda Lab Environment 11 | - [https://killercoda.com/](https://killercoda.com) 12 | 13 | ### GitHub Repsitory Used in this Course 14 | - [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 15 | 16 | ### Commands used in this lesson 17 | 18 | ```bash 19 | # list the nodes in your Kubernetes cluster 20 | kubectl get nodes 21 | 22 | # list the pods in the kube-system namespace 23 | kubectl -n kube-system get pods 24 | 25 | # see which node the pod is running on 26 | kubectl -n kube-system get pods -o wide 27 | ``` 28 | 29 | --- 30 | 31 | [Next Lesson](Running-applications-in-Kubernetes.md) 32 | 33 | [Section 00 - Introduction and Installation](README.md) -------------------------------------------------------------------------------- /week4/manifests/deploy-app-routing.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: aks-helloworld 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: aks-helloworld 10 | template: 11 | metadata: 12 | labels: 13 | app: aks-helloworld 14 | spec: 15 | containers: 16 | - name: aks-helloworld 17 | image: mcr.microsoft.com/azuredocs/aks-helloworld:v1 18 | ports: 19 | - containerPort: 80 20 | env: 21 | - name: TITLE 22 | value: "Welcome to Azure Kubernetes Service (AKS)" 23 | --- 24 | apiVersion: v1 25 | kind: Service 26 | metadata: 27 | name: aks-helloworld 28 | spec: 29 | type: ClusterIP 30 | ports: 31 | - port: 80 32 | selector: 33 | app: aks-helloworld 34 | --- 35 | apiVersion: networking.k8s.io/v1 36 | kind: Ingress 37 | metadata: 38 | name: aks-helloworld 39 | annotations: 40 | kubernetes.io/ingress.class: addon-http-application-routing 41 | spec: 42 | rules: 43 | - host: aks-helloworld. 44 | http: 45 | paths: 46 | - path: / 47 | pathType: Prefix 48 | backend: 49 | service: 50 | name: aks-helloworld 51 | port: 52 | number: 80 -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the Go source (relies on .dockerignore to filter) 15 | COPY . . 16 | 17 | # Build 18 | # the GOARCH has no default value to allow the binary to be built according to the host where the command 19 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 20 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 21 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 22 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -a -o manager ./cmd/main.go 23 | 24 | 25 | # Use distroless as minimal base image to package the manager binary 26 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 27 | FROM gcr.io/distroless/static:nonroot 28 | WORKDIR / 29 | COPY --from=builder /workspace/manager . 30 | USER 65532:65532 31 | 32 | ENTRYPOINT ["/manager"] -------------------------------------------------------------------------------- /01-GitOps-and-Observability/kps-values.yaml: -------------------------------------------------------------------------------- 1 | # kps-values.yaml 2 | fullnameOverride: prometheus-stack 3 | 4 | # Discover ServiceMonitors/PodMonitors cluster-wide (not just by Helm labels) 5 | prometheus: 6 | prometheusSpec: 7 | serviceMonitorSelectorNilUsesHelmValues: false 8 | podMonitorSelectorNilUsesHelmValues: false 9 | retention: 15d 10 | scrapeInterval: 30s 11 | ruleSelectorNilUsesHelmValues: false 12 | resources: 13 | requests: 14 | cpu: "250m" 15 | memory: "1Gi" 16 | limits: 17 | memory: "4Gi" 18 | 19 | # Alertmanager minimal, add your routes later 20 | alertmanager: 21 | alertmanagerSpec: 22 | replicas: 2 23 | resources: 24 | requests: 25 | cpu: "100m" 26 | memory: "256Mi" 27 | limits: 28 | memory: "1Gi" 29 | 30 | # Grafana: set your own admin password and expose via NodeBalancer 31 | grafana: 32 | adminUser: admin 33 | adminPassword: "superSecretPassword!!" 34 | service: 35 | type: ClusterIP 36 | port: 80 37 | targetPort: 3000 38 | grafana.ini: 39 | server: 40 | root_url: "%(protocol)s://%(domain)s/" 41 | persistence: 42 | enabled: true 43 | size: 10Gi 44 | sidecar: 45 | dashboards: 46 | enabled: true 47 | datasources: 48 | enabled: true 49 | 50 | # kube-state-metrics & node-exporter are on by default in recent releases, 51 | # keep them enabled for cluster debugging: 52 | kubeStateMetrics: 53 | enabled: true 54 | nodeExporter: 55 | enabled: true -------------------------------------------------------------------------------- /networkpolicy-rbac-examples.yaml: -------------------------------------------------------------------------------- 1 | # Namespace default deny + limited read RBAC 2 | 3 | # Deny all ingress/egress by default in ns: apps 4 | apiVersion: v1 5 | kind: Namespace 6 | metadata: 7 | name: apps 8 | labels: 9 | name: apps 10 | --- 11 | apiVersion: networking.k8s.io/v1 12 | kind: NetworkPolicy 13 | metadata: 14 | name: default-deny-all 15 | namespace: apps 16 | spec: 17 | podSelector: {} 18 | policyTypes: ["Ingress","Egress"] 19 | --- 20 | # Allow egress DNS + HTTP/HTTPS for update checks 21 | apiVersion: networking.k8s.io/v1 22 | kind: NetworkPolicy 23 | metadata: 24 | name: allow-egress-core 25 | namespace: apps 26 | spec: 27 | podSelector: {} 28 | policyTypes: ["Egress"] 29 | egress: 30 | - to: 31 | - namespaceSelector: {} 32 | ports: 33 | - protocol: UDP 34 | port: 53 35 | - protocol: TCP 36 | port: 53 37 | - protocol: TCP 38 | port: 80 39 | - protocol: TCP 40 | port: 443 41 | --- 42 | # RBAC: namespace-scoped read-only 43 | apiVersion: rbac.authorization.k8s.io/v1 44 | kind: Role 45 | metadata: 46 | name: read-only 47 | namespace: apps 48 | rules: 49 | - apiGroups: [""] 50 | resources: ["pods","services","endpoints","configmaps"] 51 | verbs: ["get","list","watch"] 52 | --- 53 | apiVersion: rbac.authorization.k8s.io/v1 54 | kind: RoleBinding 55 | metadata: 56 | name: read-only-binding 57 | namespace: apps 58 | subjects: 59 | - kind: User 60 | name: developer@example.com 61 | apiGroup: rbac.authorization.k8s.io 62 | roleRef: 63 | kind: Role 64 | name: read-only 65 | apiGroup: rbac.authorization.k8s.io 66 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/Making-your-own-image-to-run-in-kubernetes.md: -------------------------------------------------------------------------------- 1 | ## Make Your Own Image to Run in Kubernetes 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | ### Commands used in this lesson 13 | 14 | ```bash 15 | # make a new directory & change into that directory 16 | mkdir -p working && cd working 17 | 18 | # pull down the image from dockerhub 19 | docker pull nginx:latest 20 | 21 | # list the images 22 | docker images 23 | 24 | # download the index.html file 25 | wget https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/refs/heads/main/0-Introduction-and-Installation/index.html 26 | 27 | # download the Dockerfile 28 | wget https://raw.githubusercontent.com/chadmcrowell/k8s-from-scratch/refs/heads/main/0-Introduction-and-Installation/Dockerfile 29 | 30 | # build the container image 31 | docker build -t chadmcrowell/nginx-custom:v1 . 32 | 33 | # list the images 34 | docker images 35 | 36 | # push the image to dockerhub 37 | docker push chadmcrowell/nginx-custom:v1 38 | 39 | # create deployment in Kubernetes 40 | kubectl create deploy custom --image chadmcrowell/nginx-custom:v1 41 | 42 | # create a NodePort type service 43 | kubectl expose deploy custom --type NodePort --port 80 44 | 45 | # list pods 46 | kubectl get pods 47 | 48 | # list services (get node port) 49 | kubectl get svc 50 | ``` 51 | 52 | --- 53 | 54 | [Next Lesson](bootstrap-cluster-with-kubeadm-on-any-cloud.md) 55 | 56 | [Section 00 - Introduction and Installation](README.md) -------------------------------------------------------------------------------- /networkpolicy-rbac-variations.yaml: -------------------------------------------------------------------------------- 1 | # Variations: allow namespace-isolated HTTP and a cluster-wide viewer role 2 | 3 | # Allow only traffic from same namespace to port 8080 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | name: allow-same-namespace-http 8 | namespace: apps 9 | spec: 10 | podSelector: {} 11 | policyTypes: ["Ingress"] 12 | ingress: 13 | - from: 14 | - podSelector: {} # same-namespace pods 15 | ports: 16 | - protocol: TCP 17 | port: 8080 18 | --- 19 | # Allow prometheus scraping from monitoring namespace 20 | apiVersion: networking.k8s.io/v1 21 | kind: NetworkPolicy 22 | metadata: 23 | name: allow-prometheus-scrape 24 | namespace: apps 25 | spec: 26 | podSelector: 27 | matchLabels: 28 | app.kubernetes.io/name: my-app 29 | policyTypes: ["Ingress"] 30 | ingress: 31 | - from: 32 | - namespaceSelector: 33 | matchLabels: 34 | name: monitoring 35 | ports: 36 | - protocol: TCP 37 | port: 9090 38 | --- 39 | # Cluster-wide read-only (viewer) without write verbs 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: ClusterRole 42 | metadata: 43 | name: cluster-viewer-lite 44 | rules: 45 | - apiGroups: ["*"] 46 | resources: ["*"] 47 | verbs: ["get","list","watch"] 48 | --- 49 | apiVersion: rbac.authorization.k8s.io/v1 50 | kind: ClusterRoleBinding 51 | metadata: 52 | name: cluster-viewer-lite-binding 53 | subjects: 54 | - kind: User 55 | name: developer@example.com 56 | apiGroup: rbac.authorization.k8s.io 57 | roleRef: 58 | kind: ClusterRole 59 | name: cluster-viewer-lite 60 | apiGroup: rbac.authorization.k8s.io 61 | -------------------------------------------------------------------------------- /helm-values-examples.yaml: -------------------------------------------------------------------------------- 1 | # Example Helm values for common CNCF components (portable, on-prem friendly) 2 | # Use with: helm install -n -f thisfile.yaml --create-namespace 3 | 4 | # --- Kyverno --- 5 | kyverno: 6 | replicaCount: 2 7 | image: 8 | pullPolicy: IfNotPresent 9 | resources: 10 | requests: { cpu: 100m, memory: 128Mi } 11 | limits: { cpu: 200m, memory: 256Mi } 12 | admissionController: 13 | podSecurity: 14 | enabled: true # enable PSA baseline/restricted translation 15 | reportsController: 16 | enabled: true 17 | 18 | # --- Falco (Helm chart: falcosecurity/falco) --- 19 | falco: 20 | driver: 21 | kind: modern_ebpf 22 | falco: 23 | rulesFiles: 24 | - /etc/falco/rules.d 25 | resources: 26 | requests: { cpu: 100m, memory: 256Mi } 27 | limits: { cpu: 300m, memory: 512Mi } 28 | extra: 29 | env: 30 | FALCO_BPF_PROBE: "" 31 | 32 | # --- Prometheus (kube-prometheus-stack minimal) --- 33 | kube-prometheus-stack: 34 | grafana: 35 | enabled: true 36 | adminPassword: "admin" 37 | prometheus: 38 | prometheusSpec: 39 | retention: 24h 40 | retentionSize: 5GiB 41 | resources: 42 | requests: { cpu: 200m, memory: 512Mi } 43 | limits: { cpu: 500m, memory: 1Gi } 44 | alertmanager: 45 | enabled: true 46 | 47 | # --- Ingress NGINX --- 48 | ingress-nginx: 49 | controller: 50 | replicaCount: 2 51 | resources: 52 | requests: { cpu: 100m, memory: 128Mi } 53 | limits: { cpu: 300m, memory: 256Mi } 54 | admissionWebhooks: 55 | enabled: true 56 | 57 | # NOTES: 58 | # - Keep charts pinned with --version to ensure reproducibility. 59 | # - Tune resources for your nodes; defaults are intentionally conservative. 60 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/ghost-deploy-with-otel.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ghost 5 | namespace: ghost 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: ghost 11 | template: 12 | metadata: 13 | labels: 14 | app: ghost 15 | spec: 16 | containers: 17 | - name: ghost 18 | image: chadmcrowell/ghost-otel:latest 19 | ports: 20 | - containerPort: 2368 21 | env: 22 | - name: database__client 23 | value: sqlite3 24 | - name: database__connection__filename 25 | value: /var/lib/ghost/content/data/ghost.db 26 | - name: url 27 | value: https://mycluster.ddns.net 28 | - name: NODE_OPTIONS 29 | value: "--require @opentelemetry/auto-instrumentations-node/register" 30 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 31 | value: "http://otel-collector.ghost.svc.cluster.local:4318" 32 | - name: OTEL_SERVICE_NAME 33 | value: "ghost-blog" 34 | - name: OTEL_TRACES_EXPORTER 35 | value: "otlp" 36 | - name: OTEL_METRICS_EXPORTER 37 | value: "otlp" 38 | - name: OTEL_LOGS_EXPORTER 39 | value: "otlp" 40 | - name: OTEL_TRACES_SAMPLER 41 | value: "traceidratio" 42 | - name: OTEL_TRACES_SAMPLER_ARG 43 | value: "0.5" # Sample 50% of traces 44 | - name: OTEL_NODE_DISABLED_INSTRUMENTATIONS 45 | value: "fs,dns" # Reduce noise by disabling filesystem and DNS tracing 46 | volumeMounts: 47 | - name: ghost-content 48 | mountPath: /var/lib/ghost/content 49 | volumes: 50 | - name: ghost-content 51 | persistentVolumeClaim: 52 | claimName: ghost-pvc -------------------------------------------------------------------------------- /kyverno-falco-policies.yaml: -------------------------------------------------------------------------------- 1 | # Secure-by-default guardrails with Kyverno + Falco 2 | 3 | # Kyverno: Disallow hostPath 4 | apiVersion: kyverno.io/v1 5 | kind: ClusterPolicy 6 | metadata: 7 | name: disallow-hostpath 8 | spec: 9 | validationFailureAction: enforce 10 | rules: 11 | - name: no-hostpath 12 | match: 13 | resources: 14 | kinds: ["Pod"] 15 | validate: 16 | message: "HostPath volumes are not allowed." 17 | pattern: 18 | spec: 19 | volumes: 20 | - name: "*" 21 | =(hostPath): "null" 22 | --- 23 | # Kyverno: Require runAsNonRoot & readOnlyRootFilesystem 24 | apiVersion: kyverno.io/v1 25 | kind: ClusterPolicy 26 | metadata: 27 | name: require-secure-pod-options 28 | spec: 29 | validationFailureAction: enforce 30 | rules: 31 | - name: require-nonroot 32 | match: 33 | resources: 34 | kinds: ["Pod"] 35 | validate: 36 | message: "Containers must run as non-root with readOnlyRootFilesystem." 37 | pattern: 38 | spec: 39 | securityContext: 40 | runAsNonRoot: true 41 | containers: 42 | - name: "*" 43 | securityContext: 44 | runAsNonRoot: true 45 | readOnlyRootFilesystem: true 46 | --- 47 | # Falco rule: Detect terminal shells in containers 48 | apiVersion: falco.org/v1alpha1 49 | kind: FalcoRule 50 | metadata: 51 | name: detect-shell-in-container 52 | spec: 53 | rules: 54 | - rule: Terminal shell in container 55 | desc: Detect shells running in a container 56 | condition: > 57 | spawned_process and container and proc.name in (bash, sh, zsh, ash) 58 | output: > 59 | Shell spawned in container (user=%user.name process=%proc.name container_id=%container.id image=%container.image.repository) 60 | priority: Notice 61 | tags: [process, container, mitre_t1059] 62 | -------------------------------------------------------------------------------- /01-GitOps-and-Observability/otel-values.yaml: -------------------------------------------------------------------------------- 1 | # otel-values.yaml 2 | mode: deployment 3 | 4 | image: 5 | repository: otel/opentelemetry-collector-contrib 6 | tag: 0.110.0 7 | 8 | config: 9 | receivers: 10 | otlp: 11 | protocols: 12 | http: 13 | endpoint: 0.0.0.0:4318 14 | grpc: 15 | endpoint: 0.0.0.0:4317 16 | 17 | processors: 18 | batch: 19 | timeout: 10s 20 | send_batch_size: 1024 21 | 22 | memory_limiter: 23 | check_interval: 1s 24 | limit_percentage: 75 25 | spike_limit_percentage: 15 26 | 27 | exporters: 28 | # For Prometheus - expose metrics endpoint 29 | prometheus: 30 | endpoint: "0.0.0.0:8889" 31 | 32 | # For Tempo - send traces 33 | otlp/tempo: 34 | endpoint: "tempo.monitoring.svc.cluster.local:4317" 35 | tls: 36 | insecure: true 37 | 38 | # For Loki - send logs 39 | loki: 40 | endpoint: "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" 41 | 42 | service: 43 | pipelines: 44 | traces: 45 | receivers: [otlp] 46 | processors: [memory_limiter, batch] 47 | exporters: [otlp/tempo] 48 | 49 | metrics: 50 | receivers: [otlp] 51 | processors: [memory_limiter, batch] 52 | exporters: [prometheus] 53 | 54 | logs: 55 | receivers: [otlp] 56 | processors: [memory_limiter, batch] 57 | exporters: [loki] 58 | 59 | ports: 60 | otlp: 61 | enabled: true 62 | containerPort: 4317 63 | servicePort: 4317 64 | protocol: TCP 65 | otlp-http: 66 | enabled: true 67 | containerPort: 4318 68 | servicePort: 4318 69 | protocol: TCP 70 | prometheus: 71 | enabled: true 72 | containerPort: 8889 73 | servicePort: 8889 74 | protocol: TCP 75 | 76 | resources: 77 | limits: 78 | memory: 512Mi 79 | cpu: 500m 80 | requests: 81 | memory: 256Mi 82 | cpu: 100m -------------------------------------------------------------------------------- /01-GitOps-and-Observability/loki-values.yaml: -------------------------------------------------------------------------------- 1 | # loki-values.yaml 2 | loki: 3 | commonConfig: 4 | replication_factor: 1 5 | schemaConfig: 6 | configs: 7 | - from: "2024-04-01" 8 | store: tsdb 9 | object_store: s3 10 | schema: v13 11 | index: 12 | prefix: loki_index_ 13 | period: 24h 14 | pattern_ingester: 15 | enabled: true 16 | limits_config: 17 | allow_structured_metadata: true 18 | volume_enabled: true 19 | ruler: 20 | enable_api: true 21 | storage: 22 | type: 's3' 23 | bucketNames: 24 | chunks: loki-logs 25 | ruler: loki-logs 26 | admin: loki-logs 27 | s3: 28 | endpoint: "es-mad-1.linodeobjects.com" 29 | region: "es-mad-1" 30 | s3ForcePathStyle: true 31 | insecure: false 32 | chunksCache: 33 | enabled: false 34 | 35 | minio: 36 | enabled: false 37 | 38 | deploymentMode: SingleBinary 39 | 40 | singleBinary: 41 | replicas: 1 42 | resources: 43 | requests: 44 | cpu: 200m 45 | memory: 512Mi 46 | limits: 47 | cpu: 500m 48 | memory: 1Gi 49 | persistence: 50 | enabled: true 51 | size: 10Gi 52 | storageClass: linode-block-storage-retain 53 | extraEnv: 54 | - name: AWS_ACCESS_KEY_ID 55 | valueFrom: 56 | secretKeyRef: 57 | name: loki-object-storage 58 | key: AWS_ACCESS_KEY_ID 59 | - name: AWS_SECRET_ACCESS_KEY 60 | valueFrom: 61 | secretKeyRef: 62 | name: loki-object-storage 63 | key: AWS_SECRET_ACCESS_KEY 64 | 65 | # Zero out replica counts of other deployment modes 66 | backend: 67 | replicas: 0 68 | read: 69 | replicas: 0 70 | write: 71 | replicas: 0 72 | 73 | ingester: 74 | replicas: 0 75 | querier: 76 | replicas: 0 77 | queryFrontend: 78 | replicas: 0 79 | queryScheduler: 80 | replicas: 0 81 | distributor: 82 | replicas: 0 83 | compactor: 84 | replicas: 0 85 | indexGateway: 86 | replicas: 0 87 | bloomCompactor: 88 | replicas: 0 89 | bloomGateway: 90 | replicas: 0 -------------------------------------------------------------------------------- /week4/manifests/deploy-policy-all-db-api-web.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: db 5 | labels: 6 | app: db 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: db 12 | template: 13 | metadata: 14 | labels: 15 | app: db 16 | spec: 17 | containers: 18 | - name: couchdb 19 | image: couchdb:2.3.0 20 | ports: 21 | - containerPort: 5984 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: db 27 | spec: 28 | selector: 29 | app: db 30 | ports: 31 | - name: db 32 | port: 15984 33 | targetPort: 5984 34 | type: ClusterIP 35 | --- 36 | apiVersion: apps/v1 37 | kind: Deployment 38 | metadata: 39 | name: api 40 | labels: 41 | app: api 42 | spec: 43 | replicas: 1 44 | selector: 45 | matchLabels: 46 | app: api 47 | template: 48 | metadata: 49 | labels: 50 | app: api 51 | spec: 52 | containers: 53 | - name: nodebrady 54 | image: mabenoit/nodebrady 55 | ports: 56 | - containerPort: 3000 57 | --- 58 | apiVersion: v1 59 | kind: Service 60 | metadata: 61 | name: api 62 | spec: 63 | selector: 64 | app: api 65 | ports: 66 | - name: api 67 | port: 8080 68 | targetPort: 3000 69 | type: ClusterIP 70 | --- 71 | apiVersion: apps/v1 72 | kind: Deployment 73 | metadata: 74 | name: web 75 | labels: 76 | app: web 77 | spec: 78 | replicas: 1 79 | selector: 80 | matchLabels: 81 | app: web 82 | template: 83 | metadata: 84 | labels: 85 | app: web 86 | spec: 87 | containers: 88 | - name: nginx 89 | image: nginx 90 | ports: 91 | - containerPort: 80 92 | --- 93 | apiVersion: v1 94 | kind: Service 95 | metadata: 96 | name: web 97 | spec: 98 | selector: 99 | app: web 100 | ports: 101 | - name: web 102 | port: 80 103 | targetPort: 80 104 | type: LoadBalancer -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # mac 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /kustomization-patches.yaml: -------------------------------------------------------------------------------- 1 | # Example base Deployment and three overlay patches 2 | 3 | # base/deployment.yaml 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: my-app 8 | labels: { app: my-app } 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: { app: my-app } 13 | template: 14 | metadata: 15 | labels: { app: my-app } 16 | spec: 17 | containers: 18 | - name: my-app 19 | image: ghcr.io/example/my-app:1.0.0 20 | ports: [{ containerPort: 8080 }] 21 | readinessProbe: 22 | httpGet: { path: /healthz, port: 8080 } 23 | initialDelaySeconds: 5 24 | resources: 25 | requests: { cpu: 50m, memory: 64Mi } 26 | limits: { cpu: 200m, memory: 128Mi } 27 | --- 28 | # overlays/dev/deploy-patch.yaml 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: my-app 33 | spec: 34 | replicas: 1 35 | template: 36 | spec: 37 | containers: 38 | - name: my-app 39 | image: ghcr.io/example/my-app:1.0.0-dev 40 | env: 41 | - name: LOG_LEVEL 42 | value: "debug" 43 | --- 44 | # overlays/stage/deploy-patch.yaml 45 | apiVersion: apps/v1 46 | kind: Deployment 47 | metadata: 48 | name: my-app 49 | spec: 50 | replicas: 2 51 | template: 52 | spec: 53 | containers: 54 | - name: my-app 55 | image: ghcr.io/example/my-app:1.0.0-rc 56 | resources: 57 | requests: { cpu: 100m, memory: 128Mi } 58 | limits: { cpu: 300m, memory: 256Mi } 59 | --- 60 | # overlays/prod/deploy-patch.yaml 61 | apiVersion: apps/v1 62 | kind: Deployment 63 | metadata: 64 | name: my-app 65 | spec: 66 | replicas: 3 67 | template: 68 | spec: 69 | topologySpreadConstraints: 70 | - maxSkew: 1 71 | topologyKey: kubernetes.io/hostname 72 | whenUnsatisfiable: ScheduleAnyway 73 | labelSelector: 74 | matchLabels: { app: my-app } 75 | containers: 76 | - name: my-app 77 | image: ghcr.io/example/my-app:1.0.0 78 | resources: 79 | requests: { cpu: 150m, memory: 192Mi } 80 | limits: { cpu: 500m, memory: 384Mi } 81 | -------------------------------------------------------------------------------- /custom-resource-definitions.yaml: -------------------------------------------------------------------------------- 1 | # Minimal self-contained CRDs useful for operator exercises and demos. 2 | # These CRDs are examples (group "ops.kubeskills.io") and safe to apply to any cluster. 3 | 4 | apiVersion: apiextensions.k8s.io/v1 5 | kind: CustomResourceDefinition 6 | metadata: 7 | name: apps.ops.kubeskills.io 8 | spec: 9 | group: ops.kubeskills.io 10 | names: 11 | kind: App 12 | plural: apps 13 | singular: app 14 | shortNames: ["kfsapp"] 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | served: true 19 | storage: true 20 | schema: 21 | openAPIV3Schema: 22 | type: object 23 | properties: 24 | spec: 25 | type: object 26 | required: ["image"] 27 | properties: 28 | image: 29 | type: string 30 | pattern: "^[\w./:-]+$" 31 | replicas: 32 | type: integer 33 | minimum: 0 34 | env: 35 | type: array 36 | items: 37 | type: object 38 | required: ["name","value"] 39 | properties: 40 | name: { type: string } 41 | value: { type: string } 42 | status: 43 | type: object 44 | properties: 45 | readyReplicas: { type: integer } 46 | --- 47 | apiVersion: apiextensions.k8s.io/v1 48 | kind: CustomResourceDefinition 49 | metadata: 50 | name: policies.ops.kubeskills.io 51 | spec: 52 | group: ops.kubeskills.io 53 | names: 54 | kind: Policy 55 | plural: policies 56 | singular: policy 57 | shortNames: ["kfspol"] 58 | scope: Cluster 59 | versions: 60 | - name: v1alpha1 61 | served: true 62 | storage: true 63 | schema: 64 | openAPIV3Schema: 65 | type: object 66 | properties: 67 | spec: 68 | type: object 69 | properties: 70 | type: 71 | type: string 72 | enum: ["deny-egress","image-allowlist","pss-restricted"] 73 | params: 74 | type: object 75 | additionalProperties: true 76 | -------------------------------------------------------------------------------- /week5/main.bicep: -------------------------------------------------------------------------------- 1 | // params 2 | @description('The DNS prefix to use with hosted Kubernetes API server FQDN.') 3 | param dnsPrefix string = 'aks' 4 | 5 | @description('The name of the Managed Cluster resource.') 6 | param clusterName string = 'cluster' 7 | 8 | @description('Specifies the Azure location where the key vault should be created.') 9 | param location string = resourceGroup().location 10 | 11 | @minValue(1) 12 | @maxValue(50) 13 | @description('The number of nodes for the cluster. 1 Node is enough for Dev/Test and minimum 3 nodes, is recommended for Production') 14 | param agentCount int = 2 15 | 16 | @description('The size of the Virtual Machine.') 17 | param agentVMSize string = 'Standard_B2s' 18 | 19 | // vars 20 | var kubernetesVersion = '1.20.7' 21 | var subnetRef = '${vn.id}/subnets/${subnetName}' 22 | var addressPrefix = '20.0.0.0/16' 23 | var subnetName = 'Subnet01' 24 | var subnetPrefix = '20.0.0.0/24' 25 | var virtualNetworkName = 'AKSVNET02' 26 | var nodeResourceGroup = 'rg-${dnsPrefix}-${clusterName}' 27 | var tags = { 28 | environment: 'production' 29 | vmssValue: 'true' 30 | projectCode: '264082' 31 | } 32 | var agentPoolName = 'linuxpool' 33 | 34 | // Azure virtual network 35 | resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = { 36 | name: virtualNetworkName 37 | location: location 38 | tags: tags 39 | properties: { 40 | addressSpace: { 41 | addressPrefixes: [ 42 | addressPrefix 43 | ] 44 | } 45 | subnets: [ 46 | { 47 | name: subnetName 48 | properties: { 49 | addressPrefix: subnetPrefix 50 | } 51 | } 52 | ] 53 | } 54 | } 55 | 56 | // Azure kubernetes service 57 | resource aks 'Microsoft.ContainerService/managedClusters@2020-09-01' = { 58 | name: clusterName 59 | location: location 60 | tags: tags 61 | identity: { 62 | type: 'SystemAssigned' 63 | } 64 | properties: { 65 | kubernetesVersion: kubernetesVersion 66 | enableRBAC: true 67 | dnsPrefix: dnsPrefix 68 | agentPoolProfiles: [ 69 | { 70 | name: agentPoolName 71 | count: agentCount 72 | mode: 'System' 73 | vmSize: agentVMSize 74 | type: 'VirtualMachineScaleSets' 75 | osType: 'Linux' 76 | enableAutoScaling: false 77 | vnetSubnetID: subnetRef 78 | } 79 | ] 80 | servicePrincipalProfile: { 81 | clientId: 'msi' 82 | } 83 | nodeResourceGroup: nodeResourceGroup 84 | networkProfile: { 85 | networkPlugin: 'azure' 86 | loadBalancerSku: 'standard' 87 | } 88 | } 89 | } 90 | 91 | output id string = aks.id 92 | output apiServerAddress string = aks.properties.fqdn 93 | -------------------------------------------------------------------------------- /week6/main.bicep: -------------------------------------------------------------------------------- 1 | // params 2 | @description('The DNS prefix to use with hosted Kubernetes API server FQDN.') 3 | param dnsPrefix string = 'aks' 4 | 5 | @description('The name of the Managed Cluster resource.') 6 | param clusterName string = 'cluster' 7 | 8 | @description('Specifies the Azure location where the key vault should be created.') 9 | param location string = resourceGroup().location 10 | 11 | @minValue(1) 12 | @maxValue(50) 13 | @description('The number of nodes for the cluster. 1 Node is enough for Dev/Test and minimum 3 nodes, is recommended for Production') 14 | param agentCount int = 2 15 | 16 | @description('The size of the Virtual Machine.') 17 | param agentVMSize string = 'Standard_B2s' 18 | 19 | // vars 20 | var kubernetesVersion = '1.20.7' 21 | var subnetRef = '${vn.id}/subnets/${subnetName}' 22 | var addressPrefix = '20.0.0.0/16' 23 | var subnetName = 'Subnet01' 24 | var subnetPrefix = '20.0.0.0/24' 25 | var virtualNetworkName = 'AKSVNET02' 26 | var nodeResourceGroup = 'rg-${dnsPrefix}-${clusterName}' 27 | var tags = { 28 | environment: 'production' 29 | vmssValue: 'true' 30 | projectCode: '264082' 31 | } 32 | var agentPoolName = 'linuxpool' 33 | 34 | // Azure virtual network 35 | resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = { 36 | name: virtualNetworkName 37 | location: location 38 | tags: tags 39 | properties: { 40 | addressSpace: { 41 | addressPrefixes: [ 42 | addressPrefix 43 | ] 44 | } 45 | subnets: [ 46 | { 47 | name: subnetName 48 | properties: { 49 | addressPrefix: subnetPrefix 50 | } 51 | } 52 | ] 53 | } 54 | } 55 | 56 | // Azure kubernetes service 57 | resource aks 'Microsoft.ContainerService/managedClusters@2020-09-01' = { 58 | name: clusterName 59 | location: location 60 | tags: tags 61 | identity: { 62 | type: 'SystemAssigned' 63 | } 64 | properties: { 65 | kubernetesVersion: kubernetesVersion 66 | enableRBAC: true 67 | dnsPrefix: dnsPrefix 68 | agentPoolProfiles: [ 69 | { 70 | name: agentPoolName 71 | count: agentCount 72 | mode: 'System' 73 | vmSize: agentVMSize 74 | type: 'VirtualMachineScaleSets' 75 | osType: 'Linux' 76 | enableAutoScaling: false 77 | vnetSubnetID: subnetRef 78 | } 79 | ] 80 | servicePrincipalProfile: { 81 | clientId: 'msi' 82 | } 83 | nodeResourceGroup: nodeResourceGroup 84 | networkProfile: { 85 | networkPlugin: 'azure' 86 | loadBalancerSku: 'standard' 87 | } 88 | } 89 | } 90 | 91 | output id string = aks.id 92 | output apiServerAddress string = aks.properties.fqdn 93 | -------------------------------------------------------------------------------- /week7/main.bicep: -------------------------------------------------------------------------------- 1 | // params 2 | @description('The DNS prefix to use with hosted Kubernetes API server FQDN.') 3 | param dnsPrefix string = 'aks' 4 | 5 | @description('The name of the Managed Cluster resource.') 6 | param clusterName string = 'cluster' 7 | 8 | @description('Specifies the Azure location where the key vault should be created.') 9 | param location string = resourceGroup().location 10 | 11 | @minValue(1) 12 | @maxValue(50) 13 | @description('The number of nodes for the cluster. 1 Node is enough for Dev/Test and minimum 3 nodes, is recommended for Production') 14 | param agentCount int = 2 15 | 16 | @description('The size of the Virtual Machine.') 17 | param agentVMSize string = 'Standard_B2s' 18 | 19 | // vars 20 | var kubernetesVersion = '1.20.7' 21 | var subnetRef = '${vn.id}/subnets/${subnetName}' 22 | var addressPrefix = '20.0.0.0/16' 23 | var subnetName = 'Subnet01' 24 | var subnetPrefix = '20.0.0.0/24' 25 | var virtualNetworkName = 'AKSVNET02' 26 | var nodeResourceGroup = 'rg-${dnsPrefix}-${clusterName}' 27 | var tags = { 28 | environment: 'production' 29 | vmssValue: 'true' 30 | projectCode: '264082' 31 | } 32 | var agentPoolName = 'systempool' 33 | 34 | // Azure virtual network 35 | resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = { 36 | name: virtualNetworkName 37 | location: location 38 | tags: tags 39 | properties: { 40 | addressSpace: { 41 | addressPrefixes: [ 42 | addressPrefix 43 | ] 44 | } 45 | subnets: [ 46 | { 47 | name: subnetName 48 | properties: { 49 | addressPrefix: subnetPrefix 50 | } 51 | } 52 | ] 53 | } 54 | } 55 | 56 | // Azure kubernetes service 57 | resource aks 'Microsoft.ContainerService/managedClusters@2020-09-01' = { 58 | name: clusterName 59 | location: location 60 | tags: tags 61 | identity: { 62 | type: 'SystemAssigned' 63 | } 64 | properties: { 65 | kubernetesVersion: kubernetesVersion 66 | enableRBAC: true 67 | dnsPrefix: dnsPrefix 68 | agentPoolProfiles: [ 69 | { 70 | name: agentPoolName 71 | count: agentCount 72 | mode: 'System' 73 | vmSize: agentVMSize 74 | type: 'VirtualMachineScaleSets' 75 | osType: 'Linux' 76 | enableAutoScaling: false 77 | vnetSubnetID: subnetRef 78 | } 79 | ] 80 | servicePrincipalProfile: { 81 | clientId: 'msi' 82 | } 83 | nodeResourceGroup: nodeResourceGroup 84 | networkProfile: { 85 | networkPlugin: 'azure' 86 | loadBalancerSku: 'standard' 87 | } 88 | } 89 | } 90 | 91 | output id string = aks.id 92 | output apiServerAddress string = aks.properties.fqdn 93 | -------------------------------------------------------------------------------- /week4/main.tf: -------------------------------------------------------------------------------- 1 | provider "azurerm" { 2 | features {} 3 | } 4 | 5 | variable "client_id" {} 6 | 7 | variable "client_secret" {} 8 | 9 | variable "ssh_public_key" { 10 | default = "~/.ssh/id_rsa.pub" 11 | } 12 | 13 | variable resource_group_name { 14 | default = "aks-vnode-rg" 15 | } 16 | 17 | variable location { 18 | default = "South Central US" 19 | } 20 | 21 | resource "azurerm_resource_group" "k8s" { 22 | name = var.resource_group_name 23 | location = var.location 24 | } 25 | 26 | resource "azurerm_virtual_network" "example" { 27 | name = "aks-vnode-vnet" 28 | location = azurerm_resource_group.k8s.location 29 | resource_group_name = azurerm_resource_group.k8s.name 30 | address_space = ["2.0.0.0/24"] 31 | } 32 | 33 | resource "azurerm_subnet" "example-nodepool" { 34 | name = "default" 35 | virtual_network_name = azurerm_virtual_network.example.name 36 | resource_group_name = azurerm_resource_group.k8s.name 37 | address_prefixes = ["2.0.0.0/25"] 38 | depends_on = [azurerm_virtual_network.example] 39 | } 40 | 41 | resource "azurerm_subnet" "example-aci" { 42 | name = "aci" 43 | virtual_network_name = azurerm_virtual_network.example.name 44 | resource_group_name = azurerm_resource_group.k8s.name 45 | address_prefixes = ["2.0.0.248/29"] 46 | depends_on = [azurerm_virtual_network.example] 47 | 48 | delegation { 49 | name = "aciDelegation" 50 | service_delegation { 51 | name = "Microsoft.ContainerInstance/containerGroups" 52 | actions = ["Microsoft.Network/virtualNetworks/subnets/action"] 53 | } 54 | } 55 | } 56 | 57 | resource "azurerm_kubernetes_cluster" "example" { 58 | name = "virtualnodecluster" 59 | location = azurerm_resource_group.k8s.location 60 | resource_group_name = azurerm_resource_group.k8s.name 61 | dns_prefix = "k8s" 62 | kubernetes_version = "1.20.7" 63 | 64 | default_node_pool { 65 | name = "default" 66 | node_count = 1 67 | vm_size = "Standard_B2s" 68 | type = "AvailabilitySet" 69 | vnet_subnet_id = azurerm_subnet.example-nodepool.id 70 | } 71 | 72 | role_based_access_control { 73 | enabled = true 74 | } 75 | 76 | network_profile { 77 | network_plugin = "azure" 78 | network_policy = "azure" 79 | load_balancer_sku = "standard" 80 | } 81 | 82 | service_principal { 83 | client_id = var.client_id 84 | client_secret = var.client_secret 85 | } 86 | 87 | addon_profile { 88 | azure_policy { 89 | enabled = false 90 | } 91 | 92 | http_application_routing { 93 | enabled = false 94 | } 95 | 96 | oms_agent { 97 | enabled = false 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /01-GitOps-and-Observability/visualize-with-grafana.md: -------------------------------------------------------------------------------- 1 | ## Visualize Observability Data with Grafana 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | --- 12 | 13 | ## Links 14 | 15 | - [Prometheus data source - Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/prometheus/) 16 | - [Loki data source - Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/loki/) 17 | - [Tempo data source - Grafana Docs](https://grafana.com/docs/grafana/latest/datasources/tempo/) 18 | - [Grafana Explore for metrics, logs, and traces](https://grafana.com/docs/grafana/latest/explore/) 19 | - [Grafana Dashboards Library](https://grafana.com/grafana/dashboards/) 20 | 21 | 22 | --- 23 | 24 | ### Commands used in this lesson 25 | 26 | ```bash 27 | # create ssh tunnel 28 | ssh -L 3000:localhost:3000 -N -f root@172.233.98.105 29 | # Stop it with: pkill -f "ssh.*3000:localhost:3000" 30 | # pkill -f "ssh.*8080:localhost:3000" 31 | 32 | # port forward to access Grafana UI 33 | kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80 34 | # Access: http://localhost:3000 35 | 36 | # retreive Grafana login creds 37 | # The Helm chart creates a secret with the password 38 | kubectl get secret -n monitoring prometheus-grafana -o jsonpath='{.data.admin-password}' | base64 -d && echo 39 | # Expected output: prom-operator 40 | 41 | 42 | # Find Loki service 43 | kubectl get svc -n monitoring | grep loki 44 | 45 | # Get Loki URL 46 | kubectl get svc -n monitoring loki -o jsonpath='http://{.metadata.name}:{.spec.ports[?(@.name=="http-metrics")].port}' 47 | # Expected: http://loki:3100 (adjust based on your actual service name) 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | ### PromQL Queries 55 | 56 | ```promql 57 | # PANEL: Cluster Node Health 58 | # Explanation:# - up: Built-in metric (1 = target reachable, 0 = down)# - job="node-exporter": Filter to cluster nodes 59 | up{job="node-exporter"} 60 | 61 | # PANEL: Control Plane API Server Requests 62 | # apiserver_request_total: Counter of all API server requests# - rate([5m]): Calculate per-second rate over 5 minutes# - sum by (verb, code): Group by HTTP method and status code 63 | sum(rate(apiserver_request_total[5m])) by (verb, code) 64 | 65 | # Error rate (4xx and 5xx) 66 | sum(rate(apiserver_request_total{code=~"^(4|5).*"}[5m])) by (verb, code) 67 | 68 | # PANEL: Node CPU Usage 69 | # CPU usage percentage by node and mode 70 | 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) 71 | 72 | # Alternative: Break down by CPU mode 73 | avg by (instance, mode) (rate(node_cpu_seconds_total[5m])) * 100 74 | 75 | # PANEL: Pod Status Across Cluster 76 | # Count pods by phase 77 | count by (phase, namespace) (kube_pod_status_phase) 78 | 79 | 80 | ``` -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/podset_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | // PodSetSpec defines the desired state of PodSet 27 | type PodSetSpec struct { 28 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 29 | // Important: Run "make" to regenerate code after modifying this file 30 | // The following markers will use OpenAPI v3 schema to validate the value 31 | // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html 32 | 33 | // foo is an example field of PodSet. Edit podset_types.go to remove/update 34 | Replicas int32 `json:"replicas"` 35 | Image string `json:"image"` 36 | } 37 | 38 | // PodSetStatus defines the observed state of PodSet. 39 | type PodSetStatus struct { 40 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 41 | // Important: Run "make" to regenerate code after modifying this file 42 | 43 | // For Kubernetes API conventions, see: 44 | // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties 45 | 46 | // conditions represent the current state of the PodSet resource. 47 | // Each condition has a unique type and reflects the status of a specific aspect of the resource. 48 | // 49 | // Standard condition types include: 50 | // - "Available": the resource is fully functional 51 | // - "Progressing": the resource is being created or updated 52 | // - "Degraded": the resource failed to reach or maintain its desired state 53 | // 54 | Ready bool `json:"ready"` 55 | Message string `json:"message,omitempty"` 56 | } 57 | 58 | // +kubebuilder:object:root=true 59 | // +kubebuilder:subresource:status 60 | 61 | // PodSet is the Schema for the podsets API 62 | type PodSet struct { 63 | metav1.TypeMeta `json:",inline"` 64 | 65 | // metadata is a standard object metadata 66 | // +optional 67 | metav1.ObjectMeta `json:"metadata,omitzero"` 68 | 69 | // spec defines the desired state of PodSet 70 | // +required 71 | Spec PodSetSpec `json:"spec"` 72 | 73 | // status defines the observed state of PodSet 74 | // +optional 75 | Status PodSetStatus `json:"status,omitzero"` 76 | } 77 | 78 | // +kubebuilder:object:root=true 79 | 80 | // PodSetList contains a list of PodSet 81 | type PodSetList struct { 82 | metav1.TypeMeta `json:",inline"` 83 | metav1.ListMeta `json:"metadata,omitzero"` 84 | Items []PodSet `json:"items"` 85 | } 86 | 87 | func init() { 88 | SchemeBuilder.Register(&PodSet{}, &PodSetList{}) 89 | } -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/build-simple-opeartor.md: -------------------------------------------------------------------------------- 1 | ## Building a Simple Operator with Kubebuilder 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | --- 12 | 13 | ## Links 14 | 15 | - [What are Kubernetes Operators?](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 16 | - [Kubebuilder quick start](https://book.kubebuilder.io/quick-start.html) 17 | 18 | 19 | ## Files Modified in this Lessson 20 | 21 | - [podset_types.go](podset_types.go) 22 | - [podset_controller.go](podset_controller.go) 23 | - [Makefile](Makefile) 24 | - [Dockerfile](Dockerfile) 25 | - [registries.conf](registries.conf) 26 | - [my-podset.yaml](my-podset.yaml) 27 | 28 | ## Commands Run in this Lesson 29 | 30 | ```bash 31 | 32 | # Delete the old CRD 33 | kubectl delete crd podsets.example.com 34 | 35 | # Verify it's gone 36 | kubectl get crds | grep podset 37 | 38 | # Delete any existing PodSet instances (if any) 39 | kubectl delete podsets --all -A 40 | 41 | # Create project directory 42 | mkdir podset-operator && cd podset-operator 43 | 44 | # Initialize Kubebuilder project 45 | kubebuilder init --domain example.com --repo github.com/chadmcrowell/podset-operator 46 | 47 | # Create the API and controller 48 | kubebuilder create api --group apps --version v1 --kind PodSet 49 | # Answer 'y' to both prompts 50 | 51 | # Open api/v1/podset_types.go and add fields to the spec and status 52 | vim api/v1/podset_types.go 53 | 54 | # Generate the CRD manifests 55 | make manifests 56 | 57 | # Show the generated YAML in config/crd/bases/ 58 | ls config/crd/bases 59 | 60 | # Open internal/controller/podset_controller.go and implement the Reconcile function 61 | vim internal/controller/podset_controller.go 62 | 63 | # RUN LOCALLY (before building the operator container) 64 | 65 | # Install the CRD into your cluster 66 | make install 67 | 68 | # Verify CRD exists 69 | kubectl get crds | grep podsets 70 | 71 | # Run the operator locally (in your terminal) 72 | make run 73 | 74 | # create a test PodSet 75 | kubectl apply -f test-podset.yaml 76 | 77 | # Watch the operator logs in the first terminal 78 | 79 | # Check if ConfigMap was created 80 | kubectl get configmap test-podset-config -o yaml 81 | 82 | # BUILD THE OPERATOR CONTAINER 83 | 84 | # FIRST, stop the 'make run' command with ctrl + c 85 | 86 | # Install Podman 87 | sudo apt install -y podman 88 | 89 | # Alias it to docker 90 | alias docker=podman 91 | 92 | # Login to Docker Hub first 93 | docker login 94 | 95 | # Build and push Docker image (or use kind load for local testing) 96 | make docker-build docker-push IMG=chadmcrowell/podset-operator:v1 97 | 98 | # Deploy the operator to cluster 99 | make deploy IMG=chadmcrowell/podset-operator:v1 100 | 101 | # Verify operator pod is running 102 | kubectl get pods -n podset-operator-system 103 | 104 | # The operator is just a pod 105 | kubectl get pods -n podset-operator-system 106 | 107 | # With a Go binary watching the API 108 | kubectl logs -n podset-operator-system deployment/podset-operator-controller-manager 109 | 110 | # It has RBAC permissions to watch PodSets and create ConfigMaps 111 | kubectl get clusterrole | grep podset 112 | 113 | 114 | ``` 115 | 116 | ## Key Takeaways 117 | 118 | - Operators aren't magic—they're regular programs running in Pods 119 | - They use the Kubernetes client library to watch resources via the API server 120 | - The reconcile loop runs whenever resources change (informers/watches) 121 | - Kubebuilder generated all the boilerplate (RBAC, deployment manifests, Docker setup) -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/what-are-crds.md: -------------------------------------------------------------------------------- 1 | ## What Are Custom Resource Definitions (CRDs)? 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | --- 12 | 13 | ## Links 14 | 15 | - [Custom resources and API extension - Kubernetes Docs](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) 16 | - [Create a CustomResourceDefinition - Kubernetes Tasks](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) 17 | - [CRD validation with OpenAPI schemas](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation) 18 | - [CRD versioning and upgrades](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/) 19 | - [CustomResourceDefinition API reference (apiextensions.k8s.io/v1)](https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/) 20 | 21 | 22 | --- 23 | 24 | ### Commands used in this lesson 25 | 26 | ```bash 27 | # the certificate resource - created by cert-manager - is one of the many CRDs 28 | k -n ghost get certificate 29 | 30 | # view the structure of a Certificate resource in Kubernetes 31 | k -n ghost get certificate -o yaml 32 | 33 | # list all the crds in the cluster 34 | kubectl get crds 35 | 36 | # list the specification of the certificates resource 37 | kubectl describe crd certificates.cert-manager.io 38 | 39 | # create a simple CRD (YAML BELOW) 40 | kubectl apply -f crd.yaml 41 | 42 | # once created, view the new crd in the list of crds 43 | kubectl get crds | grep podset 44 | 45 | # create an instance of the PodSet resource (YAML BELOW) 46 | kubectl apply -f my-podset.yaml 47 | 48 | # get the podset resources 49 | kubectl get podsets 50 | 51 | # list the structure of the podset resource 52 | kubectl describe podset my-podset 53 | 54 | # delete the podset resource 55 | kubectl delete podset my-podset 56 | 57 | 58 | ``` 59 | 60 | > NOTE: A CRD alone is just data sitting in etcd without any behavior. An operator is needed to give that CRD functionality by watching for changes to custom resources and taking action to make things happen in your cluster. 61 | 62 | ### Create your first simple CRD 63 | Create a basic PodSet CRD that represents a group of identical pods: 64 | ```yaml 65 | # crd.yaml 66 | apiVersion: apiextensions.k8s.io/v1 67 | kind: CustomResourceDefinition 68 | metadata: 69 | name: podsets.example.com 70 | spec: 71 | group: example.com 72 | names: 73 | kind: PodSet 74 | plural: podsets 75 | singular: podset 76 | scope: Namespaced 77 | versions: 78 | - name: v1 79 | served: true 80 | storage: true 81 | schema: 82 | openAPIV3Schema: 83 | type: object 84 | properties: 85 | spec: 86 | type: object 87 | properties: 88 | replicas: 89 | type: integer 90 | minimum: 1 91 | maximum: 10 92 | 93 | ``` 94 | 95 | ### Create a PodSet 96 | Create an instance of the PodSet resource: 97 | ```yaml 98 | apiVersion: example.com/v1 99 | kind: PodSet 100 | metadata: 101 | name: my-podset 102 | spec: 103 | replicas: 3 104 | ``` 105 | 106 | ## Key Concepts Recap 107 | - CRDs extend Kubernetes without modifying core code 108 | - They're just YAML definitions that create new API endpoints 109 | - Custom resources work with kubectl like any native resource 110 | - CRDs define the "what" (schema), controllers define the "how" (behavior) 111 | - Real-world examples include Certificates, Ingresses (custom in some clusters), and application-specific resources -------------------------------------------------------------------------------- /00-Introduction-and-Installation/containderd-k8s-install.sh: -------------------------------------------------------------------------------- 1 | 2 | ############################################### 3 | ### ⚠️ INSTALLING CONTAINERD ⚠️ ############### 4 | ############################################### 5 | 6 | # update packages in apt package manager 7 | sudo apt update 8 | 9 | # install containerd using the apt package manager 10 | # containerd is lightwieght, reliable and fast (CRI native) 11 | sudo apt-get install -y containerd 12 | 13 | # create /etc/containerd directory for containerd configuration 14 | sudo mkdir -p /etc/containerd 15 | 16 | # Generate the default containerd configuration 17 | # Change the pause container to version 3.10 (pause container holds the linux ns for Kubernetes namespaces) 18 | # Set `SystemdCgroup` to true to use same cgroup drive as kubelet 19 | containerd config default \ 20 | | sed 's/SystemdCgroup = false/SystemdCgroup = true/' \ 21 | | sed 's|sandbox_image = ".*"|sandbox_image = "registry.k8s.io/pause:3.10"|' \ 22 | | sudo tee /etc/containerd/config.toml > /dev/null 23 | 24 | # Restart containerd to apply the configuration changes 25 | sudo systemctl restart containerd 26 | 27 | # Kubernetes doesn’t support swap unless explicitly configured under cgroup v2 28 | sudo swapoff -a 29 | 30 | ############################################### 31 | ### ⚠️ INSTALLING KUBERNETES ⚠️ ############### 32 | ############################################### 33 | 34 | # update packages 35 | sudo apt update 36 | 37 | # install apt-transport-https ca-certificates curl and gpg packages using 38 | # apt package manager in order to fetch Kubernetes packages from 39 | # external HTTPS repositories 40 | sudo apt-get install -y apt-transport-https ca-certificates curl gpg 41 | 42 | # create a secure directory for storing GPG keyring files 43 | # used by APT to verify trusted repositories. 44 | # This is part of a newer, more secure APT repository layout that 45 | # keeps trusted keys isolated from system-wide GPG configurations 46 | sudo mkdir -p -m 755 /etc/apt/keyrings 47 | 48 | # download the k8s release gpg key FOR 1.33 49 | sudo curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.34/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg 50 | 51 | 52 | # Download and convert the Kubernetes APT repository's GPG public key into 53 | # a binary format (`.gpg`) that APT can use to verify the integrity 54 | # and authenticity of Kubernetes packages during installation. 55 | # This overwrites any existing configuration in 56 | # /etc/apt/sources.list.d/kubernetes.list FOR 1.33 57 | # (`tee` without `-a` (append) will **replace** the contents of the file) 58 | echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.34/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list 59 | 60 | # update packages in apt 61 | sudo apt-get update 62 | 63 | apt-cache madison kubelet 64 | apt-cache madison kubectl 65 | apt-cache madison kubeadm 66 | 67 | 68 | KUBE_VERSION="1.34.1-1.1" 69 | 70 | # install kubelet, kubeadm, and kubectl at version 1.33.2-1.1 71 | sudo apt-get install -y kubelet=$KUBE_VERSION kubeadm=$KUBE_VERSION kubectl=$KUBE_VERSION 72 | 73 | # hold these packages at version 74 | sudo apt-mark hold kubelet kubeadm kubectl 75 | 76 | # enable IP packet forwarding on the node, which allows the Linux kernel 77 | # to route network traffic between interfaces. 78 | # This is essential in Kubernetes for pod-to-pod communication 79 | # across nodes and for routing traffic through the control plane 80 | # or CNI-managed networks 81 | sudo sysctl -w net.ipv4.ip_forward=1 82 | 83 | # uncomment the line in /etc/sysctl.conf enabling IP forwarding after reboot 84 | sudo sed -i '/^#net\.ipv4\.ip_forward=1/s/^#//' /etc/sysctl.conf 85 | 86 | # Apply the changes to sysctl.conf 87 | # Any changes made to sysctl configuration files take immediate effect without requiring a reboot 88 | sudo sysctl -p 89 | 90 | ############################################### 91 | ### ⚠️ MOVE ON TO INITIALIZING K8S ⚠️ ########## 92 | ############################################### 93 | 94 | # Now that you have containerd and kubernetes installed, you can move on to initializing kubernetes. 95 | # Go to the file initializing-k8s.sh -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/registries.conf: -------------------------------------------------------------------------------- 1 | # For more information on this configuration file, see containers-registries.conf(5). 2 | # 3 | # NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES 4 | # We recommend always using fully qualified image names including the registry 5 | # server (full dns name), namespace, image name, and tag 6 | # (e.g., registry.redhat.io/ubi8/ubi:latest). Pulling by digest (i.e., 7 | # quay.io/repository/name@digest) further eliminates the ambiguity of tags. 8 | # When using short names, there is always an inherent risk that the image being 9 | # pulled could be spoofed. For example, a user wants to pull an image named 10 | # `foobar` from a registry and expects it to come from myregistry.com. If 11 | # myregistry.com is not first in the search list, an attacker could place a 12 | # different `foobar` image at a registry earlier in the search list. The user 13 | # would accidentally pull and run the attacker's image and code rather than the 14 | # intended content. We recommend only adding registries which are completely 15 | # trusted (i.e., registries which don't allow unknown or anonymous users to 16 | # create accounts with arbitrary names). This will prevent an image from being 17 | # spoofed, squatted or otherwise made insecure. If it is necessary to use one 18 | # of these registries, it should be added at the end of the list. 19 | # 20 | # # An array of host[:port] registries to try when pulling an unqualified image, in order. 21 | # unqualified-search-registries = ["example.com"] 22 | # 23 | # [[registry]] 24 | # # The "prefix" field is used to choose the relevant [[registry]] TOML table; 25 | # # (only) the TOML table with the longest match for the input image name 26 | # # (taking into account namespace/repo/tag/digest separators) is used. 27 | # # 28 | # # The prefix can also be of the form: *.example.com for wildcard subdomain 29 | # # matching. 30 | # # 31 | # # If the prefix field is missing, it defaults to be the same as the "location" field. 32 | # prefix = "example.com/foo" 33 | # 34 | # # If true, unencrypted HTTP as well as TLS connections with untrusted 35 | # # certificates are allowed. 36 | # insecure = false 37 | # 38 | # # If true, pulling images with matching names is forbidden. 39 | # blocked = false 40 | # 41 | # # The physical location of the "prefix"-rooted namespace. 42 | # # 43 | # # By default, this is equal to "prefix" (in which case "prefix" can be omitted 44 | # # and the [[registry]] TOML table can only specify "location"). 45 | # # 46 | # # Example: Given 47 | # # prefix = "example.com/foo" 48 | # # location = "internal-registry-for-example.net/bar" 49 | # # requests for the image example.com/foo/myimage:latest will actually work with the 50 | # # internal-registry-for-example.net/bar/myimage:latest image. 51 | # 52 | # # The location can be empty iff prefix is in a 53 | # # wildcarded format: "*.example.com". In this case, the input reference will 54 | # # be used as-is without any rewrite. 55 | # location = internal-registry-for-example.com/bar" 56 | # 57 | # # (Possibly-partial) mirrors for the "prefix"-rooted namespace. 58 | # # 59 | # # The mirrors are attempted in the specified order; the first one that can be 60 | # # contacted and contains the image will be used (and if none of the mirrors contains the image, 61 | # # the primary location specified by the "registry.location" field, or using the unmodified 62 | # # user-specified reference, is tried last). 63 | # # 64 | # # Each TOML table in the "mirror" array can contain the following fields, with the same semantics 65 | # # as if specified in the [[registry]] TOML table directly: 66 | # # - location 67 | # # - insecure 68 | # [[registry.mirror]] 69 | # location = "example-mirror-0.local/mirror-for-foo" 70 | # [[registry.mirror]] 71 | # location = "example-mirror-1.local/mirrors/foo" 72 | # insecure = true 73 | # # Given the above, a pull of example.com/foo/image:latest will try: 74 | # # 1. example-mirror-0.local/mirror-for-foo/image:latest 75 | # # 2. example-mirror-1.local/mirrors/foo/image:latest 76 | # # 3. internal-registry-for-example.net/bar/image:latest 77 | # # in order, and use the first one that exists. 78 | [registries.search] 79 | registries = ['docker.io'] 80 | 81 | [registries.insecure] 82 | registries = [] -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/podset_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | "sigs.k8s.io/controller-runtime/pkg/log" 30 | 31 | appsv1 "github.com/chadmcrowell/podset-operator/api/v1" 32 | ) 33 | 34 | // PodSetReconciler reconciles a PodSet object 35 | type PodSetReconciler struct { 36 | client.Client 37 | Scheme *runtime.Scheme 38 | } 39 | 40 | // +kubebuilder:rbac:groups=apps.example.com,resources=podsets,verbs=get;list;watch;create;update;patch;delete 41 | // +kubebuilder:rbac:groups=apps.example.com,resources=podsets/status,verbs=get;update;patch 42 | // +kubebuilder:rbac:groups=apps.example.com,resources=podsets/finalizers,verbs=update 43 | // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete 44 | 45 | // Reconcile is part of the main kubernetes reconciliation loop which aims to 46 | // move the current state of the cluster closer to the desired state. 47 | // TODO(user): Modify the Reconcile function to compare the state specified by 48 | // the PodSet object against the actual cluster state, and then 49 | // perform operations to make the cluster state reflect the state specified by 50 | // the user. 51 | // 52 | // For more details, check Reconcile and its Result here: 53 | // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/reconcile 54 | func (r *PodSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | log := log.FromContext(ctx) 56 | 57 | // Fetch the PodSet resource 58 | var podSet appsv1.PodSet 59 | if err := r.Get(ctx, req.NamespacedName, &podSet); err != nil { 60 | return ctrl.Result{}, client.IgnoreNotFound(err) 61 | } 62 | 63 | // Log what we found 64 | log.Info("Reconciling PodSet", "name", podSet.Name, "replicas", podSet.Spec.Replicas) 65 | 66 | // Step 3: Create or update a ConfigMap 67 | configMap := &corev1.ConfigMap{ 68 | ObjectMeta: metav1.ObjectMeta{ 69 | Name: podSet.Name + "-config", 70 | Namespace: podSet.Namespace, 71 | }, 72 | Data: map[string]string{ 73 | "replicas": fmt.Sprintf("%d", podSet.Spec.Replicas), 74 | "image": podSet.Spec.Image, 75 | "status": "managed-by-operator", 76 | }, 77 | } 78 | 79 | // Set PodSet as owner of ConfigMap (for garbage collection) 80 | if err := ctrl.SetControllerReference(&podSet, configMap, r.Scheme); err != nil { 81 | log.Error(err, "Failed to set controller reference") 82 | return ctrl.Result{}, err 83 | } 84 | 85 | // Create or update ConfigMap 86 | err := r.Create(ctx, configMap) 87 | if err != nil { 88 | if errors.IsAlreadyExists(err) { 89 | log.Info("ConfigMap already exists, updating") 90 | if err := r.Update(ctx, configMap); err != nil { 91 | log.Error(err, "Failed to update ConfigMap") 92 | return ctrl.Result{}, err 93 | } 94 | } else { 95 | log.Error(err, "Failed to create ConfigMap") 96 | return ctrl.Result{}, err 97 | } 98 | } 99 | 100 | // Update PodSet status 101 | podSet.Status.Ready = true 102 | podSet.Status.Message = "ConfigMap created successfully" 103 | if err := r.Status().Update(ctx, &podSet); err != nil { 104 | log.Error(err, "Failed to update PodSet status") 105 | return ctrl.Result{}, err 106 | } 107 | 108 | log.Info("Successfully reconciled PodSet") 109 | return ctrl.Result{}, nil 110 | } 111 | 112 | // SetupWithManager sets up the controller with the Manager. 113 | func (r *PodSetReconciler) SetupWithManager(mgr ctrl.Manager) error { 114 | return ctrl.NewControllerManagedBy(mgr). 115 | For(&appsv1.PodSet{}). 116 | Named("podset"). 117 | Complete(r) 118 | } -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/operator-pattern.md: -------------------------------------------------------------------------------- 1 | ## The Operator Pattern: Controllers That Watch and Act 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | --- 12 | 13 | ## Links 14 | 15 | - [Kubernetes Operator pattern overview](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 16 | - [Controllers in Kubernetes](https://kubernetes.io/docs/concepts/architecture/controller/) 17 | - [Custom Resource Definitions (CRDs)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) 18 | 19 | 20 | --- 21 | 22 | ### Commands used in this lesson 23 | 24 | ```bash 25 | # operator for cert-manager 26 | k -n cert-manager get po 27 | # 'cert-manager-d6746cf45-fl4lg' is the main controller 28 | # 'cert-manager-cainjector' injects the CA bundles into CRDs 29 | # 'cert-manager-webhook' validates the certificte before stored in etcd 30 | 31 | # operator for servicemonitor 32 | k -n monitoring get po 33 | # 'prometheus-stack-operator-65c5cdd76f-4dglb' is the main controller 34 | # generates prometheus scrape configs 35 | # updates prometheus config when servicemonitors change 36 | 37 | 38 | # download binaries 39 | curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)" 40 | 41 | # move into /usr/local/bin 42 | chmod +x kubebuilder && mv kubebuilder /usr/local/bin/ 43 | 44 | # Create project directory 45 | mkdir myoperator && cd myoperator 46 | 47 | # Initialize the project (scaffolds entire structure) 48 | # --repo specifies your Go module path 49 | # This creates a local Go module - no GitHub repo needed yet 50 | kubebuilder init --domain example.com --repo github.com/chadmcrowell/myoperator 51 | 52 | # Create your first API/CRD 53 | # scaffolds a new CRD and controller for our operator project 54 | kubebuilder create api --group apps --version v1 --kind MyApp 55 | 56 | 57 | ``` 58 | 59 | ### Scaffolding that Kubebuilder creates 60 | 61 | ```bash 62 | . 63 | ├── api 64 | │   └── v1 65 | │   ├── groupversion_info.go 66 | │   ├── myapp_types.go 67 | │   └── zz_generated.deepcopy.go 68 | ├── bin 69 | │   ├── controller-gen -> /root/myoperator/bin/controller-gen-v0.19.0 70 | │   └── controller-gen-v0.19.0 71 | ├── cmd 72 | │   └── main.go 73 | ├── config 74 | │   ├── crd 75 | │   │   ├── kustomization.yaml 76 | │   │   └── kustomizeconfig.yaml 77 | │   ├── default 78 | │   │   ├── cert_metrics_manager_patch.yaml 79 | │   │   ├── kustomization.yaml 80 | │   │   ├── manager_metrics_patch.yaml 81 | │   │   └── metrics_service.yaml 82 | │   ├── manager 83 | │   │   ├── kustomization.yaml 84 | │   │   └── manager.yaml 85 | │   ├── network-policy 86 | │   │   ├── allow-metrics-traffic.yaml 87 | │   │   └── kustomization.yaml 88 | │   ├── prometheus 89 | │   │   ├── kustomization.yaml 90 | │   │   ├── monitor_tls_patch.yaml 91 | │   │   └── monitor.yaml 92 | │   ├── rbac 93 | │   │   ├── kustomization.yaml 94 | │   │   ├── leader_election_role_binding.yaml 95 | │   │   ├── leader_election_role.yaml 96 | │   │   ├── metrics_auth_role_binding.yaml 97 | │   │   ├── metrics_auth_role.yaml 98 | │   │   ├── metrics_reader_role.yaml 99 | │   │   ├── myapp_admin_role.yaml 100 | │   │   ├── myapp_editor_role.yaml 101 | │   │   ├── myapp_viewer_role.yaml 102 | │   │   ├── role_binding.yaml 103 | │   │   ├── role.yaml 104 | │   │   └── service_account.yaml 105 | │   └── samples 106 | │   ├── apps_v1_myapp.yaml 107 | │   └── kustomization.yaml 108 | ├── Dockerfile 109 | ├── go1.23.4.linux-amd64.tar.gz 110 | ├── go.mod 111 | ├── go.sum 112 | ├── hack 113 | │   └── boilerplate.go.txt 114 | ├── internal 115 | │   └── controller 116 | │   ├── myapp_controller.go 117 | │   ├── myapp_controller_test.go 118 | │   └── suite_test.go 119 | ├── Makefile 120 | ├── PROJECT 121 | ├── README.md 122 | └── test 123 | ├── e2e 124 | │   ├── e2e_suite_test.go 125 | │   └── e2e_test.go 126 | └── utils 127 | └── utils.go 128 | 129 | ``` 130 | 131 | ## Key Takeaways 132 | 133 | - Kubebuilder scaffolds CRDs, controllers, RBAC, and sample manifests so you can focus on reconciling business logic. 134 | - The Operator Framework adds lifecycle tooling (OLM) to package, install, and upgrade operators in clusters. 135 | - Both approaches standardize controller patterns: a reconcile loop watches resources, compares desired vs. actual state, and issues Kubernetes API updates. 136 | - Choose Kubebuilder for Go-first controller development; use Operator Framework when you also need cataloging, versioning, and cluster-wide operator distribution. 137 | -------------------------------------------------------------------------------- /00-Introduction-and-Installation/bootstrap-cluster-with-kubeadm-on-any-cloud.md: -------------------------------------------------------------------------------- 1 | ## Boostrap a Cluster with Kubeadm on Any Cloud 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | ### Commands used in this lesson 13 | 14 | ```bash 15 | ############################################### 16 | ### ⚠️ INSTALLING CONTAINERD ⚠️ ############### 17 | ############################################### 18 | 19 | # update packages in apt package manager 20 | sudo apt update 21 | 22 | # install containerd using the apt package manager 23 | # containerd is lightwieght, reliable and fast (CRI native) 24 | sudo apt-get install -y containerd 25 | 26 | # create /etc/containerd directory for containerd configuration 27 | sudo mkdir -p /etc/containerd 28 | 29 | # Generate the default containerd configuration 30 | # Change the pause container to version 3.10 (pause container holds the linux ns for Kubernetes namespaces) 31 | # Set `SystemdCgroup` to true to use same cgroup drive as kubelet 32 | containerd config default \ 33 | | sed 's/SystemdCgroup = false/SystemdCgroup = true/' \ 34 | | sed 's|sandbox_image = ".*"|sandbox_image = "registry.k8s.io/pause:3.10"|' \ 35 | | sudo tee /etc/containerd/config.toml > /dev/null 36 | 37 | # Restart containerd to apply the configuration changes 38 | sudo systemctl restart containerd 39 | 40 | # Kubernetes doesn’t support swap unless explicitly configured under cgroup v2 41 | sudo swapoff -a 42 | 43 | ############################################### 44 | ### ⚠️ INSTALLING KUBERNETES ⚠️ ############### 45 | ############################################### 46 | 47 | # update packages 48 | sudo apt update 49 | 50 | # install apt-transport-https ca-certificates curl and gpg packages using 51 | # apt package manager in order to fetch Kubernetes packages from 52 | # external HTTPS repositories 53 | sudo apt-get install -y apt-transport-https ca-certificates curl gpg 54 | 55 | # create a secure directory for storing GPG keyring files 56 | # used by APT to verify trusted repositories. 57 | # This is part of a newer, more secure APT repository layout that 58 | # keeps trusted keys isolated from system-wide GPG configurations 59 | sudo mkdir -p -m 755 /etc/apt/keyrings 60 | 61 | # download the k8s release gpg key FOR 1.33 62 | sudo curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg 63 | 64 | 65 | # Download and convert the Kubernetes APT repository's GPG public key into 66 | # a binary format (`.gpg`) that APT can use to verify the integrity 67 | # and authenticity of Kubernetes packages during installation. 68 | # This overwrites any existing configuration in 69 | # /etc/apt/sources.list.d/kubernetes.list FOR 1.33 70 | # (`tee` without `-a` (append) will **replace** the contents of the file) 71 | echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list 72 | 73 | # update packages in apt 74 | sudo apt-get update 75 | 76 | apt-cache madison kubelet 77 | apt-cache madison kubectl 78 | apt-cache madison kubeadm 79 | 80 | 81 | KUBE_VERSION="1.33.2-1.1" 82 | 83 | # install kubelet, kubeadm, and kubectl at version 1.33.2-1.1 84 | sudo apt-get install -y kubelet=$KUBE_VERSION kubeadm=$KUBE_VERSION kubectl=$KUBE_VERSION 85 | 86 | # hold these packages at version 87 | sudo apt-mark hold kubelet kubeadm kubectl 88 | 89 | # enable IP packet forwarding on the node, which allows the Linux kernel 90 | # to route network traffic between interfaces. 91 | # This is essential in Kubernetes for pod-to-pod communication 92 | # across nodes and for routing traffic through the control plane 93 | # or CNI-managed networks 94 | sudo sysctl -w net.ipv4.ip_forward=1 95 | 96 | # uncomment the line in /etc/sysctl.conf enabling IP forwarding after reboot 97 | sudo sed -i '/^#net\.ipv4\.ip_forward=1/s/^#//' /etc/sysctl.conf 98 | 99 | # Apply the changes to sysctl.conf 100 | # Any changes made to sysctl configuration files take immediate effect without requiring a reboot 101 | sudo sysctl -p 102 | 103 | ``` 104 | 105 | ### ONLY FROM THE CONTROL PLANE 106 | 107 | ```bash 108 | sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --cri-socket=unix:///run/containerd/containerd.sock 109 | 110 | # sleep 111 | sleep 300 112 | 113 | # apply calico manifest 114 | kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml 115 | ``` 116 | 117 | --- 118 | 119 | [Next Lesson](ghost-with-ingress-and-cert-manager.md) 120 | 121 | [Section 00 - Introduction and Installation](README.md) -------------------------------------------------------------------------------- /01-GitOps-and-Observability/observability-prometheus-and-loki.md: -------------------------------------------------------------------------------- 1 | ## Observability in Kubernetes with Prometheus and Loki 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | ### Resources 13 | - [Prometheus Stack Overview - GitHub](https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md) 14 | - [Prometheus Stack Values File](kps-values.yaml) 15 | - [Loki Values File](loki-values.yaml) 16 | 17 | ### Commands used in this lesson 18 | 19 | ```bash 20 | ################################################# 21 | ####### INSTALL PROMETHEUS STACK ################ 22 | ################################################# 23 | 24 | # https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md 25 | 26 | # values file 'kps-values.yaml' in this same directory 27 | helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack \ 28 | --version 76.3.0 \ 29 | --namespace monitoring \ 30 | -f kps-values.yaml 31 | 32 | # create an SSH tunnel from your local machine to the control plane server 33 | ssh -L 9090:localhost:9090 root@ 34 | 35 | # proxy communication to the prometheus service and open a browser tab to http://localhost:9090 36 | kubectl port-forward -n monitoring svc/prometheus-stack-prometheus 9090:9090 37 | 38 | ########################################### 39 | ####### Common PromQL Queries ############# 40 | ########################################### 41 | 42 | # Node CPU usage % 43 | 100 * sum by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m])) 44 | / sum by (instance) (rate(node_cpu_seconds_total[5m])) 45 | 46 | # Node CPU Usage 47 | sum(rate(node_cpu_seconds_total{mode!="idle"}[5m])) by (instance) 48 | 49 | # memory usage % 50 | 100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) 51 | 52 | # Node Memory usage 53 | node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes 54 | 55 | # node disk usage 56 | node_filesystem_size_bytes - node_filesystem_free_bytes 57 | 58 | # Node up/down status 59 | up{job="node-exporter"} 60 | 61 | # Pod CPU usage 62 | sum(rate(container_cpu_usage_seconds_total{container!="",pod!=""}[5m])) by (namespace, pod) 63 | 64 | # Pod memory usage 65 | sum(container_memory_usage_bytes{container!="",pod!=""}) by (namespace, pod) 66 | 67 | # Pod restarts 68 | sum(kube_pod_container_status_restarts_total) by (namespace, pod) 69 | 70 | # Pod restarts by namespace 71 | topk(5, sum by (namespace) (increase(kube_pod_container_status_restarts_total[1h]))) 72 | 73 | # running and failed pods by namespace 74 | count(kube_pod_status_phase{phase="Running"}) by (namespace) 75 | count(kube_pod_status_phase{phase="Failed"}) by (namespace) 76 | 77 | # deployment health 78 | kube_deployment_status_replicas_available 79 | kube_deployment_status_replicas_unavailable 80 | 81 | # daemonset status 82 | kube_daemonset_status_current_number_scheduled 83 | kube_daemonset_status_desired_number_scheduled 84 | 85 | # job success/failure 86 | kube_job_status_succeeded 87 | kube_job_status_failed 88 | 89 | # NGINX Ingress Error Rate 90 | sum(rate(nginx_ingress_controller_requests{status=~"5.."}[5m])) by (ingress) 91 | 92 | # CPU allocated vs. used 93 | sum(kube_pod_container_resource_limits_cpu_cores) by (namespace) 94 | sum(rate(container_cpu_usage_seconds_total{container!="",pod!=""}[5m])) by (namespace) 95 | 96 | # MEM allocated vs. used 97 | sum(kube_pod_container_resource_limits_memory_bytes) by (namespace) 98 | sum(container_memory_usage_bytes{container!="",pod!=""}) by (namespace) 99 | 100 | # prometheus target health 101 | up{job="prometheus"} 102 | 103 | # AlertManager Notifications sent 104 | alertmanager_notification_latency_seconds_average 105 | 106 | # K8s API server request rate 107 | sum(rate(apiserver_request_total[5m])) by (verb, resource) 108 | 109 | # k8s scheduler latency 110 | histogram_quantile(0.99, sum(rate(scheduler_schedule_latency_seconds_bucket[5m])) by (le)) 111 | 112 | # ghost app HTTP request rate 113 | sum(rate(http_requests_total{app="ghost"}[5m])) by (instance) 114 | 115 | # ghost app HTTP errors 116 | sum(rate(http_requests_total{app="ghost",status=~"5.."}[5m])) by (instance) 117 | 118 | # all exported metrics 119 | count({__name__=~".+"}) 120 | 121 | # All active alerts 122 | ALERTS{alertstate="firing"} 123 | 124 | 125 | ########################################### 126 | ####### INSTALL LOKI ###################### 127 | ########################################### 128 | 129 | # values file 'loki-values.yaml' in this same directory (change values for your S3 bucket) 130 | 131 | # create secret with S3 bucket creds 132 | kubectl create secret generic loki-object-storage \ 133 | --namespace monitoring \ 134 | --from-literal=AWS_ACCESS_KEY_ID=$ACCESS_KEY \ 135 | --from-literal=AWS_SECRET_ACCESS_KEY=$SECRET_KEY 136 | 137 | 138 | # add charts repo 139 | helm repo add grafana https://grafana.github.io/helm-charts 140 | helm repo update 141 | 142 | # Install Loki 143 | helm upgrade --install loki grafana/loki \ 144 | --namespace monitoring \ 145 | --values loki-values.yaml 146 | 147 | # view all monitoring pods 148 | k -n monitoring get po 149 | 150 | ``` 151 | 152 | 153 | ### Additional Resources 154 | 155 | - [Prometheus Stack Values File - Use with Helm Install](kps-values.yaml) 156 | - [Loki Values File - Use with Helm Install](loki-values.yaml) 157 | - [Prometheus Stack Overview - GitHub](https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md) 158 | - [Installing Loki - Grafana Docs](https://grafana.com/docs/loki/latest/setup/install/helm/install-monolithic/) 159 | - [Create S3 Bucket with Access Keys in Linode](https://techdocs.akamai.com/cloud-computing/docs/create-and-manage-buckets) 160 | 161 | 162 | 163 | 164 | --- 165 | 166 | [Next Lesson](apps-with-gitops-and-argocd.md) 167 | 168 | [Section 01 - GitOps and Observability](README.md) -------------------------------------------------------------------------------- /01-GitOps-and-Observability/metrics-logs-traces-with-opentelemetry.md: -------------------------------------------------------------------------------- 1 | ## Apps Emitting Metrics, Logs, and Traces with OpenTelemetry 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | 13 | ## Links 14 | 15 | - [Docker container used for the Ghost deployment in this lesson](https://hub.docker.com/repository/docker/chadmcrowell/ghost-otel) 16 | 17 | 18 | - [OpenTelemetry Operator](https://opentelemetry.io/docs/platforms/kubernetes/operator/) 19 | - [OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) 20 | 21 | - [Service Monitors in Prometheus](https://grafana.com/docs/alloy/latest/reference/components/prometheus/prometheus.operator.servicemonitors/) 22 | 23 | ### Dockerfile 24 | 25 | > NOTE: We follow a similar pattern as in a previous lesson here: 26 | [Making your own image to run in Kubernetes](../00-Introduction-and-Installation/Making-your-own-image-to-run-in-kubernetes.md) 27 | 28 | 29 | - [Dockerfile for a new Ghost image](Dockerfile) 30 | 31 | ```bash 32 | # replace with your Docker Hub username. 33 | # For a private registry on Linode or elsewhere, use the full registry URL 34 | docker build --platform=linux/amd64 -t /ghost-otel:latest . 35 | 36 | # push your image to the container registry 37 | docker push /ghost-otel:latest 38 | 39 | ``` 40 | 41 | 42 | ### Course structure by the end of this lesson 43 | 44 | ``` 45 | my-gitops-repo/ 46 | ├── infrastructure/ 47 | │ ├── cert-manager/ 48 | │ ├── ingress-nginx/ 49 | │ └── monitoring/ 50 | │ ├── loki/ 51 | │ │ └── values.yaml 52 | │ ├── prometheus/ 53 | │ │ └── values.yaml 54 | │ ├── opentelemetry-collector/ # NEW 55 | │ │ ├── otel-values.yaml # NEW 56 | │ │ └── servicemon-otel.yaml # NEW 57 | │ └── tempo/ # NEW 58 | │ └── tempo-values.yaml # NEW 59 | │ 60 | ├── applications/ 61 | │ └── ghost/ 62 | │ └── deployment.yaml # REPLACE 63 | │ 64 | ├── argocd-apps/ 65 | │ ├── infrastructure/ 66 | │ │ ├── cert-manager-app.yaml 67 | │ │ ├── ingress-nginx-app.yaml 68 | │ │ ├── loki-app.yaml 69 | │ │ ├── prometheus-app.yaml 70 | │ │ ├── otel-collector-app.yaml # NEW 71 | │ │ └── tempo-app.yaml # NEW 72 | │ ├── applications/ 73 | │ │ └── ghost-app.yaml 74 | │ └── app-of-apps.yaml 75 | │ 76 | └── README.md 77 | ``` 78 | 79 | ### Files to add to the directories above 80 | 81 | - [ghost-deploy-with-otel.yaml](ghost-deploy-with-otel.yaml) 82 | - [otel-values.yaml](otel-values.yaml) 83 | - [tempo-values.yaml](tempo-values.yaml) 84 | - [tempo-app.yaml](tempo-app.yaml) 85 | - [servicemon-otel.yaml](servicemon-otel.yaml) 86 | 87 | ### Commands used in this lesson 88 | 89 | ```bash 90 | 91 | # commit and push to your gitops repo 92 | git add .; git commit -m "Add OpenTelemetry Collector with LGTM"; git push origin main 93 | 94 | # OTHER HELPFUL COMMANDS 95 | 96 | # Check if the Collector pod is running 97 | kubectl get pods -n monitoring -l app.kubernetes.io/name=opentelemetry-collector 98 | 99 | # Check the Collector service 100 | kubectl get svc -n monitoring opentelemetry-collector 101 | 102 | # View Collector logs 103 | kubectl logs -n monitoring -l app.kubernetes.io/name=opentelemetry-collector -f 104 | 105 | # Verify instrumentation is loaded 106 | kubectl exec -it -- env | grep OTEL 107 | 108 | # Test OTLP endpoint connectivity 109 | kubectl exec -it -- wget -O- http://otel-collector.default.svc.cluster.local:4318 110 | 111 | ``` 112 | 113 | 114 | ### RESOURCES 115 | 116 | [https://invisibl.io/blog/kubernetes-observability-loki-cortex-tempo-prometheus-grafana/](https://invisibl.io/blog/kubernetes-observability-loki-cortex-tempo-prometheus-grafana/) 117 | 118 | [https://last9.io/blog/opentelemetry-backends/](https://last9.io/blog/opentelemetry-backends/) 119 | 120 | [https://github.com/magsther/awesome-opentelemetry](https://github.com/magsther/awesome-opentelemetry) 121 | 122 | [https://grafana.com/oss/tempo/](https://grafana.com/oss/tempo/) 123 | 124 | [https://signoz.io/blog/opentelemetry-backend/](https://signoz.io/blog/opentelemetry-backend/) 125 | 126 | [https://grafana.com/blog/2023/07/28/simplify-managing-grafana-tempo-instances-in-kubernetes-with-the-tempo-operator/](https://grafana.com/blog/2023/07/28/simplify-managing-grafana-tempo-instances-in-kubernetes-with-the-tempo-operator/) 127 | 128 | [https://odigos.io/blog/open-source-stack](https://odigos.io/blog/open-source-stack) 129 | 130 | [https://www.civo.com/learn/distributed-tracing-kubernetes-grafana-tempo-opentelemetry](https://www.civo.com/learn/distributed-tracing-kubernetes-grafana-tempo-opentelemetry) 131 | 132 | [https://n2x.io/docs/integrations-guides/observability/distributed-tracing/](https://n2x.io/docs/integrations-guides/observability/distributed-tracing/) 133 | 134 | [https://last9.io/blog/loki-vs-prometheus/](https://last9.io/blog/loki-vs-prometheus/) 135 | 136 | [https://grafana.com/docs/loki/latest/configure/storage/](https://grafana.com/docs/loki/latest/configure/storage/) 137 | 138 | [https://grafana.com/docs/loki/latest/get-started/overview/](https://grafana.com/docs/loki/latest/get-started/overview/) 139 | 140 | [https://www.reddit.com/r/PrometheusMonitoring/comments/qpzmgy/how_is_prometheus_involved_in_loki/](https://www.reddit.com/r/PrometheusMonitoring/comments/qpzmgy/how_is_prometheus_involved_in_loki/) 141 | 142 | [https://www.reddit.com/r/devops/comments/154m1d2/what_observability_stack_would_you_recommend_for/](https://www.reddit.com/r/devops/comments/154m1d2/what_observability_stack_would_you_recommend_for/) 143 | 144 | [https://uptrace.dev/blog/opentelemetry-backend](https://uptrace.dev/blog/opentelemetry-backend) 145 | 146 | [https://signoz.io/comparisons/opentelemetry-vs-loki/](https://signoz.io/comparisons/opentelemetry-vs-loki/) 147 | 148 | [https://overcast.blog/kubernetes-distributed-storage-backend-a-guide-0a0a437414b0](https://overcast.blog/kubernetes-distributed-storage-backend-a-guide-0a0a437414b0) 149 | 150 | [https://opentelemetry.io/docs/concepts/components/](https://opentelemetry.io/docs/concepts/components/) 151 | 152 | [https://www.reddit.com/r/grafana/comments/18ihd6h/how_big_is_the_lift_to_set_upmaintain_lgtm_oss/](https://www.reddit.com/r/grafana/comments/18ihd6h/how_big_is_the_lift_to_set_upmaintain_lgtm_oss/) 153 | 154 | [https://www.reddit.com/r/OpenTelemetry/comments/1b18tbn/one_backend_for_all/](https://www.reddit.com/r/OpenTelemetry/comments/1b18tbn/one_backend_for_all/) -------------------------------------------------------------------------------- /00-Introduction-and-Installation/ghost-with-ingress-and-cert-manager.md: -------------------------------------------------------------------------------- 1 | # Installing Ghost with Ingress & Cert-manager 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | ## What We Are Installing 6 | 7 | - [Ghost](https://github.com/TryGhost/Ghost) 8 | - [Nginx Ingress Controller](https://github.com/kubernetes/ingress-nginx) 9 | - [Cert-manager](https://github.com/cert-manager/cert-manager) 10 | 11 | ![Application stack on Kubernetes](ghost-blog-on-k8s-figure1.png) 12 | 13 | ![Request flow on Kubernetes](ghost-blog-on-k8s-figure2.png) 14 | 15 | 16 | --- 17 | 18 | ### GitHub Repository 19 | 20 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 21 | 22 | 23 | ### Commands used in this Lesson 24 | 25 | ```bash 26 | # linode CSI drivers 27 | helm repo add linode-csi https://linode.github.io/linode-blockstorage-csi-driver/ 28 | helm repo update linode-csi 29 | 30 | # deploy CSI driver 31 | export LINODE_API_TOKEN="" 32 | export REGION="" 33 | helm install linode-csi-driver \ 34 | --set apiToken="${LINODE_API_TOKEN}" \ 35 | --set region="${REGION}" \ 36 | linode-csi/linode-blockstorage-csi-driver 37 | 38 | # install nginx ingress controller manifest in the 'ingress-nginx' namespace 39 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.13.3/deploy/static/provider/baremetal/deploy.yaml 40 | 41 | # Install the cert-manager Helm chart 42 | helm install \ 43 | cert-manager oci://quay.io/jetstack/charts/cert-manager \ 44 | --version v1.19.1 \ 45 | --namespace cert-manager \ 46 | --create-namespace \ 47 | --set crds.enabled=true \ 48 | --no-hooks 49 | 50 | 51 | # create ghost namespace 52 | kubectl create ns ghost 53 | 54 | # create PVC 55 | cat < ⚙️ Built by **KubeSkills** to help learners, engineers, and SREs understand Kubernetes from first principles. 286 | 287 | 288 | -------------------------------------------------------------------------------- /week8/manifests/configmap-prometheus.yml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | data: 4 | schema-version: 5 | #string.used by agent to parse config. supported versions are {v1}. Configs with other schema versions will be rejected by the agent. 6 | v1 7 | config-version: 8 | #string.used by customer to keep track of this config file's version in their source control/repository (max allowed 10 chars, other chars will be truncated) 9 | ver1 10 | log-data-collection-settings: |- 11 | # Log data collection settings 12 | # Any errors related to config map settings can be found in the KubeMonAgentEvents table in the Log Analytics workspace that the cluster is sending data to. 13 | 14 | [log_collection_settings] 15 | [log_collection_settings.stdout] 16 | # In the absense of this configmap, default value for enabled is true 17 | enabled = true 18 | # exclude_namespaces setting holds good only if enabled is set to true 19 | # kube-system log collection is disabled by default in the absence of 'log_collection_settings.stdout' setting. If you want to enable kube-system, remove it from the following setting. 20 | # If you want to continue to disable kube-system log collection keep this namespace in the following setting and add any other namespace you want to disable log collection to the array. 21 | # In the absense of this configmap, default value for exclude_namespaces = ["kube-system"] 22 | exclude_namespaces = ["kube-system"] 23 | 24 | [log_collection_settings.stderr] 25 | # Default value for enabled is true 26 | enabled = true 27 | # exclude_namespaces setting holds good only if enabled is set to true 28 | # kube-system log collection is disabled by default in the absence of 'log_collection_settings.stderr' setting. If you want to enable kube-system, remove it from the following setting. 29 | # If you want to continue to disable kube-system log collection keep this namespace in the following setting and add any other namespace you want to disable log collection to the array. 30 | # In the absense of this cofigmap, default value for exclude_namespaces = ["kube-system"] 31 | exclude_namespaces = ["kube-system"] 32 | 33 | [log_collection_settings.env_var] 34 | # In the absense of this configmap, default value for enabled is true 35 | enabled = true 36 | [log_collection_settings.enrich_container_logs] 37 | # In the absense of this configmap, default value for enrich_container_logs is false 38 | enabled = false 39 | # When this is enabled (enabled = true), every container log entry (both stdout & stderr) will be enriched with container Name & container Image 40 | [log_collection_settings.collect_all_kube_events] 41 | # In the absense of this configmap, default value for collect_all_kube_events is false 42 | # When the setting is set to false, only the kube events with !normal event type will be collected 43 | enabled = false 44 | # When this is enabled (enabled = true), all kube events including normal events will be collected 45 | 46 | prometheus-data-collection-settings: |- 47 | # Custom Prometheus metrics data collection settings 48 | [prometheus_data_collection_settings.cluster] 49 | # Cluster level scrape endpoint(s). These metrics will be scraped from agent's Replicaset (singleton) 50 | # Any errors related to prometheus scraping can be found in the KubeMonAgentEvents table in the Log Analytics workspace that the cluster is sending data to. 51 | 52 | #Interval specifying how often to scrape for metrics. This is duration of time and can be specified for supporting settings by combining an integer value and time unit as a string value. Valid time units are ns, us (or µs), ms, s, m, h. 53 | interval = "1m" 54 | 55 | ## Uncomment the following settings with valid string arrays for prometheus scraping 56 | #fieldpass = ["metric_to_pass1", "metric_to_pass12"] 57 | 58 | #fielddrop = ["metric_to_drop"] 59 | 60 | # An array of urls to scrape metrics from. 61 | # urls = ["http://myurl:9101/metrics"] 62 | 63 | # An array of Kubernetes services to scrape metrics from. 64 | # kubernetes_services = ["http://my-service-dns.my-namespace:9102/metrics"] 65 | 66 | # When monitor_kubernetes_pods = true, replicaset will scrape Kubernetes pods for the following prometheus annotations: 67 | - prometheus.io/scrape: "true" 68 | - prometheus.io/scheme: "http" 69 | - prometheus.io/path: "/" 70 | - prometheus.io/port: "8000" 71 | monitor_kubernetes_pods = true 72 | 73 | ## Restricts Kubernetes monitoring to namespaces for pods that have annotations set and are scraped using the monitor_kubernetes_pods setting. 74 | ## This will take effect when monitor_kubernetes_pods is set to true 75 | ## ex: monitor_kubernetes_pods_namespaces = ["default1", "default2", "default3"] 76 | # monitor_kubernetes_pods_namespaces = ["default1"] 77 | 78 | ## Label selector to target pods which have the specified label 79 | ## This will take effect when monitor_kubernetes_pods is set to true 80 | ## Reference the docs at https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors 81 | # kubernetes_label_selector = "env=dev,app=nginx" 82 | 83 | ## Field selector to target pods which have the specified field 84 | ## This will take effect when monitor_kubernetes_pods is set to true 85 | ## Reference the docs at https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/ 86 | ## eg. To scrape pods on a specific node 87 | # kubernetes_field_selector = "spec.nodeName=$HOSTNAME" 88 | 89 | [prometheus_data_collection_settings.node] 90 | # Node level scrape endpoint(s). These metrics will be scraped from agent's DaemonSet running in every node in the cluster 91 | # Any errors related to prometheus scraping can be found in the KubeMonAgentEvents table in the Log Analytics workspace that the cluster is sending data to. 92 | 93 | #Interval specifying how often to scrape for metrics. This is duration of time and can be specified for supporting settings by combining an integer value and time unit as a string value. Valid time units are ns, us (or µs), ms, s, m, h. 94 | interval = "1m" 95 | 96 | ## Uncomment the following settings with valid string arrays for prometheus scraping 97 | 98 | # An array of urls to scrape metrics from. $NODE_IP (all upper case) will substitute of running Node's IP address 99 | # urls = ["http://$NODE_IP:9103/metrics"] 100 | 101 | #fieldpass = ["metric_to_pass1", "metric_to_pass12"] 102 | 103 | #fielddrop = ["metric_to_drop"] 104 | 105 | metric_collection_settings: |- 106 | # Metrics collection settings for metrics sent to Log Analytics and MDM 107 | [metric_collection_settings.collect_kube_system_pv_metrics] 108 | # In the absense of this configmap, default value for collect_kube_system_pv_metrics is false 109 | # When the setting is set to false, only the persistent volume metrics outside the kube-system namespace will be collected 110 | enabled = false 111 | # When this is enabled (enabled = true), persistent volume metrics including those in the kube-system namespace will be collected 112 | 113 | alertable-metrics-configuration-settings: |- 114 | # Alertable metrics configuration settings for container resource utilization 115 | [alertable_metrics_configuration_settings.container_resource_utilization_thresholds] 116 | # The threshold(Type Float) will be rounded off to 2 decimal points 117 | # Threshold for container cpu, metric will be sent only when cpu utilization exceeds or becomes equal to the following percentage 118 | container_cpu_threshold_percentage = 95.0 119 | # Threshold for container memoryRss, metric will be sent only when memory rss exceeds or becomes equal to the following percentage 120 | container_memory_rss_threshold_percentage = 95.0 121 | # Threshold for container memoryWorkingSet, metric will be sent only when memory working set exceeds or becomes equal to the following percentage 122 | container_memory_working_set_threshold_percentage = 95.0 123 | 124 | # Alertable metrics configuration settings for persistent volume utilization 125 | [alertable_metrics_configuration_settings.pv_utilization_thresholds] 126 | # Threshold for persistent volume usage bytes, metric will be sent only when persistent volume utilization exceeds or becomes equal to the following percentage 127 | pv_usage_threshold_percentage = 60.0 128 | 129 | # Alertable metrics configuration settings for completed jobs count 130 | [alertable_metrics_configuration_settings.job_completion_threshold] 131 | # Threshold for completed job count , metric will be sent only for those jobs which were completed earlier than the following threshold 132 | job_completion_threshold_time_minutes = 360 133 | integrations: |- 134 | [integrations.azure_network_policy_manager] 135 | collect_basic_metrics = false 136 | collect_advanced_metrics = false 137 | metadata: 138 | name: container-azm-ms-agentconfig 139 | namespace: kube-system -------------------------------------------------------------------------------- /01-GitOps-and-Observability/apps-with-gitops-and-argocd.md: -------------------------------------------------------------------------------- 1 | ## Deploying Applications with GitOps and ArgoCD 2 | 3 | [Access this lesson](https://community.kubeskills.com/c/kubernetes-from-scratch) 4 | 5 | --- 6 | 7 | ### GitHub Repository (including scripts) 8 | 9 | [https://github.com/chadmcrowell/k8s-from-scratch](https://github.com/chadmcrowell/k8s-from-scratch) 10 | 11 | 12 | ### Links 13 | - [https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/](https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/) 14 | - [Server Side Apply - Kubernetes docs](https://kubernetes.io/docs/reference/using-api/server-side-apply/) 15 | - [App of Apps Pattern - ArgoCD Docs](https://argo-cd.readthedocs.io/en/latest/operator-manual/cluster-bootstrapping/) 16 | - [Sync Phases and Waves - ArgoCD Docs](https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/) 17 | 18 | ### GitOps Repo Structure 19 | 20 | ```bash 21 | my-gitops-repo/ 22 | ├── infrastructure/ 23 | │ ├── cert-manager/ 24 | │ │ └── clusterissuer.yaml 25 | │ ├── ingress-nginx/ 26 | │ │ └── ingress-nginx.yaml 27 | │ └── monitoring/ 28 | │ ├── loki/ 29 | │ │ └── values.yaml 30 | │ └── prometheus/ 31 | │ └── values.yaml 32 | │ 33 | ├── applications/ 34 | │ └── ghost/ 35 | │ ├── namespace.yaml 36 | │ ├── deployment.yaml 37 | │ ├── service.yaml 38 | │ ├── ingress.yaml 39 | │ └── pvc.yaml 40 | │ 41 | ├── argocd-apps/ 42 | │ ├── infrastructure/ 43 | │ │ ├── cert-manager-app.yaml 44 | │ │ ├── ingress-nginx-app.yaml 45 | │ │ ├── loki-app.yaml 46 | │ │ └── prometheus-app.yaml 47 | │ ├── applications/ 48 | │ │ └── ghost-app.yaml 49 | │ └── app-of-apps.yaml # ← Parent application 50 | │ 51 | └── README.md 52 | ``` 53 | 54 | 55 | ### Commands used in this lesson 56 | 57 | ```bash 58 | 59 | # create namespace 60 | kubectl create ns argocd 61 | 62 | # install argocd 63 | kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml 64 | 65 | # wait for pods 66 | kubectl -n argocd get pods 67 | 68 | # get admin password for argo UI 69 | kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 --decode ; echo 70 | 71 | # create an SSH tunnel from your local machine to the control plane server 72 | # ssh -L 8080:localhost:8080 root@ 73 | ssh -L 8080:localhost:8080 root@172.233.98.105 74 | 75 | # port forward to access UI 76 | kubectl port-forward svc/argocd-server -n argocd 8080:443 77 | 78 | # open browser to https://localhost:8080 79 | 80 | # username is admin 81 | # password is from the secret above 82 | 83 | # clone down the course repo 84 | git clone https://github.com/chadmcrowell/k8s-from-scratch.git 85 | 86 | # set this to your own gitops repo 87 | MY_GITOPS_REPO="https://github.com//my-gitops-repo.git" 88 | 89 | git clone $MY_GITOPS_REPO 90 | 91 | # script to create directory strcuture for ArgoCD 92 | cp k8s-from-scratch/01-GitOps-and-Observability/setup-repo.sh my-git-repo/ 93 | ./setup-repo.sh 94 | 95 | # copy all the files from the course repo into your gitops repo 96 | cp ./01-GitOps-and-Observability/clusterissuer.yaml ../my-gitops-repo/infrastructure/cert-manager/ 97 | cp ./01-GitOps-and-Observability/ingress-nginx.yaml ../my-gitops-repo/infrastructure/ingress-nginx/ 98 | cp ./01-GitOps-and-Observability/kps-values.yaml ../my-gitops-repo/infrastructure/monitoring/prometheus/ 99 | cp ./01-GitOps-and-Observability/loki-values.yaml ../my-gitops-repo/infrastructure/monitoring/loki/ 100 | cp ./01-GitOps-and-Observability/ingress.yaml ../my-gitops-repo/applications/ghost/ 101 | cp ./01-GitOps-and-Observability/namespace.yaml ../my-gitops-repo/applications/ghost/ 102 | cp ./01-GitOps-and-Observability/deployment.yaml ../my-gitops-repo/applications/ghost/ 103 | cp ./01-GitOps-and-Observability/service.yaml ../my-gitops-repo/applications/ghost/ 104 | cp ./01-GitOps-and-Observability/pvc.yaml ../my-gitops-repo/applications/ghost/ 105 | 106 | 107 | ``` 108 | 109 | - [app-of-apps.yaml](app-of-apps.yaml) 110 | - [cert-manager-app.yaml](cert-manager-app.yaml) 111 | - [ingress-nginx-app.yaml](ingress-nginx-app.yaml) 112 | - [prometheus-app.yaml](prometheus-app.yaml) 113 | - [loki-app.yaml](loki-app.yaml) 114 | - [ghost-app.yaml](ghost-app.yaml) 115 | 116 | ```bash 117 | # commit and push 118 | git add . 119 | git commit -m "Prepare repo for ArgoCD" 120 | git push origin main 121 | 122 | kubectl apply -f argocd-apps/app-of-apps.yaml 123 | 124 | 125 | # List all applications managed by app-of-apps 126 | argocd app list 127 | 128 | ``` 129 | 130 | 131 | 132 | 133 | ### Resources 134 | 135 | - [https://argo-cd.readthedocs.io/en/stable/understand_the_basics/](https://argo-cd.readthedocs.io/en/stable/understand_the_basics/) 136 | - [https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/](https://argo-cd.readthedocs.io/en/stable/user-guide/best_practices/) 137 | - [https://argo-cd.readthedocs.io](https://argo-cd.readthedocs.io/) 138 | - [https://argo-cd.readthedocs.io/en/stable/getting_started/](https://argo-cd.readthedocs.io/en/stable/getting_started/) 139 | - [https://www.youtube.com/watch?v=8AJlVQy6Cx0](https://www.youtube.com/watch?v=8AJlVQy6Cx0) 140 | - [https://codefresh.io/learn/argo-cd/](https://codefresh.io/learn/argo-cd/) 141 | - [https://akuity.io/blog/gitops-best-practices-whitepaper](https://akuity.io/blog/gitops-best-practices-whitepaper) 142 | - [https://codefresh.io/blog/argo-cd-best-practices/](https://codefresh.io/blog/argo-cd-best-practices/) 143 | - [https://configu.com/blog/gitops-with-argocd-a-practical-guide/](https://configu.com/blog/gitops-with-argocd-a-practical-guide/) 144 | - [https://www.microtica.com/blog/the-ultimate-list-of-gitops-resources](https://www.microtica.com/blog/the-ultimate-list-of-gitops-resources) 145 | - [https://scalr.com/learning-center/top-10-gitops-tools-for-2025-a-comprehensive-guide/](https://scalr.com/learning-center/top-10-gitops-tools-for-2025-a-comprehensive-guide/) 146 | - [https://www.cncf.io/blog/2025/06/09/gitops-in-2025-from-old-school-updates-to-the-modern-way/](https://www.cncf.io/blog/2025/06/09/gitops-in-2025-from-old-school-updates-to-the-modern-way/) 147 | - [https://github.com/kubernetes-sigs/kustomize/issues/4633](https://github.com/kubernetes-sigs/kustomize/issues/4633) 148 | - [https://github.com/kubernetes/kubernetes/issues/66450](https://github.com/kubernetes/kubernetes/issues/66450) 149 | - [https://github.com/kubernetes-sigs/cluster-api/issues/7913](https://github.com/kubernetes-sigs/cluster-api/issues/7913) 150 | - [https://github.com/kubernetes/kubectl/issues/1766](https://github.com/kubernetes/kubectl/issues/1766) 151 | - [https://github.com/kubernetes-sigs/karpenter/issues/1177](https://github.com/kubernetes-sigs/karpenter/issues/1177) 152 | - [https://kubernetes.io/docs/concepts/extend-kubernetes/operator/](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 153 | - [https://github.com/kubernetes-sigs/external-dns/issues/4895](https://github.com/kubernetes-sigs/external-dns/issues/4895) 154 | - [https://kubernetes.io/docs/concepts/security/service-accounts/](https://kubernetes.io/docs/concepts/security/service-accounts/) 155 | - [https://github.com/kubernetes-sigs/external-dns/issues/2386](https://github.com/kubernetes-sigs/external-dns/issues/2386) 156 | - [https://kubernetes.io/docs/concepts/cluster-administration/](https://kubernetes.io/docs/concepts/cluster-administration/) 157 | - [https://github.com/kubernetes-sigs/external-dns/issues/3755](https://github.com/kubernetes-sigs/external-dns/issues/3755) 158 | - [https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/](https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/) 159 | - [https://github.com/kubernetes-sigs/kustomize/discussions/5046](https://github.com/kubernetes-sigs/kustomize/discussions/5046) 160 | - [https://github.com/kubernetes-sigs/cluster-api/discussions/5501](https://github.com/kubernetes-sigs/cluster-api/discussions/5501) 161 | - [https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/) 162 | - [https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) 163 | - [https://kubernetes.io/docs/reference/using-api/server-side-apply/](https://kubernetes.io/docs/reference/using-api/server-side-apply/) 164 | - [https://kubernetes.io/docs/concepts/architecture/controller/](https://kubernetes.io/docs/concepts/architecture/controller/) 165 | - [https://github.com/kubernetes-sigs/kubebuilder/discussions/3074](https://github.com/kubernetes-sigs/kubebuilder/discussions/3074) 166 | - [https://github.com/kubernetes-sigs/kustomize/issues/388](https://github.com/kubernetes-sigs/kustomize/issues/388) 167 | - [https://www.reddit.com/r/devops/comments/1dzrep6/mastering_gitops_argocd_vs_fluxcd_complete_guide/](https://www.reddit.com/r/devops/comments/1dzrep6/mastering_gitops_argocd_vs_fluxcd_complete_guide/) 168 | - [https://spacelift.io/blog/gitops-tools](https://spacelift.io/blog/gitops-tools) 169 | - [https://www.youtube.com/watch?v=eqiqQN1CCmM](https://www.youtube.com/watch?v=eqiqQN1CCmM) 170 | - [https://www.reddit.com/r/devops/comments/1hvpejm/open_source_devops_learning_app_with_15_projects/](https://www.reddit.com/r/devops/comments/1hvpejm/open_source_devops_learning_app_with_15_projects/) 171 | - [https://github.com/argoproj/argo-cd/discussions/5667](https://github.com/argoproj/argo-cd/discussions/5667) 172 | - [https://www.trek10.com/blog/exploring-gitops-with-argo-cd](https://www.trek10.com/blog/exploring-gitops-with-argo-cd) 173 | - [https://argoproj.github.io/cd/](https://argoproj.github.io/cd/) 174 | - [https://argo-cd.readthedocs.io/en/latest/user-guide/tracking_strategies/](https://argo-cd.readthedocs.io/en/latest/user-guide/tracking_strategies/) 175 | 176 | 177 | 178 | --- 179 | 180 | [Next Lesson](metrics-logs-traces-with-opentelemetry.md) 181 | 182 | [Section 01 - GitOps and Observability](README.md) -------------------------------------------------------------------------------- /02-Extending-K8s-with-Operators-and-Custom-Resources/Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | IMG ?= controller:latest 3 | 4 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 5 | ifeq (,$(shell go env GOBIN)) 6 | GOBIN=$(shell go env GOPATH)/bin 7 | else 8 | GOBIN=$(shell go env GOBIN) 9 | endif 10 | 11 | # CONTAINER_TOOL defines the container tool to be used for building images. 12 | # Be aware that the target commands are only tested with Docker which is 13 | # scaffolded by default. However, you might want to replace it to use other 14 | # tools. (i.e. podman) 15 | CONTAINER_TOOL ?= podman 16 | 17 | # Setting SHELL to bash allows bash commands to be executed by recipes. 18 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 19 | SHELL = /usr/bin/env bash -o pipefail 20 | .SHELLFLAGS = -ec 21 | 22 | .PHONY: all 23 | all: build 24 | 25 | ##@ General 26 | 27 | # The help target prints out all targets with their descriptions organized 28 | # beneath their categories. The categories are represented by '##@' and the 29 | # target descriptions by '##'. The awk command is responsible for reading the 30 | # entire set of makefiles included in this invocation, looking for lines of the 31 | # file as xyz: ## something, and then pretty-format the target and help. Then, 32 | # if there's a line with ##@ something, that gets pretty-printed as a category. 33 | # More info on the usage of ANSI control characters for terminal formatting: 34 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 35 | # More info on the awk command: 36 | # http://linuxcommand.org/lc3_adv_awk.php 37 | 38 | .PHONY: help 39 | help: ## Display this help. 40 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 41 | 42 | ##@ Development 43 | 44 | .PHONY: manifests 45 | manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. 46 | "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases 47 | 48 | .PHONY: generate 49 | generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. 50 | "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." 51 | 52 | .PHONY: fmt 53 | fmt: ## Run go fmt against code. 54 | go fmt ./... 55 | 56 | .PHONY: vet 57 | vet: ## Run go vet against code. 58 | go vet ./... 59 | 60 | .PHONY: test 61 | test: manifests generate fmt vet setup-envtest ## Run tests. 62 | KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out 63 | 64 | # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. 65 | # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. 66 | # CertManager is installed by default; skip with: 67 | # - CERT_MANAGER_INSTALL_SKIP=true 68 | KIND_CLUSTER ?= podset-operator-test-e2e 69 | 70 | .PHONY: setup-test-e2e 71 | setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist 72 | @command -v $(KIND) >/dev/null 2>&1 || { \ 73 | echo "Kind is not installed. Please install Kind manually."; \ 74 | exit 1; \ 75 | } 76 | @case "$$($(KIND) get clusters)" in \ 77 | *"$(KIND_CLUSTER)"*) \ 78 | echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ 79 | *) \ 80 | echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ 81 | $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ 82 | esac 83 | 84 | .PHONY: test-e2e 85 | test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. 86 | KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v 87 | $(MAKE) cleanup-test-e2e 88 | 89 | .PHONY: cleanup-test-e2e 90 | cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests 91 | @$(KIND) delete cluster --name $(KIND_CLUSTER) 92 | 93 | .PHONY: lint 94 | lint: golangci-lint ## Run golangci-lint linter 95 | "$(GOLANGCI_LINT)" run 96 | 97 | .PHONY: lint-fix 98 | lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes 99 | "$(GOLANGCI_LINT)" run --fix 100 | 101 | .PHONY: lint-config 102 | lint-config: golangci-lint ## Verify golangci-lint linter configuration 103 | "$(GOLANGCI_LINT)" config verify 104 | 105 | ##@ Build 106 | 107 | .PHONY: build 108 | build: manifests generate fmt vet ## Build manager binary. 109 | go build -o bin/manager cmd/main.go 110 | 111 | .PHONY: run 112 | run: manifests generate fmt vet ## Run a controller from your host. 113 | go run ./cmd/main.go 114 | 115 | # If you wish to build the manager image targeting other platforms you can use the --platform flag. 116 | # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. 117 | # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 118 | .PHONY: docker-build 119 | docker-build: ## Build docker image with the manager. 120 | $(CONTAINER_TOOL) build -t ${IMG} --build-arg TARGETOS=linux --build-arg TARGETARCH=amd64 . 121 | 122 | .PHONY: docker-push 123 | docker-push: ## Push docker image with the manager. 124 | $(CONTAINER_TOOL) push ${IMG} 125 | 126 | # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple 127 | # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: 128 | # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ 129 | # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ 130 | # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) 131 | # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. 132 | PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le 133 | .PHONY: docker-buildx 134 | docker-buildx: ## Build and push docker image for the manager for cross-platform support 135 | # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile 136 | sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross 137 | - $(CONTAINER_TOOL) buildx create --name podset-operator-builder 138 | $(CONTAINER_TOOL) buildx use podset-operator-builder 139 | - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . 140 | - $(CONTAINER_TOOL) buildx rm podset-operator-builder 141 | rm Dockerfile.cross 142 | 143 | .PHONY: build-installer 144 | build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. 145 | mkdir -p dist 146 | cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} 147 | "$(KUSTOMIZE)" build config/default > dist/install.yaml 148 | 149 | ##@ Deployment 150 | 151 | ifndef ignore-not-found 152 | ignore-not-found = false 153 | endif 154 | 155 | .PHONY: install 156 | install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. 157 | @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ 158 | if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi 159 | 160 | .PHONY: uninstall 161 | uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 162 | @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ 163 | if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi 164 | 165 | .PHONY: deploy 166 | deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. 167 | cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} 168 | "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f - 169 | 170 | .PHONY: undeploy 171 | undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 172 | "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - 173 | 174 | ##@ Dependencies 175 | 176 | ## Location to install dependencies to 177 | LOCALBIN ?= $(shell pwd)/bin 178 | $(LOCALBIN): 179 | mkdir -p "$(LOCALBIN)" 180 | 181 | ## Tool Binaries 182 | KUBECTL ?= kubectl 183 | KIND ?= kind 184 | KUSTOMIZE ?= $(LOCALBIN)/kustomize 185 | CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen 186 | ENVTEST ?= $(LOCALBIN)/setup-envtest 187 | GOLANGCI_LINT = $(LOCALBIN)/golangci-lint 188 | 189 | ## Tool Versions 190 | KUSTOMIZE_VERSION ?= v5.7.1 191 | CONTROLLER_TOOLS_VERSION ?= v0.19.0 192 | 193 | #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) 194 | ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ 195 | [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ 196 | printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') 197 | 198 | #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) 199 | ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ 200 | [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ 201 | printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') 202 | 203 | GOLANGCI_LINT_VERSION ?= v2.5.0 204 | .PHONY: kustomize 205 | kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. 206 | $(KUSTOMIZE): $(LOCALBIN) 207 | $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) 208 | 209 | .PHONY: controller-gen 210 | controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. 211 | $(CONTROLLER_GEN): $(LOCALBIN) 212 | $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) 213 | 214 | .PHONY: setup-envtest 215 | setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. 216 | @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." 217 | @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ 218 | echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ 219 | exit 1; \ 220 | } 221 | 222 | .PHONY: envtest 223 | envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. 224 | $(ENVTEST): $(LOCALBIN) 225 | $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) 226 | 227 | .PHONY: golangci-lint 228 | golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. 229 | $(GOLANGCI_LINT): $(LOCALBIN) 230 | $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) 231 | 232 | # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist 233 | # $1 - target path with name of binary 234 | # $2 - package url which can be installed 235 | # $3 - specific version of package 236 | define go-install-tool 237 | @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ 238 | set -e; \ 239 | package=$(2)@$(3) ;\ 240 | echo "Downloading $${package}" ;\ 241 | rm -f "$(1)" ;\ 242 | GOBIN="$(LOCALBIN)" go install $${package} ;\ 243 | mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ 244 | } ;\ 245 | ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" 246 | endef 247 | 248 | define gomodver 249 | $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) 250 | endef -------------------------------------------------------------------------------- /combined_output.yaml: -------------------------------------------------------------------------------- 1 | # --- File: helm-values-examples.yaml 2 | # Example Helm values for common CNCF components (portable, on-prem friendly) 3 | # Use with: helm install -n -f thisfile.yaml --create-namespace 4 | 5 | # --- Kyverno --- 6 | kyverno: 7 | replicaCount: 2 8 | image: 9 | pullPolicy: IfNotPresent 10 | resources: 11 | requests: { cpu: 100m, memory: 128Mi } 12 | limits: { cpu: 200m, memory: 256Mi } 13 | admissionController: 14 | podSecurity: 15 | enabled: true # enable PSA baseline/restricted translation 16 | reportsController: 17 | enabled: true 18 | 19 | # --- Falco (Helm chart: falcosecurity/falco) --- 20 | falco: 21 | driver: 22 | kind: modern_ebpf 23 | falco: 24 | rulesFiles: 25 | - /etc/falco/rules.d 26 | resources: 27 | requests: { cpu: 100m, memory: 256Mi } 28 | limits: { cpu: 300m, memory: 512Mi } 29 | extra: 30 | env: 31 | FALCO_BPF_PROBE: "" 32 | 33 | # --- Prometheus (kube-prometheus-stack minimal) --- 34 | kube-prometheus-stack: 35 | grafana: 36 | enabled: true 37 | adminPassword: "admin" 38 | prometheus: 39 | prometheusSpec: 40 | retention: 24h 41 | retentionSize: 5GiB 42 | resources: 43 | requests: { cpu: 200m, memory: 512Mi } 44 | limits: { cpu: 500m, memory: 1Gi } 45 | alertmanager: 46 | enabled: true 47 | 48 | # --- Ingress NGINX --- 49 | ingress-nginx: 50 | controller: 51 | replicaCount: 2 52 | resources: 53 | requests: { cpu: 100m, memory: 128Mi } 54 | limits: { cpu: 300m, memory: 256Mi } 55 | admissionWebhooks: 56 | enabled: true 57 | 58 | # NOTES: 59 | # - Keep charts pinned with --version to ensure reproducibility. 60 | # - Tune resources for your nodes; defaults are intentionally conservative. 61 | 62 | # --- File: kustomization-examples.yaml 63 | # Three portable overlays (dev/stage/prod) demonstrating namePrefix, labels, and patches. 64 | # Directory suggestion: 65 | # base/ (deployment+service) 66 | # overlays/dev|stage|prod/ 67 | 68 | apiVersion: kustomize.config.k8s.io/v1beta1 69 | kind: Kustomization 70 | resources: 71 | - ./base 72 | namePrefix: dev- 73 | commonLabels: 74 | app.kubernetes.io/part-of: kfs 75 | app.kubernetes.io/environment: dev 76 | patchesStrategicMerge: 77 | - ./overlays/dev/deploy-patch.yaml 78 | --- 79 | apiVersion: kustomize.config.k8s.io/v1beta1 80 | kind: Kustomization 81 | resources: 82 | - ./base 83 | namePrefix: stage- 84 | commonLabels: 85 | app.kubernetes.io/part-of: kfs 86 | app.kubernetes.io/environment: stage 87 | patchesStrategicMerge: 88 | - ./overlays/stage/deploy-patch.yaml 89 | --- 90 | apiVersion: kustomize.config.k8s.io/v1beta1 91 | kind: Kustomization 92 | resources: 93 | - ./base 94 | namePrefix: prod- 95 | commonLabels: 96 | app.kubernetes.io/part-of: kfs 97 | app.kubernetes.io/environment: prod 98 | patchesStrategicMerge: 99 | - ./overlays/prod/deploy-patch.yaml 100 | 101 | # --- File: kustomization-patches.yaml 102 | # Example base Deployment and three overlay patches 103 | 104 | # base/deployment.yaml 105 | apiVersion: apps/v1 106 | kind: Deployment 107 | metadata: 108 | name: my-app 109 | labels: { app: my-app } 110 | spec: 111 | replicas: 1 112 | selector: 113 | matchLabels: { app: my-app } 114 | template: 115 | metadata: 116 | labels: { app: my-app } 117 | spec: 118 | containers: 119 | - name: my-app 120 | image: ghcr.io/example/my-app:1.0.0 121 | ports: [{ containerPort: 8080 }] 122 | readinessProbe: 123 | httpGet: { path: /healthz, port: 8080 } 124 | initialDelaySeconds: 5 125 | resources: 126 | requests: { cpu: 50m, memory: 64Mi } 127 | limits: { cpu: 200m, memory: 128Mi } 128 | --- 129 | # overlays/dev/deploy-patch.yaml 130 | apiVersion: apps/v1 131 | kind: Deployment 132 | metadata: 133 | name: my-app 134 | spec: 135 | replicas: 1 136 | template: 137 | spec: 138 | containers: 139 | - name: my-app 140 | image: ghcr.io/example/my-app:1.0.0-dev 141 | env: 142 | - name: LOG_LEVEL 143 | value: "debug" 144 | --- 145 | # overlays/stage/deploy-patch.yaml 146 | apiVersion: apps/v1 147 | kind: Deployment 148 | metadata: 149 | name: my-app 150 | spec: 151 | replicas: 2 152 | template: 153 | spec: 154 | containers: 155 | - name: my-app 156 | image: ghcr.io/example/my-app:1.0.0-rc 157 | resources: 158 | requests: { cpu: 100m, memory: 128Mi } 159 | limits: { cpu: 300m, memory: 256Mi } 160 | --- 161 | # overlays/prod/deploy-patch.yaml 162 | apiVersion: apps/v1 163 | kind: Deployment 164 | metadata: 165 | name: my-app 166 | spec: 167 | replicas: 3 168 | template: 169 | spec: 170 | topologySpreadConstraints: 171 | - maxSkew: 1 172 | topologyKey: kubernetes.io/hostname 173 | whenUnsatisfiable: ScheduleAnyway 174 | labelSelector: 175 | matchLabels: { app: my-app } 176 | containers: 177 | - name: my-app 178 | image: ghcr.io/example/my-app:1.0.0 179 | resources: 180 | requests: { cpu: 150m, memory: 192Mi } 181 | limits: { cpu: 500m, memory: 384Mi } 182 | 183 | # --- File: custom-resource-definitions.yaml 184 | # Minimal self-contained CRDs useful for operator exercises and demos. 185 | # These CRDs are examples (group "ops.kubeskills.io") and safe to apply to any cluster. 186 | 187 | apiVersion: apiextensions.k8s.io/v1 188 | kind: CustomResourceDefinition 189 | metadata: 190 | name: apps.ops.kubeskills.io 191 | spec: 192 | group: ops.kubeskills.io 193 | names: 194 | kind: App 195 | plural: apps 196 | singular: app 197 | shortNames: ["kfsapp"] 198 | scope: Namespaced 199 | versions: 200 | - name: v1alpha1 201 | served: true 202 | storage: true 203 | schema: 204 | openAPIV3Schema: 205 | type: object 206 | properties: 207 | spec: 208 | type: object 209 | required: ["image"] 210 | properties: 211 | image: 212 | type: string 213 | pattern: "^[\w./:-]+$" 214 | replicas: 215 | type: integer 216 | minimum: 0 217 | env: 218 | type: array 219 | items: 220 | type: object 221 | required: ["name","value"] 222 | properties: 223 | name: { type: string } 224 | value: { type: string } 225 | status: 226 | type: object 227 | properties: 228 | readyReplicas: { type: integer } 229 | --- 230 | apiVersion: apiextensions.k8s.io/v1 231 | kind: CustomResourceDefinition 232 | metadata: 233 | name: policies.ops.kubeskills.io 234 | spec: 235 | group: ops.kubeskills.io 236 | names: 237 | kind: Policy 238 | plural: policies 239 | singular: policy 240 | shortNames: ["kfspol"] 241 | scope: Cluster 242 | versions: 243 | - name: v1alpha1 244 | served: true 245 | storage: true 246 | schema: 247 | openAPIV3Schema: 248 | type: object 249 | properties: 250 | spec: 251 | type: object 252 | properties: 253 | type: 254 | type: string 255 | enum: ["deny-egress","image-allowlist","pss-restricted"] 256 | params: 257 | type: object 258 | additionalProperties: true 259 | 260 | # --- File: kyverno-falco-policies.yaml 261 | # Secure-by-default guardrails with Kyverno + Falco 262 | 263 | # Kyverno: Disallow hostPath 264 | apiVersion: kyverno.io/v1 265 | kind: ClusterPolicy 266 | metadata: 267 | name: disallow-hostpath 268 | spec: 269 | validationFailureAction: enforce 270 | rules: 271 | - name: no-hostpath 272 | match: 273 | resources: 274 | kinds: ["Pod"] 275 | validate: 276 | message: "HostPath volumes are not allowed." 277 | pattern: 278 | spec: 279 | volumes: 280 | - name: "*" 281 | =(hostPath): "null" 282 | --- 283 | # Kyverno: Require runAsNonRoot & readOnlyRootFilesystem 284 | apiVersion: kyverno.io/v1 285 | kind: ClusterPolicy 286 | metadata: 287 | name: require-secure-pod-options 288 | spec: 289 | validationFailureAction: enforce 290 | rules: 291 | - name: require-nonroot 292 | match: 293 | resources: 294 | kinds: ["Pod"] 295 | validate: 296 | message: "Containers must run as non-root with readOnlyRootFilesystem." 297 | pattern: 298 | spec: 299 | securityContext: 300 | runAsNonRoot: true 301 | containers: 302 | - name: "*" 303 | securityContext: 304 | runAsNonRoot: true 305 | readOnlyRootFilesystem: true 306 | --- 307 | # Falco rule: Detect terminal shells in containers 308 | apiVersion: falco.org/v1alpha1 309 | kind: FalcoRule 310 | metadata: 311 | name: detect-shell-in-container 312 | spec: 313 | rules: 314 | - rule: Terminal shell in container 315 | desc: Detect shells running in a container 316 | condition: > 317 | spawned_process and container and proc.name in (bash, sh, zsh, ash) 318 | output: > 319 | Shell spawned in container (user=%user.name process=%proc.name container_id=%container.id image=%container.image.repository) 320 | priority: Notice 321 | tags: [process, container, mitre_t1059] 322 | 323 | # --- File: networkpolicy-rbac-examples.yaml 324 | # Namespace default deny + limited read RBAC 325 | 326 | # Deny all ingress/egress by default in ns: apps 327 | apiVersion: v1 328 | kind: Namespace 329 | metadata: 330 | name: apps 331 | labels: 332 | name: apps 333 | --- 334 | apiVersion: networking.k8s.io/v1 335 | kind: NetworkPolicy 336 | metadata: 337 | name: default-deny-all 338 | namespace: apps 339 | spec: 340 | podSelector: {} 341 | policyTypes: ["Ingress","Egress"] 342 | --- 343 | # Allow egress DNS + HTTP/HTTPS for update checks 344 | apiVersion: networking.k8s.io/v1 345 | kind: NetworkPolicy 346 | metadata: 347 | name: allow-egress-core 348 | namespace: apps 349 | spec: 350 | podSelector: {} 351 | policyTypes: ["Egress"] 352 | egress: 353 | - to: 354 | - namespaceSelector: {} 355 | ports: 356 | - protocol: UDP 357 | port: 53 358 | - protocol: TCP 359 | port: 53 360 | - protocol: TCP 361 | port: 80 362 | - protocol: TCP 363 | port: 443 364 | --- 365 | # RBAC: namespace-scoped read-only 366 | apiVersion: rbac.authorization.k8s.io/v1 367 | kind: Role 368 | metadata: 369 | name: read-only 370 | namespace: apps 371 | rules: 372 | - apiGroups: [""] 373 | resources: ["pods","services","endpoints","configmaps"] 374 | verbs: ["get","list","watch"] 375 | --- 376 | apiVersion: rbac.authorization.k8s.io/v1 377 | kind: RoleBinding 378 | metadata: 379 | name: read-only-binding 380 | namespace: apps 381 | subjects: 382 | - kind: User 383 | name: developer@example.com 384 | apiGroup: rbac.authorization.k8s.io 385 | roleRef: 386 | kind: Role 387 | name: read-only 388 | apiGroup: rbac.authorization.k8s.io 389 | 390 | # --- File: networkpolicy-rbac-variations.yaml 391 | # Variations: allow namespace-isolated HTTP and a cluster-wide viewer role 392 | 393 | # Allow only traffic from same namespace to port 8080 394 | apiVersion: networking.k8s.io/v1 395 | kind: NetworkPolicy 396 | metadata: 397 | name: allow-same-namespace-http 398 | namespace: apps 399 | spec: 400 | podSelector: {} 401 | policyTypes: ["Ingress"] 402 | ingress: 403 | - from: 404 | - podSelector: {} # same-namespace pods 405 | ports: 406 | - protocol: TCP 407 | port: 8080 408 | --- 409 | # Allow prometheus scraping from monitoring namespace 410 | apiVersion: networking.k8s.io/v1 411 | kind: NetworkPolicy 412 | metadata: 413 | name: allow-prometheus-scrape 414 | namespace: apps 415 | spec: 416 | podSelector: 417 | matchLabels: 418 | app.kubernetes.io/name: my-app 419 | policyTypes: ["Ingress"] 420 | ingress: 421 | - from: 422 | - namespaceSelector: 423 | matchLabels: 424 | name: monitoring 425 | ports: 426 | - protocol: TCP 427 | port: 9090 428 | --- 429 | # Cluster-wide read-only (viewer) without write verbs 430 | apiVersion: rbac.authorization.k8s.io/v1 431 | kind: ClusterRole 432 | metadata: 433 | name: cluster-viewer-lite 434 | rules: 435 | - apiGroups: ["*"] 436 | resources: ["*"] 437 | verbs: ["get","list","watch"] 438 | --- 439 | apiVersion: rbac.authorization.k8s.io/v1 440 | kind: ClusterRoleBinding 441 | metadata: 442 | name: cluster-viewer-lite-binding 443 | subjects: 444 | - kind: User 445 | name: developer@example.com 446 | apiGroup: rbac.authorization.k8s.io 447 | roleRef: 448 | kind: ClusterRole 449 | name: cluster-viewer-lite 450 | apiGroup: rbac.authorization.k8s.io 451 | 452 | --------------------------------------------------------------------------------