├── 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 | 
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 | 
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 | 
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 | 
12 |
13 | 
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 |
--------------------------------------------------------------------------------