diff --git a/app_go/.dockerignore b/app_go/.dockerignore
index 6f1931b7cd..64ce80193c 100644
--- a/app_go/.dockerignore
+++ b/app_go/.dockerignore
@@ -2,3 +2,4 @@
!go.mod
!go.sum
!*.go
+main_test.go
diff --git a/app_go/Dockerfile b/app_go/Dockerfile
index 09e812af80..e8e9d0dedd 100644
--- a/app_go/Dockerfile
+++ b/app_go/Dockerfile
@@ -1,8 +1,8 @@
-FROM golang:1.25-alpine AS build
+FROM golang:1.26-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
-COPY *.go ./
+COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o devops-info-service.out
FROM scratch
diff --git a/app_go/go.mod b/app_go/go.mod
index 364c72ba68..1e5f872dad 100644
--- a/app_go/go.mod
+++ b/app_go/go.mod
@@ -1,6 +1,6 @@
module example.com/devops-info-service
-go 1.25.0
+go 1.26.1
require github.com/prometheus/client_golang v1.23.2
diff --git a/app_go/main.go b/app_go/main.go
index 898f940d7b..7e482b4e80 100644
--- a/app_go/main.go
+++ b/app_go/main.go
@@ -20,7 +20,7 @@ import (
const (
serviceName = "devops-info-service"
- serviceVersion = "1.8.0"
+ serviceVersion = "1.10.0"
serviceDescription = "DevOps course info service"
serviceFramework = "Go net/http"
serviceLoggerName = "devops_info_service"
diff --git a/app_python/pyproject.toml b/app_python/pyproject.toml
index deb962795e..ca312b1657 100644
--- a/app_python/pyproject.toml
+++ b/app_python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "devops-info-service"
-version = "1.8.0"
+version = "1.10.0"
description = ""
authors = [
{name = "LocalT0aster",email = "90502400+LocalT0aster@users.noreply.github.com"}
diff --git a/app_python/src/router.py b/app_python/src/router.py
index bd9315d8b5..35b32270f6 100644
--- a/app_python/src/router.py
+++ b/app_python/src/router.py
@@ -25,7 +25,7 @@
record_endpoint_call,
)
-__version__ = "1.8.0"
+__version__ = "1.10.0"
def get_service_info() -> dict[str, str]:
diff --git a/k8s/HELM.md b/k8s/HELM.md
new file mode 100644
index 0000000000..43b7c1d2cd
--- /dev/null
+++ b/k8s/HELM.md
@@ -0,0 +1,16 @@
+# Helm Notes
+
+This file exists to satisfy the Lab 10 requirement for a dedicated Helm document without forcing the entire Kubernetes module back into a flat documentation layout.
+
+## Lab 10 Documentation
+
+The full Helm lab write-up, command transcripts, and verification logs are kept in [docs/LAB10.md](docs/LAB10.md). The Task 5 documentation section that covers chart overview, configuration, hooks, operations, and validation is here: [docs/LAB10.md#task-5-documentation](docs/LAB10.md#task-5-documentation).
+
+## Why This Structure Is Better
+
+- `k8s/README.md` stays short and works as the module entry point instead of becoming a 50 kB transcript dump.
+- `k8s/docs/LAB09.md` and `k8s/docs/LAB10.md` keep each lab self-contained, which scales better as more Kubernetes labs are added.
+- Raw manifests and Helm chart files remain easy to find because documentation is separated from implementation files.
+- `k8s/HELM.md` provides the explicit Helm-facing document name the lab expects, while the detailed content stays in the more maintainable `docs/` hierarchy.
+
+In short, `HELM.md` is the compatibility layer, and `k8s/docs/` is the maintainable structure.
diff --git a/k8s/README.md b/k8s/README.md
index 01f5ed71a6..28a4c6df35 100644
--- a/k8s/README.md
+++ b/k8s/README.md
@@ -1,650 +1,16 @@
-# Kubernetes Lab 9
+# Kubernetes Module
-## Task 1 - Local Kubernetes Setup
+This directory contains the Kubernetes deliverables for the course application. It includes the raw Kubernetes manifests used in Lab 9, the Helm chart created in Lab 10, and the lab write-ups moved into `k8s/docs/` so the module root stays readable.
-I used `minikube` because it was in Arch Linux extra repo (`kind` is only in AUR), integrates cleanly with the Docker driver, and has more features.
+The main deployment assets are:
-
-Cluster setup verification output
+- `deployment.yml`: baseline Kubernetes `Deployment` manifest for the Python app.
+- `service.yml`: baseline Kubernetes `Service` manifest exposing the app inside the cluster and via `NodePort`.
+- `devops-app-py/`: Helm chart version of the application deployment.
+- `docs/`: lab documentation split by assignment.
-```text
-$ minikube status
-minikube
-type: Control Plane
-host: Running
-kubelet: Running
-apiserver: Running
-kubeconfig: Configured
+## Documentation
-
-$ kubectl cluster-info
-Kubernetes control plane is running at https://192.168.49.2:8443
-CoreDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
-
-To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
-
-$ kubectl get nodes -o wide
-NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
-minikube Ready control-plane 2m45s v1.35.1 192.168.49.2 Debian GNU/Linux 12 (bookworm) 6.19.10-1-cachyos docker://29.2.1
-
-$ kubectl get namespaces
-NAME STATUS AGE
-default Active 3m9s
-kube-node-lease Active 3m9s
-kube-public Active 3m9s
-kube-system Active 3m9s
-```
-
-
-
-## Task 2 - Application Deployment
-
-The initial Task 2 deployment used `localt0aster/devops-app-py:1.9-dev` with 3 replicas, rolling updates, and resource requests and limits. At that stage, the probes were `GET /health` for liveness and `GET /ready` for readiness. Task 4 later scaled the manifest to 5 replicas and tightened the rollout strategy.
-
-
-Deployment rollout verification output
-
-```text
-$ kubectl delete deployment devops-app-py --cascade=foreground --wait=true
-deployment.apps "devops-app-py" deleted from default namespace
-
-$ kubectl apply -f k8s/deployment.yml
-deployment.apps/devops-app-py created
-
-$ kubectl rollout status deployment/devops-app-py --timeout=180s
-Waiting for deployment "devops-app-py" rollout to finish: 0 of 3 updated replicas are available...
-Waiting for deployment "devops-app-py" rollout to finish: 1 of 3 updated replicas are available...
-Waiting for deployment "devops-app-py" rollout to finish: 2 of 3 updated replicas are available...
-deployment "devops-app-py" successfully rolled out
-
-$ kubectl get deployment devops-app-py
-NAME READY UP-TO-DATE AVAILABLE AGE
-devops-app-py 3/3 3 3 8s
-
-$ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
-NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
-devops-app-py-76fc7985df-jq2tr 1/1 Running 0 8s 10.244.0.14 minikube
-devops-app-py-76fc7985df-jwpsf 1/1 Running 0 8s 10.244.0.13 minikube
-devops-app-py-76fc7985df-nwr58 1/1 Running 0 8s 10.244.0.12 minikube
-
-$ kubectl describe deployment devops-app-py
-Name: devops-app-py
-Namespace: default
-CreationTimestamp: Fri, 27 Mar 2026 05:16:21 +0300
-Labels: app.kubernetes.io/name=devops-app-py
- app.kubernetes.io/part-of=devops-core-s26
-Annotations: deployment.kubernetes.io/revision: 1
-Selector: app.kubernetes.io/name=devops-app-py
-Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
-StrategyType: RollingUpdate
-MinReadySeconds: 0
-RollingUpdateStrategy: 1 max unavailable, 1 max surge
-Pod Template:
- Labels: app.kubernetes.io/name=devops-app-py
- app.kubernetes.io/part-of=devops-core-s26
- Containers:
- devops-app-py:
- Image: localt0aster/devops-app-py:1.9-dev
- Port: 5000/TCP (http)
- Host Port: 0/TCP (http)
- Limits:
- cpu: 250m
- memory: 256Mi
- Requests:
- cpu: 100m
- memory: 128Mi
- Liveness: http-get http://:http/health delay=10s timeout=2s period=10s #success=1 #failure=3
- Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3
- Environment:
- HOST: 0.0.0.0
- PORT: 5000
- Mounts:
- Volumes:
- Node-Selectors:
- Tolerations:
-Conditions:
- Type Status Reason
- ---- ------ ------
- Available True MinimumReplicasAvailable
- Progressing True NewReplicaSetAvailable
-OldReplicaSets:
-NewReplicaSet: devops-app-py-76fc7985df (3/3 replicas created)
-Events:
- Type Reason Age From Message
- ---- ------ ---- ---- -------
- Normal ScalingReplicaSet 9s deployment-controller Scaled up replica set devops-app-py-76fc7985df from 0 to 3
-```
-
-
-
-## Task 3 - Service Configuration
-
-The Service uses type `NodePort` and targets the Deployment Pods with the `app.kubernetes.io/name=devops-app-py` label. It exposes service port `80` and forwards traffic to container port `5000` on a fixed NodePort, `30080`.
-
-For connectivity verification, I used `kubectl port-forward service/devops-app-py-service 8080:80`. I tested `minikube service ... --url` first, but in this Docker-driver setup the returned node IP was not directly reachable from the host, so port-forward was the practical local-access path.
-
-
-Service verification output
-
-```text
-$ kubectl apply -f k8s/service.yml
-service/devops-app-py-service unchanged
-
-$ kubectl get services
-NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
-devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 32s
-kubernetes ClusterIP 10.96.0.1 443/TCP 80m
-
-$ kubectl describe service devops-app-py-service
-Name: devops-app-py-service
-Namespace: default
-Labels: app.kubernetes.io/name=devops-app-py
- app.kubernetes.io/part-of=devops-core-s26
-Annotations:
-Selector: app.kubernetes.io/name=devops-app-py
-Type: NodePort
-IP Family Policy: SingleStack
-IP Families: IPv4
-IP: 10.110.168.128
-IPs: 10.110.168.128
-Port: http 80/TCP
-TargetPort: 5000/TCP
-NodePort: http 30080/TCP
-Endpoints: 10.244.0.12:5000,10.244.0.13:5000,10.244.0.14:5000
-Session Affinity: None
-External Traffic Policy: Cluster
-Internal Traffic Policy: Cluster
-Events:
-
-$ kubectl get endpoints devops-app-py-service
-Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
-NAME ENDPOINTS AGE
-devops-app-py-service 10.244.0.12:5000,10.244.0.13:5000,10.244.0.14:5000 32s
-
-$ kubectl port-forward service/devops-app-py-service 8080:80
-Forwarding from 127.0.0.1:8080 -> 5000
-Forwarding from [::1]:8080 -> 5000
-Handling connection for 8080
-Handling connection for 8080
-Handling connection for 8080
-Handling connection for 8080
-
-$ curl -fsSL 127.0.0.1:8080 | jq .service.name
-"devops-info-service"
-
-$ curl -fsSL 127.0.0.1:8080/health | jq .status
-"healthy"
-
-$ curl -fsSL 127.0.0.1:8080/ready | jq .status
-"ready"
-
-$ curl -fsSL 127.0.0.1:8080/metrics | head -n 12
-# HELP http_requests_total Total HTTP requests handled by the service.
-# TYPE http_requests_total counter
-http_requests_total{endpoint="/ready",method="GET",status_code="200"} 180.0
-http_requests_total{endpoint="/health",method="GET",status_code="200"} 90.0
-http_requests_total{endpoint="/",method="GET",status_code="200"} 2.0
-http_requests_total{endpoint="/metrics",method="GET",status_code="200"} 1.0
-# HELP http_requests_created Total HTTP requests handled by the service.
-# TYPE http_requests_created gauge
-http_requests_created{endpoint="/ready",method="GET",status_code="200"} 1.7745777896655755e+09
-http_requests_created{endpoint="/health",method="GET",status_code="200"} 1.7745778018120363e+09
-http_requests_created{endpoint="/",method="GET",status_code="200"} 1.7745779956714542e+09
-http_requests_created{endpoint="/metrics",method="GET",status_code="200"} 1.7745779957933705e+09
-```
-
-
-
-## Task 4 - Scaling and Updates
-
-I scaled the Deployment declaratively to 5 replicas and verified that all 5 Pods were running. For the rolling-update portion, I changed the pod template with a temporary `LOG_LEVEL=DEBUG` environment variable. An in-cluster probe exposed a brief failed request with `maxUnavailable: 1`, so I changed the strategy to `maxUnavailable: 0` and reran the rollout. With that stricter strategy, the Service returned `200` for 35 consecutive `/ready` checks during the rollout. I then used `kubectl rollout undo` and returned the live Deployment to the baseline `1.9-dev` pod template while keeping the safer rollout strategy in the manifest.
-
-
-Scaling to 5 replicas
-
-```text
-$ kubectl apply -f k8s/deployment.yml
-deployment.apps/devops-app-py configured
-
-$ kubectl rollout status deployment/devops-app-py --timeout=180s
-Waiting for deployment "devops-app-py" rollout to finish: 3 of 5 updated replicas are available...
-Waiting for deployment "devops-app-py" rollout to finish: 4 of 5 updated replicas are available...
-deployment "devops-app-py" successfully rolled out
-
-$ kubectl get deployment devops-app-py -o wide
-NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
-devops-app-py 5/5 5 5 21m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py
-
-$ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
-NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
-devops-app-py-76fc7985df-jmnrd 1/1 Running 0 13s 10.244.0.16 minikube
-devops-app-py-76fc7985df-jq2tr 1/1 Running 0 21m 10.244.0.14 minikube
-devops-app-py-76fc7985df-jrgms 1/1 Running 0 13s 10.244.0.15 minikube
-devops-app-py-76fc7985df-jwpsf 1/1 Running 0 21m 10.244.0.13 minikube
-devops-app-py-76fc7985df-nwr58 1/1 Running 0 21m 10.244.0.12 minikube
-```
-
-
-
-
-Rolling update with corrected zero-downtime strategy
-
-```text
-$ kubectl apply -f k8s/deployment.yml
-deployment.apps/devops-app-py configured
-
-$ kubectl rollout status deployment/devops-app-py --timeout=240s
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-deployment "devops-app-py" successfully rolled out
-
-$ kubectl get rs -l app.kubernetes.io/name=devops-app-py -o wide
-NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
-devops-app-py-65fc658668 5 5 5 6m45s devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py,pod-template-hash=65fc658668
-devops-app-py-76fc7985df 0 0 0 29m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py,pod-template-hash=76fc7985df
-
-$ kubectl get deployment devops-app-py -o wide
-NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
-devops-app-py 5/5 5 5 29m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py
-
-$ kubectl rollout history deployment/devops-app-py
-deployment.apps/devops-app-py
-REVISION CHANGE-CAUSE
-5
-6
-```
-
-
-
-
-In-cluster readiness probe during rollout
-
-```text
-$ kubectl run task4-probe-zdt \
- --image=curlimages/curl --rm -i --command -- \
- sh -c '
- for i in $(seq 1 35); do
- code=$(curl -sS -o /dev/null -w "%{http_code}" http://devops-app-py-service/ready)
- printf "%s %s\n" "$(date +%H:%M:%S)" "$code"
- sleep 1
- done
- '
-All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
-If you don't see a command prompt, try pressing enter.
-02:44:47 200
-02:44:48 200
-02:44:49 200
-02:44:50 200
-02:44:51 200
-02:44:52 200
-02:44:53 200
-02:44:54 200
-02:44:55 200
-02:44:56 200
-02:44:57 200
-02:44:58 200
-02:44:59 200
-02:45:00 200
-02:45:01 200
-02:45:02 200
-02:45:03 200
-02:45:04 200
-02:45:05 200
-02:45:06 200
-02:45:07 200
-02:45:08 200
-02:45:09 200
-02:45:10 200
-02:45:11 200
-02:45:12 200
-02:45:13 200
-02:45:14 200
-02:45:15 200
-02:45:16 200
-02:45:17 200
-02:45:18 200
-02:45:19 200
-02:45:20 200
-pod "task4-probe-zdt" deleted from default namespace
-```
-
-
-
-
-Rollback and rollout history
-
-```text
-$ kubectl rollout undo deployment/devops-app-py
-deployment.apps/devops-app-py rolled back
-
-$ kubectl rollout status deployment/devops-app-py --timeout=240s
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
-deployment "devops-app-py" successfully rolled out
-
-$ kubectl get rs -l app.kubernetes.io/name=devops-app-py -o wide
-NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
-devops-app-py-65fc658668 0 0 0 7m45s devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py,pod-template-hash=65fc658668
-devops-app-py-76fc7985df 5 5 5 30m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py,pod-template-hash=76fc7985df
-
-$ kubectl get deployment devops-app-py -o wide
-NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
-devops-app-py 5/5 5 5 30m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/name=devops-app-py
-
-$ kubectl rollout history deployment/devops-app-py
-deployment.apps/devops-app-py
-REVISION CHANGE-CAUSE
-6
-7
-```
-
-
-
-## Task 5 - Documentation
-
-### Architecture Overview
-
-The final Kubernetes layout is one `Deployment` and one `NodePort` `Service` in the default namespace. The Deployment runs 5 Flask Pods from `localt0aster/devops-app-py:1.9-dev`, and the Service load-balances traffic from port `80` to container port `5000`.
-
-```mermaid
-flowchart LR
- Client[Client]
- Service[NodePort Service
devops-app-py-service
80 -> 5000
30080/TCP]
- Deployment[Deployment
devops-app-py
5 replicas]
- Pod1[Pod
/]
- Pod2[Pod
/health]
- Pod3[Pod
/ready]
- Pod4[Pod
/metrics]
- Pod5[Pod
5000/TCP]
-
- Client --> Service
- Service --> Deployment
- Deployment --> Pod1
- Deployment --> Pod2
- Deployment --> Pod3
- Deployment --> Pod4
- Deployment --> Pod5
-```
-
-The resource strategy is intentionally small and predictable for a local lab cluster: each Pod requests `100m` CPU and `128Mi` memory, with limits of `250m` CPU and `256Mi` memory. For rollouts, the Deployment now uses `maxSurge: 1` and `maxUnavailable: 0` to preserve availability during Pod replacement.
-
-### Manifest Files
-
-- `k8s/deployment.yml`: defines the `Deployment`, `5` replicas, the `1.9-dev` Python image, labels/selectors, container port, `HOST` and `PORT` environment variables, resource requests and limits, and liveness/readiness probes. `maxUnavailable: 0` was chosen after testing showed that `1` could still allow a transient failed request during rollout.
-- `k8s/service.yml`: defines the `NodePort` `Service`, maps service port `80` to target port `5000`, uses node port `30080`, and selects Pods by `app.kubernetes.io/name=devops-app-py`.
-
-### Deployment Evidence
-
-
-Final cluster evidence
-
-```text
-$ kubectl apply -f k8s/deployment.yml
-deployment.apps/devops-app-py configured
-
-$ kubectl get all
-NAME READY STATUS RESTARTS AGE
-pod/devops-app-py-76fc7985df-6hmn5 1/1 Running 0 52s
-pod/devops-app-py-76fc7985df-6rk64 1/1 Running 0 69s
-pod/devops-app-py-76fc7985df-hr29v 1/1 Running 0 61s
-pod/devops-app-py-76fc7985df-ptjkm 1/1 Running 0 78s
-pod/devops-app-py-76fc7985df-t6d7b 1/1 Running 0 44s
-
-NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
-service/devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 27m
-service/kubernetes ClusterIP 10.96.0.1 443/TCP 107m
-
-NAME READY UP-TO-DATE AVAILABLE AGE
-deployment.apps/devops-app-py 5/5 5 5 30m
-
-NAME DESIRED CURRENT READY AGE
-replicaset.apps/devops-app-py-65fc658668 0 0 0 8m20s
-replicaset.apps/devops-app-py-76fc7985df 5 5 5 30m
-
-$ kubectl get pods,svc -o wide
-NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
-pod/devops-app-py-76fc7985df-6hmn5 1/1 Running 0 52s 10.244.0.47 minikube
-pod/devops-app-py-76fc7985df-6rk64 1/1 Running 0 69s 10.244.0.45 minikube
-pod/devops-app-py-76fc7985df-hr29v 1/1 Running 0 61s 10.244.0.46 minikube
-pod/devops-app-py-76fc7985df-ptjkm 1/1 Running 0 78s 10.244.0.44 minikube
-pod/devops-app-py-76fc7985df-t6d7b 1/1 Running 0 44s 10.244.0.48 minikube
-
-NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
-service/devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 27m app.kubernetes.io/name=devops-app-py
-service/kubernetes ClusterIP 10.96.0.1 443/TCP 107m
-
-$ kubectl describe deployment devops-app-py
-Name: devops-app-py
-Namespace: default
-CreationTimestamp: Fri, 27 Mar 2026 05:16:21 +0300
-Labels: app.kubernetes.io/name=devops-app-py
- app.kubernetes.io/part-of=devops-core-s26
-Annotations: deployment.kubernetes.io/revision: 7
-Selector: app.kubernetes.io/name=devops-app-py
-Replicas: 5 desired | 5 updated | 5 total | 5 available | 0 unavailable
-StrategyType: RollingUpdate
-MinReadySeconds: 0
-RollingUpdateStrategy: 0 max unavailable, 1 max surge
-Pod Template:
- Labels: app.kubernetes.io/name=devops-app-py
- app.kubernetes.io/part-of=devops-core-s26
- Containers:
- devops-app-py:
- Image: localt0aster/devops-app-py:1.9-dev
- Port: 5000/TCP (http)
- Host Port: 0/TCP (http)
- Limits:
- cpu: 250m
- memory: 256Mi
- Requests:
- cpu: 100m
- memory: 128Mi
- Liveness: http-get http://:http/health delay=10s timeout=2s period=10s #success=1 #failure=3
- Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3
- Environment:
- HOST: 0.0.0.0
- PORT: 5000
- Mounts:
- Volumes:
- Node-Selectors:
- Tolerations:
-Conditions:
- Type Status Reason
- ---- ------ ------
- Available True MinimumReplicasAvailable
- Progressing True NewReplicaSetAvailable
-OldReplicaSets: devops-app-py-65fc658668 (0/0 replicas created)
-NewReplicaSet: devops-app-py-76fc7985df (5/5 replicas created)
-Events:
- Type Reason Age From Message
- ---- ------ ---- ---- -------
- Normal ScalingReplicaSet 30m deployment-controller Scaled up replica set devops-app-py-76fc7985df from 0 to 3
- Normal ScalingReplicaSet 9m21s deployment-controller Scaled up replica set devops-app-py-76fc7985df from 3 to 5
- Normal ScalingReplicaSet 8m20s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 0 to 1
- Normal ScalingReplicaSet 8m20s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 1 to 2
- Normal ScalingReplicaSet 8m12s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 4 to 3
- Normal ScalingReplicaSet 8m12s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 2 to 3
- Normal ScalingReplicaSet 8m12s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 3 to 2
- Normal ScalingReplicaSet 8m12s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 3 to 4
- Normal ScalingReplicaSet 8m3s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 2 to 1
- Normal ScalingReplicaSet 4m58s (x2 over 8m20s) deployment-controller Scaled down replica set devops-app-py-76fc7985df from 5 to 4
- Normal ScalingReplicaSet 3m39s (x22 over 8m3s) deployment-controller (combined from similar events): Scaled up replica set devops-app-py-76fc7985df from 0 to 1
-
-$ kubectl run task5-curl \
- --image=curlimages/curl \
- \
- --rm -i \
- --command -- \
- sh -c '
- curl -fsS http://devops-app-py-service
- printf "\n\n"
- curl -fsS http://devops-app-py-service/health
- printf "\n\n"
- curl -fsS http://devops-app-py-service/ready
- printf "\n\n"
- curl -fsS http://devops-app-py-service/metrics | head -n 12
- '
-{
- "endpoints": [
- {
- "description": "Service information.",
- "method": "GET",
- "path": "/"
- },
- {
- "description": "Health check.",
- "method": "GET",
- "path": "/health"
- },
- {
- "description": "Prometheus metrics.",
- "method": "GET",
- "path": "/metrics"
- },
- {
- "description": "Readiness check.",
- "method": "GET",
- "path": "/ready"
- }
- ],
- "request": {
- "client_ip": "10.244.0.49",
- "method": "GET",
- "path": "/",
- "user_agent": "curl/8.12.1"
- },
- "runtime": {
- "human": "0 hours, 1 minutes",
- "seconds": 61
- },
- "service": {
- "description": "DevOps course info service",
- "framework": "Flask",
- "name": "devops-info-service",
- "version": "1.8.0"
- },
- "system": {
- "architecture": "x86_64",
- "cpu_count": 8,
- "hostname": "devops-app-py-76fc7985df-6rk64",
- "platform": "Linux",
- "platform_version": "Alpine Linux v3.23",
- "python_version": "3.14.3"
- }
-}
-
-
-{
- "status": "healthy",
- "timestamp": "2026-03-27T02:47:15.357513+00:00",
- "uptime_seconds": 70
-}
-
-
-{
- "status": "ready",
- "timestamp": "2026-03-27T02:47:15.363373+00:00",
- "uptime_seconds": 53
-}
-
-
-# HELP http_requests_total Total HTTP requests handled by the service.
-# TYPE http_requests_total counter
-http_requests_total{endpoint="/ready",method="GET",status_code="200"} 13.0
-http_requests_total{endpoint="/health",method="GET",status_code="200"} 5.0
-http_requests_total{endpoint="/",method="GET",status_code="200"} 1.0
-# HELP http_requests_created Total HTTP requests handled by the service.
-# TYPE http_requests_created gauge
-http_requests_created{endpoint="/ready",method="GET",status_code="200"} 1.7745795737986815e+09
-http_requests_created{endpoint="/health",method="GET",status_code="200"} 1.7745795857890186e+09
-http_requests_created{endpoint="/",method="GET",status_code="200"} 1.774579635349531e+09
-# HELP http_request_duration_seconds HTTP request duration in seconds.
-# TYPE http_request_duration_seconds histogram
-pod "task5-curl" deleted from default namespace
-```
-
-
-
-### Operations Performed
-
-1. Deployment and verification:
-
- ```bash
- kubectl apply -f k8s/deployment.yml
- kubectl rollout status deployment/devops-app-py
- kubectl get deployment devops-app-py -o wide
- kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
- kubectl describe deployment devops-app-py
- ```
-
-2. Service setup and access checks:
-
- ```bash
- kubectl apply -f k8s/service.yml
- kubectl describe service devops-app-py-service
- kubectl port-forward service/devops-app-py-service 8080:80
- kubectl run task5-curl --image=curlimages/curl --rm -i --command -- sh
- ```
-
-3. Scaling to 5 replicas:
-
- ```bash
- kubectl apply -f k8s/deployment.yml
- kubectl rollout status deployment/devops-app-py --timeout=180s
- kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
- ```
-
-4. Rolling updates and rollback:
-
- ```bash
- kubectl apply -f k8s/deployment.yml
- kubectl rollout status deployment/devops-app-py --timeout=240s
- kubectl rollout history deployment/devops-app-py
- kubectl rollout undo deployment/devops-app-py
- ```
-
-### Production Considerations
-
-- Health checks: `/health` is used for liveness and `/ready` is used for readiness so Kubernetes only sends traffic to Pods that are actually prepared to serve requests.
-- Rollout safety: `maxUnavailable: 0` and `maxSurge: 1` were chosen to keep capacity available during updates. This was not just theoretical; the strategy was tightened after observing a transient failed request with `maxUnavailable: 1`.
-- Resource limits: `100m/128Mi` requests and `250m/256Mi` limits are reasonable for a lab environment and protect the single-node cluster from noisy-neighbor behavior.
-- Observability: the app exposes `/metrics`, which is ready for Prometheus scraping. `kubectl describe`, rollout history, and event inspection were enough for this lab, but production should add centralized logs and dashboards.
-- Further improvements: use image digests instead of mutable tags, add a `PodDisruptionBudget`, enable `HorizontalPodAutoscaler`, isolate workloads in a dedicated namespace, and place an Ingress with TLS in front of the NodePort service.
-
-### Challenges & Solutions
-
-- `minikube service devops-app-py-service --url` returned a valid NodePort URL, but with the Docker driver that node IP was not directly reachable from the host. I used `kubectl port-forward` for local testing and an in-cluster curl Pod for authoritative service verification.
-- The first zero-downtime test with `maxUnavailable: 1` produced a brief failed request during rollout. Instead of papering over it, I changed the Deployment strategy to `maxUnavailable: 0` and reran the test until the probe showed a clean `200` sequence.
-- Host-side port-forward tests can introduce their own connection artifacts during fast backend replacement. The more reliable method here was probing the Kubernetes Service from inside the cluster.
+- [Helm Notes](HELM.md)
+- [Lab 09 - Kubernetes Basics](docs/LAB09.md)
+- [Lab 10 - Helm Package Manager](docs/LAB10.md)
diff --git a/k8s/deployment.yml b/k8s/deployment.yml
index d54bc202ff..362ce70e04 100644
--- a/k8s/deployment.yml
+++ b/k8s/deployment.yml
@@ -24,7 +24,7 @@ spec:
spec:
containers:
- name: devops-app-py
- image: localt0aster/devops-app-py:1.9-dev
+ image: localt0aster/devops-app-py:1.9
imagePullPolicy: IfNotPresent
ports:
- name: http
diff --git a/k8s/devops-app-py/.helmignore b/k8s/devops-app-py/.helmignore
new file mode 100644
index 0000000000..0e8a0eb36f
--- /dev/null
+++ b/k8s/devops-app-py/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/k8s/devops-app-py/Chart.yaml b/k8s/devops-app-py/Chart.yaml
new file mode 100644
index 0000000000..27bec28eb8
--- /dev/null
+++ b/k8s/devops-app-py/Chart.yaml
@@ -0,0 +1,11 @@
+apiVersion: v2
+name: devops-app-py
+description: Helm chart for the DevOps Core Python application
+type: application
+version: 0.2.0
+appVersion: "1.9"
+keywords:
+ - python
+ - flask
+ - kubernetes
+ - helm
diff --git a/k8s/devops-app-py/templates/NOTES.txt b/k8s/devops-app-py/templates/NOTES.txt
new file mode 100644
index 0000000000..7919c1775c
--- /dev/null
+++ b/k8s/devops-app-py/templates/NOTES.txt
@@ -0,0 +1,9 @@
+1. Review the release:
+ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
+
+2. Forward the service locally:
+ kubectl port-forward svc/{{ include "devops-app-py.serviceName" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }}
+
+3. Verify the application:
+ curl -fsSL http://127.0.0.1:8080/health | jq
+ curl -fsSL http://127.0.0.1:8080/ready | jq
diff --git a/k8s/devops-app-py/templates/_helpers.tpl b/k8s/devops-app-py/templates/_helpers.tpl
new file mode 100644
index 0000000000..80d7509c6b
--- /dev/null
+++ b/k8s/devops-app-py/templates/_helpers.tpl
@@ -0,0 +1,73 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "devops-app-py.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "devops-app-py.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "devops-app-py.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "devops-app-py.labels" -}}
+helm.sh/chart: {{ include "devops-app-py.chart" . }}
+{{ include "devops-app-py.selectorLabels" . }}
+{{- if (or .Values.image.tag .Chart.AppVersion) }}
+app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: {{ .Values.partOf }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "devops-app-py.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "devops-app-py.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the service name.
+*/}}
+{{- define "devops-app-py.serviceName" -}}
+{{- printf "%s-service" (include "devops-app-py.fullname" .) | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create the pre-install hook job name.
+*/}}
+{{- define "devops-app-py.preInstallJobName" -}}
+{{- printf "%s-pre-install" (include "devops-app-py.fullname" .) | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create the post-install hook job name.
+*/}}
+{{- define "devops-app-py.postInstallJobName" -}}
+{{- printf "%s-post-install" (include "devops-app-py.fullname" .) | trunc 63 | trimSuffix "-" }}
+{{- end }}
diff --git a/k8s/devops-app-py/templates/deployment.yaml b/k8s/devops-app-py/templates/deployment.yaml
new file mode 100644
index 0000000000..d77904e81b
--- /dev/null
+++ b/k8s/devops-app-py/templates/deployment.yaml
@@ -0,0 +1,55 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "devops-app-py.fullname" . }}
+ labels:
+ {{- include "devops-app-py.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }}
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: {{ .Values.deployment.strategy.maxSurge }}
+ maxUnavailable: {{ .Values.deployment.strategy.maxUnavailable }}
+ selector:
+ matchLabels:
+ {{- include "devops-app-py.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ {{- with .Values.podAnnotations }}
+ annotations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "devops-app-py.selectorLabels" . | nindent 8 }}
+ app.kubernetes.io/part-of: {{ .Values.partOf }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ spec:
+ containers:
+ - name: {{ include "devops-app-py.name" . }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.containerPort }}
+ protocol: TCP
+ env:
+ {{- range .Values.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+ {{- with .Values.livenessProbe }}
+ livenessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.readinessProbe }}
+ readinessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
diff --git a/k8s/devops-app-py/templates/hooks/post-install-job.yaml b/k8s/devops-app-py/templates/hooks/post-install-job.yaml
new file mode 100644
index 0000000000..80c10d64b2
--- /dev/null
+++ b/k8s/devops-app-py/templates/hooks/post-install-job.yaml
@@ -0,0 +1,47 @@
+{{- if .Values.hooks.postInstall.enabled }}
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ include "devops-app-py.postInstallJobName" . }}
+ labels:
+ {{- include "devops-app-py.labels" . | nindent 4 }}
+ app.kubernetes.io/component: hook
+ annotations:
+ "helm.sh/hook": post-install
+ "helm.sh/hook-weight": {{ .Values.hooks.postInstall.weight | quote }}
+ "helm.sh/hook-delete-policy": {{ .Values.hooks.postInstall.deletePolicy | quote }}
+spec:
+ backoffLimit: 0
+ template:
+ metadata:
+ labels:
+ {{- include "devops-app-py.selectorLabels" . | nindent 8 }}
+ app.kubernetes.io/component: hook
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: post-install-smoke-test
+ image: "{{ .Values.hooks.postInstall.image.repository }}:{{ .Values.hooks.postInstall.image.tag }}"
+ imagePullPolicy: {{ .Values.hooks.postInstall.image.pullPolicy }}
+ command:
+ - sh
+ - -c
+ - |
+ set -eu
+ url="http://{{ include "devops-app-py.serviceName" . }}:{{ .Values.service.port }}/ready"
+ attempt=1
+ while [ "$attempt" -le {{ .Values.hooks.postInstall.maxAttempts }} ]; do
+ code="$(curl -sS -o /tmp/ready.json -w "%{http_code}" "$url" || true)"
+ if [ "$code" = "200" ]; then
+ echo "Smoke test passed on attempt ${attempt}"
+ cat /tmp/ready.json
+ sleep 3
+ exit 0
+ fi
+ echo "Attempt ${attempt} returned ${code}"
+ attempt=$((attempt + 1))
+ sleep {{ .Values.hooks.postInstall.retryIntervalSeconds }}
+ done
+ echo "Smoke test failed for ${url}"
+ exit 1
+{{- end }}
diff --git a/k8s/devops-app-py/templates/hooks/pre-install-job.yaml b/k8s/devops-app-py/templates/hooks/pre-install-job.yaml
new file mode 100644
index 0000000000..5493e472b5
--- /dev/null
+++ b/k8s/devops-app-py/templates/hooks/pre-install-job.yaml
@@ -0,0 +1,37 @@
+{{- if .Values.hooks.preInstall.enabled }}
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ include "devops-app-py.preInstallJobName" . }}
+ labels:
+ {{- include "devops-app-py.labels" . | nindent 4 }}
+ app.kubernetes.io/component: hook
+ annotations:
+ "helm.sh/hook": pre-install
+ "helm.sh/hook-weight": {{ .Values.hooks.preInstall.weight | quote }}
+ "helm.sh/hook-delete-policy": {{ .Values.hooks.preInstall.deletePolicy | quote }}
+spec:
+ backoffLimit: 0
+ template:
+ metadata:
+ labels:
+ {{- include "devops-app-py.selectorLabels" . | nindent 8 }}
+ app.kubernetes.io/component: hook
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: pre-install-validation
+ image: "{{ .Values.hooks.preInstall.image.repository }}:{{ .Values.hooks.preInstall.image.tag }}"
+ imagePullPolicy: {{ .Values.hooks.preInstall.image.pullPolicy }}
+ command:
+ - sh
+ - -c
+ - |
+ set -eu
+ echo "Pre-install validation for {{ .Release.Name }}"
+ echo "Namespace: {{ .Release.Namespace }}"
+ echo "Image tag: {{ .Values.image.tag | default .Chart.AppVersion }}"
+ echo "Replica count: {{ .Values.replicaCount }}"
+ sleep 3
+ echo "Pre-install validation completed"
+{{- end }}
diff --git a/k8s/devops-app-py/templates/service.yaml b/k8s/devops-app-py/templates/service.yaml
new file mode 100644
index 0000000000..03a7ec5675
--- /dev/null
+++ b/k8s/devops-app-py/templates/service.yaml
@@ -0,0 +1,18 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "devops-app-py.serviceName" . }}
+ labels:
+ {{- include "devops-app-py.labels" . | nindent 4 }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - name: http
+ protocol: TCP
+ port: {{ .Values.service.port }}
+ targetPort: {{ .Values.service.targetPort }}
+ {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) .Values.service.nodePort }}
+ nodePort: {{ .Values.service.nodePort }}
+ {{- end }}
+ selector:
+ {{- include "devops-app-py.selectorLabels" . | nindent 4 }}
diff --git a/k8s/devops-app-py/values-dev.yaml b/k8s/devops-app-py/values-dev.yaml
new file mode 100644
index 0000000000..f2d401a344
--- /dev/null
+++ b/k8s/devops-app-py/values-dev.yaml
@@ -0,0 +1,50 @@
+replicaCount: 1
+
+image:
+ tag: "1.9-dev"
+
+deployment:
+ revisionHistoryLimit: 2
+
+env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "5000"
+ - name: APP_ENV
+ value: "development"
+
+podLabels:
+ environment: dev
+
+service:
+ type: NodePort
+ port: 80
+ targetPort: 5000
+ nodePort: 30081
+
+resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 100m
+ memory: 128Mi
+
+livenessProbe:
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 2
+ failureThreshold: 3
+
+readinessProbe:
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 3
+ periodSeconds: 5
+ timeoutSeconds: 2
+ failureThreshold: 3
diff --git a/k8s/devops-app-py/values-prod.yaml b/k8s/devops-app-py/values-prod.yaml
new file mode 100644
index 0000000000..cca4fee171
--- /dev/null
+++ b/k8s/devops-app-py/values-prod.yaml
@@ -0,0 +1,50 @@
+replicaCount: 3
+
+image:
+ tag: "1.9"
+
+deployment:
+ revisionHistoryLimit: 10
+
+env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "5000"
+ - name: APP_ENV
+ value: "production"
+
+podLabels:
+ environment: prod
+
+service:
+ type: LoadBalancer
+ port: 80
+ targetPort: 5000
+ nodePort: 30081
+
+resources:
+ requests:
+ cpu: 200m
+ memory: 256Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+
+livenessProbe:
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 30
+ periodSeconds: 5
+ timeoutSeconds: 2
+ failureThreshold: 3
+
+readinessProbe:
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 3
+ timeoutSeconds: 2
+ failureThreshold: 3
diff --git a/k8s/devops-app-py/values.yaml b/k8s/devops-app-py/values.yaml
new file mode 100644
index 0000000000..251caf59d7
--- /dev/null
+++ b/k8s/devops-app-py/values.yaml
@@ -0,0 +1,79 @@
+replicaCount: 5
+partOf: devops-core-s26
+
+image:
+ repository: localt0aster/devops-app-py
+ tag: "1.9"
+ pullPolicy: IfNotPresent
+
+containerPort: 5000
+
+nameOverride: ""
+fullnameOverride: ""
+
+podAnnotations: {}
+podLabels: {}
+
+deployment:
+ revisionHistoryLimit: 5
+ strategy:
+ maxSurge: 1
+ maxUnavailable: 0
+
+env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "5000"
+
+service:
+ type: NodePort
+ port: 80
+ targetPort: 5000
+ nodePort: 30080
+
+resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 250m
+ memory: 256Mi
+
+livenessProbe:
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 2
+ failureThreshold: 3
+
+readinessProbe:
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 2
+ failureThreshold: 3
+
+hooks:
+ preInstall:
+ enabled: true
+ weight: -5
+ deletePolicy: before-hook-creation,hook-succeeded
+ image:
+ repository: busybox
+ tag: "1.37.0"
+ pullPolicy: IfNotPresent
+ postInstall:
+ enabled: true
+ weight: 5
+ deletePolicy: before-hook-creation,hook-succeeded
+ image:
+ repository: curlimages/curl
+ tag: "8.12.1"
+ pullPolicy: IfNotPresent
+ maxAttempts: 20
+ retryIntervalSeconds: 3
diff --git a/k8s/docs/LAB09.md b/k8s/docs/LAB09.md
new file mode 100644
index 0000000000..b20187c999
--- /dev/null
+++ b/k8s/docs/LAB09.md
@@ -0,0 +1,651 @@
+# Kubernetes Lab 9
+
+## Task 1 - Local Kubernetes Setup
+
+I used `minikube` because it was in Arch Linux extra repo (`kind` is only in AUR), integrates cleanly with the Docker driver, and has more features.
+
+
+Cluster setup verification output
+
+```text
+$ minikube status
+minikube
+type: Control Plane
+host: Running
+kubelet: Running
+apiserver: Running
+kubeconfig: Configured
+
+
+$ kubectl cluster-info
+Kubernetes control plane is running at https://192.168.49.2:8443
+CoreDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
+
+To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
+
+$ kubectl get nodes -o wide
+NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
+minikube Ready control-plane 2m45s v1.35.1 192.168.49.2 Debian GNU/Linux 12 (bookworm) 6.19.10-1-cachyos docker://29.2.1
+
+$ kubectl get namespaces
+NAME STATUS AGE
+default Active 3m9s
+kube-node-lease Active 3m9s
+kube-public Active 3m9s
+kube-system Active 3m9s
+```
+
+
+
+## Task 2 - Application Deployment
+
+The initial Task 2 deployment used `localt0aster/devops-app-py:1.9` with 3 replicas, rolling updates, and resource requests and limits. At that stage, the probes were `GET /health` for liveness and `GET /ready` for readiness. Task 4 later scaled the manifest to 5 replicas and tightened the rollout strategy.
+
+
+Deployment rollout verification output
+
+```text
+$ kubectl delete deployment devops-app-py --cascade=foreground --wait=true
+deployment.apps "devops-app-py" deleted from default namespace
+
+$ kubectl apply -f k8s/deployment.yml
+deployment.apps/devops-app-py created
+
+$ kubectl rollout status deployment/devops-app-py --timeout=180s
+Waiting for deployment "devops-app-py" rollout to finish: 0 of 3 updated replicas are available...
+Waiting for deployment "devops-app-py" rollout to finish: 1 of 3 updated replicas are available...
+Waiting for deployment "devops-app-py" rollout to finish: 2 of 3 updated replicas are available...
+deployment "devops-app-py" successfully rolled out
+
+$ kubectl get deployment devops-app-py
+NAME READY UP-TO-DATE AVAILABLE AGE
+devops-app-py 3/3 3 3 8s
+
+$ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+devops-app-py-76fc7985df-jq2tr 1/1 Running 0 8s 10.244.0.14 minikube
+devops-app-py-76fc7985df-jwpsf 1/1 Running 0 8s 10.244.0.13 minikube
+devops-app-py-76fc7985df-nwr58 1/1 Running 0 8s 10.244.0.12 minikube
+
+$ kubectl describe deployment devops-app-py
+Name: devops-app-py
+Namespace: default
+CreationTimestamp: Fri, 27 Mar 2026 05:16:21 +0300
+Labels: app.kubernetes.io/name=devops-app-py
+ app.kubernetes.io/part-of=devops-core-s26
+Annotations: deployment.kubernetes.io/revision: 1
+Selector: app.kubernetes.io/name=devops-app-py
+Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable
+StrategyType: RollingUpdate
+MinReadySeconds: 0
+RollingUpdateStrategy: 1 max unavailable, 1 max surge
+Pod Template:
+ Labels: app.kubernetes.io/name=devops-app-py
+ app.kubernetes.io/part-of=devops-core-s26
+ Containers:
+ devops-app-py:
+ Image: localt0aster/devops-app-py:1.9
+ Port: 5000/TCP (http)
+ Host Port: 0/TCP (http)
+ Limits:
+ cpu: 250m
+ memory: 256Mi
+ Requests:
+ cpu: 100m
+ memory: 128Mi
+ Liveness: http-get http://:http/health delay=10s timeout=2s period=10s #success=1 #failure=3
+ Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3
+ Environment:
+ HOST: 0.0.0.0
+ PORT: 5000
+ Mounts:
+ Volumes:
+ Node-Selectors:
+ Tolerations:
+Conditions:
+ Type Status Reason
+ ---- ------ ------
+ Available True MinimumReplicasAvailable
+ Progressing True NewReplicaSetAvailable
+OldReplicaSets:
+NewReplicaSet: devops-app-py-76fc7985df (3/3 replicas created)
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal ScalingReplicaSet 9s deployment-controller Scaled up replica set devops-app-py-76fc7985df from 0 to 3
+```
+
+
+
+## Task 3 - Service Configuration
+
+The Service uses type `NodePort` and targets the Deployment Pods with the `app.kubernetes.io/name=devops-app-py` label. It exposes service port `80` and forwards traffic to container port `5000` on a fixed NodePort, `30080`.
+
+For connectivity verification, I used `kubectl port-forward service/devops-app-py-service 8080:80`. I tested `minikube service ... --url` first, but in this Docker-driver setup the returned node IP was not directly reachable from the host, so port-forward was the practical local-access path.
+
+
+Service verification output
+
+```text
+$ kubectl apply -f k8s/service.yml
+service/devops-app-py-service unchanged
+
+$ kubectl get services
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 32s
+kubernetes ClusterIP 10.96.0.1 443/TCP 80m
+
+$ kubectl describe service devops-app-py-service
+Name: devops-app-py-service
+Namespace: default
+Labels: app.kubernetes.io/name=devops-app-py
+ app.kubernetes.io/part-of=devops-core-s26
+Annotations:
+Selector: app.kubernetes.io/name=devops-app-py
+Type: NodePort
+IP Family Policy: SingleStack
+IP Families: IPv4
+IP: 10.110.168.128
+IPs: 10.110.168.128
+Port: http 80/TCP
+TargetPort: 5000/TCP
+NodePort: http 30080/TCP
+Endpoints: 10.244.0.12:5000,10.244.0.13:5000,10.244.0.14:5000
+Session Affinity: None
+External Traffic Policy: Cluster
+Internal Traffic Policy: Cluster
+Events:
+
+$ kubectl get endpoints devops-app-py-service
+Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
+NAME ENDPOINTS AGE
+devops-app-py-service 10.244.0.12:5000,10.244.0.13:5000,10.244.0.14:5000 32s
+
+$ kubectl port-forward service/devops-app-py-service 8080:80
+Forwarding from 127.0.0.1:8080 -> 5000
+Forwarding from [::1]:8080 -> 5000
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+Handling connection for 8080
+
+$ curl -fsSL 127.0.0.1:8080 | jq .service.name
+"devops-info-service"
+
+$ curl -fsSL 127.0.0.1:8080/health | jq .status
+"healthy"
+
+$ curl -fsSL 127.0.0.1:8080/ready | jq .status
+"ready"
+
+$ curl -fsSL 127.0.0.1:8080/metrics | head -n 12
+# HELP http_requests_total Total HTTP requests handled by the service.
+# TYPE http_requests_total counter
+http_requests_total{endpoint="/ready",method="GET",status_code="200"} 180.0
+http_requests_total{endpoint="/health",method="GET",status_code="200"} 90.0
+http_requests_total{endpoint="/",method="GET",status_code="200"} 2.0
+http_requests_total{endpoint="/metrics",method="GET",status_code="200"} 1.0
+# HELP http_requests_created Total HTTP requests handled by the service.
+# TYPE http_requests_created gauge
+http_requests_created{endpoint="/ready",method="GET",status_code="200"} 1.7745777896655755e+09
+http_requests_created{endpoint="/health",method="GET",status_code="200"} 1.7745778018120363e+09
+http_requests_created{endpoint="/",method="GET",status_code="200"} 1.7745779956714542e+09
+http_requests_created{endpoint="/metrics",method="GET",status_code="200"} 1.7745779957933705e+09
+```
+
+
+
+## Task 4 - Scaling and Updates
+
+I scaled the Deployment declaratively to 5 replicas and verified that all 5 Pods were running. For the rolling-update portion, I changed the pod template with a temporary `LOG_LEVEL=DEBUG` environment variable. An in-cluster probe exposed a brief failed request with `maxUnavailable: 1`, so I changed the strategy to `maxUnavailable: 0` and reran the rollout. With that stricter strategy, the Service returned `200` for 35 consecutive `/ready` checks during the rollout. I then used `kubectl rollout undo` and returned the live Deployment to the baseline `1.9` pod template while keeping the safer rollout strategy in the manifest.
+
+
+Scaling to 5 replicas
+
+```text
+$ kubectl apply -f k8s/deployment.yml
+deployment.apps/devops-app-py configured
+
+$ kubectl rollout status deployment/devops-app-py --timeout=180s
+Waiting for deployment "devops-app-py" rollout to finish: 3 of 5 updated replicas are available...
+Waiting for deployment "devops-app-py" rollout to finish: 4 of 5 updated replicas are available...
+deployment "devops-app-py" successfully rolled out
+
+$ kubectl get deployment devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+devops-app-py 5/5 5 5 21m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py
+
+$ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+devops-app-py-76fc7985df-jmnrd 1/1 Running 0 13s 10.244.0.16 minikube
+devops-app-py-76fc7985df-jq2tr 1/1 Running 0 21m 10.244.0.14 minikube
+devops-app-py-76fc7985df-jrgms 1/1 Running 0 13s 10.244.0.15 minikube
+devops-app-py-76fc7985df-jwpsf 1/1 Running 0 21m 10.244.0.13 minikube
+devops-app-py-76fc7985df-nwr58 1/1 Running 0 21m 10.244.0.12 minikube
+```
+
+
+
+
+Rolling update with corrected zero-downtime strategy
+
+```text
+$ kubectl apply -f k8s/deployment.yml
+deployment.apps/devops-app-py configured
+
+$ kubectl rollout status deployment/devops-app-py --timeout=240s
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+deployment "devops-app-py" successfully rolled out
+
+$ kubectl get rs -l app.kubernetes.io/name=devops-app-py -o wide
+NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
+devops-app-py-65fc658668 5 5 5 6m45s devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py,pod-template-hash=65fc658668
+devops-app-py-76fc7985df 0 0 0 29m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py,pod-template-hash=76fc7985df
+
+$ kubectl get deployment devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+devops-app-py 5/5 5 5 29m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py
+
+$ kubectl rollout history deployment/devops-app-py
+deployment.apps/devops-app-py
+REVISION CHANGE-CAUSE
+5
+6
+```
+
+
+
+
+In-cluster readiness probe during rollout
+
+```text
+$ kubectl run task4-probe-zdt \
+ --image=curlimages/curl --rm -i --command -- \
+ sh -c '
+ for i in $(seq 1 35); do
+ code=$(curl -sS -o /dev/null -w "%{http_code}" http://devops-app-py-service/ready)
+ printf "%s %s\n" "$(date +%H:%M:%S)" "$code"
+ sleep 1
+ done
+ '
+All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
+If you don't see a command prompt, try pressing enter.
+02:44:47 200
+02:44:48 200
+02:44:49 200
+02:44:50 200
+02:44:51 200
+02:44:52 200
+02:44:53 200
+02:44:54 200
+02:44:55 200
+02:44:56 200
+02:44:57 200
+02:44:58 200
+02:44:59 200
+02:45:00 200
+02:45:01 200
+02:45:02 200
+02:45:03 200
+02:45:04 200
+02:45:05 200
+02:45:06 200
+02:45:07 200
+02:45:08 200
+02:45:09 200
+02:45:10 200
+02:45:11 200
+02:45:12 200
+02:45:13 200
+02:45:14 200
+02:45:15 200
+02:45:16 200
+02:45:17 200
+02:45:18 200
+02:45:19 200
+02:45:20 200
+pod "task4-probe-zdt" deleted from default namespace
+```
+
+
+
+
+Rollback and rollout history
+
+```text
+$ kubectl rollout undo deployment/devops-app-py
+deployment.apps/devops-app-py rolled back
+
+$ kubectl rollout status deployment/devops-app-py --timeout=240s
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 2 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 3 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 4 out of 5 new replicas have been updated...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+Waiting for deployment "devops-app-py" rollout to finish: 1 old replicas are pending termination...
+deployment "devops-app-py" successfully rolled out
+
+$ kubectl get rs -l app.kubernetes.io/name=devops-app-py -o wide
+NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
+devops-app-py-65fc658668 0 0 0 7m45s devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py,pod-template-hash=65fc658668
+devops-app-py-76fc7985df 5 5 5 30m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py,pod-template-hash=76fc7985df
+
+$ kubectl get deployment devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+devops-app-py 5/5 5 5 30m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/name=devops-app-py
+
+$ kubectl rollout history deployment/devops-app-py
+deployment.apps/devops-app-py
+REVISION CHANGE-CAUSE
+6
+7
+```
+
+
+
+## Task 5 - Documentation
+
+### Architecture Overview
+
+The final Kubernetes layout is one `Deployment` and one `NodePort` `Service` in the default namespace. The Deployment runs 5 Flask Pods from `localt0aster/devops-app-py:1.9`, and the Service load-balances traffic from port `80` to container port `5000`.
+
+```mermaid
+flowchart LR
+ Client[Client]
+ Service[NodePort Service
devops-app-py-service
80 -> 5000
30080/TCP]
+ Deployment[Deployment
devops-app-py
5 replicas]
+ Pod1[Pod
/]
+ Pod2[Pod
/health]
+ Pod3[Pod
/ready]
+ Pod4[Pod
/metrics]
+ Pod5[Pod
5000/TCP]
+
+ Client --> Service
+ Service --> Deployment
+ Deployment --> Pod1
+ Deployment --> Pod2
+ Deployment --> Pod3
+ Deployment --> Pod4
+ Deployment --> Pod5
+```
+
+The resource strategy is intentionally small and predictable for a local lab cluster: each Pod requests `100m` CPU and `128Mi` memory, with limits of `250m` CPU and `256Mi` memory. For rollouts, the Deployment now uses `maxSurge: 1` and `maxUnavailable: 0` to preserve availability during Pod replacement.
+
+### Manifest Files
+
+- `k8s/deployment.yml`: defines the `Deployment`, `5` replicas, the `1.9` Python image, labels/selectors, container port, `HOST` and `PORT` environment variables, resource requests and limits, and liveness/readiness probes. `maxUnavailable: 0` was chosen after testing showed that `1` could still allow a transient failed request during rollout.
+- `k8s/service.yml`: defines the `NodePort` `Service`, maps service port `80` to target port `5000`, uses node port `30080`, and selects Pods by `app.kubernetes.io/name=devops-app-py`.
+
+### Deployment Evidence
+
+
+Final cluster evidence
+
+```text
+$ kubectl apply -f k8s/deployment.yml
+deployment.apps/devops-app-py configured
+
+$ kubectl get all
+NAME READY STATUS RESTARTS AGE
+pod/devops-app-py-76fc7985df-6hmn5 1/1 Running 0 52s
+pod/devops-app-py-76fc7985df-6rk64 1/1 Running 0 69s
+pod/devops-app-py-76fc7985df-hr29v 1/1 Running 0 61s
+pod/devops-app-py-76fc7985df-ptjkm 1/1 Running 0 78s
+pod/devops-app-py-76fc7985df-t6d7b 1/1 Running 0 44s
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+service/devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 27m
+service/kubernetes ClusterIP 10.96.0.1 443/TCP 107m
+
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/devops-app-py 5/5 5 5 30m
+
+NAME DESIRED CURRENT READY AGE
+replicaset.apps/devops-app-py-65fc658668 0 0 0 8m20s
+replicaset.apps/devops-app-py-76fc7985df 5 5 5 30m
+
+$ kubectl get pods,svc -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+pod/devops-app-py-76fc7985df-6hmn5 1/1 Running 0 52s 10.244.0.47 minikube
+pod/devops-app-py-76fc7985df-6rk64 1/1 Running 0 69s 10.244.0.45 minikube
+pod/devops-app-py-76fc7985df-hr29v 1/1 Running 0 61s 10.244.0.46 minikube
+pod/devops-app-py-76fc7985df-ptjkm 1/1 Running 0 78s 10.244.0.44 minikube
+pod/devops-app-py-76fc7985df-t6d7b 1/1 Running 0 44s 10.244.0.48 minikube
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
+service/devops-app-py-service NodePort 10.110.168.128 80:30080/TCP 27m app.kubernetes.io/name=devops-app-py
+service/kubernetes ClusterIP 10.96.0.1 443/TCP 107m
+
+$ kubectl describe deployment devops-app-py
+Name: devops-app-py
+Namespace: default
+CreationTimestamp: Fri, 27 Mar 2026 05:16:21 +0300
+Labels: app.kubernetes.io/name=devops-app-py
+ app.kubernetes.io/part-of=devops-core-s26
+Annotations: deployment.kubernetes.io/revision: 7
+Selector: app.kubernetes.io/name=devops-app-py
+Replicas: 5 desired | 5 updated | 5 total | 5 available | 0 unavailable
+StrategyType: RollingUpdate
+MinReadySeconds: 0
+RollingUpdateStrategy: 0 max unavailable, 1 max surge
+Pod Template:
+ Labels: app.kubernetes.io/name=devops-app-py
+ app.kubernetes.io/part-of=devops-core-s26
+ Containers:
+ devops-app-py:
+ Image: localt0aster/devops-app-py:1.9
+ Port: 5000/TCP (http)
+ Host Port: 0/TCP (http)
+ Limits:
+ cpu: 250m
+ memory: 256Mi
+ Requests:
+ cpu: 100m
+ memory: 128Mi
+ Liveness: http-get http://:http/health delay=10s timeout=2s period=10s #success=1 #failure=3
+ Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3
+ Environment:
+ HOST: 0.0.0.0
+ PORT: 5000
+ Mounts:
+ Volumes:
+ Node-Selectors:
+ Tolerations:
+Conditions:
+ Type Status Reason
+ ---- ------ ------
+ Available True MinimumReplicasAvailable
+ Progressing True NewReplicaSetAvailable
+OldReplicaSets: devops-app-py-65fc658668 (0/0 replicas created)
+NewReplicaSet: devops-app-py-76fc7985df (5/5 replicas created)
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal ScalingReplicaSet 30m deployment-controller Scaled up replica set devops-app-py-76fc7985df from 0 to 3
+ Normal ScalingReplicaSet 9m21s deployment-controller Scaled up replica set devops-app-py-76fc7985df from 3 to 5
+ Normal ScalingReplicaSet 8m20s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 0 to 1
+ Normal ScalingReplicaSet 8m20s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 1 to 2
+ Normal ScalingReplicaSet 8m12s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 4 to 3
+ Normal ScalingReplicaSet 8m12s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 2 to 3
+ Normal ScalingReplicaSet 8m12s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 3 to 2
+ Normal ScalingReplicaSet 8m12s deployment-controller Scaled up replica set devops-app-py-65fc658668 from 3 to 4
+ Normal ScalingReplicaSet 8m3s deployment-controller Scaled down replica set devops-app-py-76fc7985df from 2 to 1
+ Normal ScalingReplicaSet 4m58s (x2 over 8m20s) deployment-controller Scaled down replica set devops-app-py-76fc7985df from 5 to 4
+ Normal ScalingReplicaSet 3m39s (x22 over 8m3s) deployment-controller (combined from similar events): Scaled up replica set devops-app-py-76fc7985df from 0 to 1
+
+$ kubectl run task5-curl \
+ --image=curlimages/curl \
+ \
+ --rm -i \
+ --command -- \
+ sh -c '
+ curl -fsS http://devops-app-py-service
+ printf "\n\n"
+ curl -fsS http://devops-app-py-service/health
+ printf "\n\n"
+ curl -fsS http://devops-app-py-service/ready
+ printf "\n\n"
+ curl -fsS http://devops-app-py-service/metrics | head -n 12
+ '
+{
+ "endpoints": [
+ {
+ "description": "Service information.",
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "description": "Health check.",
+ "method": "GET",
+ "path": "/health"
+ },
+ {
+ "description": "Prometheus metrics.",
+ "method": "GET",
+ "path": "/metrics"
+ },
+ {
+ "description": "Readiness check.",
+ "method": "GET",
+ "path": "/ready"
+ }
+ ],
+ "request": {
+ "client_ip": "10.244.0.49",
+ "method": "GET",
+ "path": "/",
+ "user_agent": "curl/8.12.1"
+ },
+ "runtime": {
+ "human": "0 hours, 1 minutes",
+ "seconds": 61
+ },
+ "service": {
+ "description": "DevOps course info service",
+ "framework": "Flask",
+ "name": "devops-info-service",
+ "version": "1.8.0"
+ },
+ "system": {
+ "architecture": "x86_64",
+ "cpu_count": 8,
+ "hostname": "devops-app-py-76fc7985df-6rk64",
+ "platform": "Linux",
+ "platform_version": "Alpine Linux v3.23",
+ "python_version": "3.14.3"
+ }
+}
+
+
+{
+ "status": "healthy",
+ "timestamp": "2026-03-27T02:47:15.357513+00:00",
+ "uptime_seconds": 70
+}
+
+
+{
+ "status": "ready",
+ "timestamp": "2026-03-27T02:47:15.363373+00:00",
+ "uptime_seconds": 53
+}
+
+
+# HELP http_requests_total Total HTTP requests handled by the service.
+# TYPE http_requests_total counter
+http_requests_total{endpoint="/ready",method="GET",status_code="200"} 13.0
+http_requests_total{endpoint="/health",method="GET",status_code="200"} 5.0
+http_requests_total{endpoint="/",method="GET",status_code="200"} 1.0
+# HELP http_requests_created Total HTTP requests handled by the service.
+# TYPE http_requests_created gauge
+http_requests_created{endpoint="/ready",method="GET",status_code="200"} 1.7745795737986815e+09
+http_requests_created{endpoint="/health",method="GET",status_code="200"} 1.7745795857890186e+09
+http_requests_created{endpoint="/",method="GET",status_code="200"} 1.774579635349531e+09
+# HELP http_request_duration_seconds HTTP request duration in seconds.
+# TYPE http_request_duration_seconds histogram
+pod "task5-curl" deleted from default namespace
+```
+
+
+
+### Operations Performed
+
+1. Deployment and verification:
+
+ ```bash
+ kubectl apply -f k8s/deployment.yml
+ kubectl rollout status deployment/devops-app-py
+ kubectl get deployment devops-app-py -o wide
+ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
+ kubectl describe deployment devops-app-py
+ ```
+
+2. Service setup and access checks:
+
+ ```bash
+ kubectl apply -f k8s/service.yml
+ kubectl describe service devops-app-py-service
+ kubectl port-forward service/devops-app-py-service 8080:80
+ kubectl run task5-curl --image=curlimages/curl --rm -i --command -- sh
+ ```
+
+3. Scaling to 5 replicas:
+
+ ```bash
+ kubectl apply -f k8s/deployment.yml
+ kubectl rollout status deployment/devops-app-py --timeout=180s
+ kubectl get pods -l app.kubernetes.io/name=devops-app-py -o wide
+ ```
+
+4. Rolling updates and rollback:
+
+ ```bash
+ kubectl apply -f k8s/deployment.yml
+ kubectl rollout status deployment/devops-app-py --timeout=240s
+ kubectl rollout history deployment/devops-app-py
+ kubectl rollout undo deployment/devops-app-py
+ ```
+
+### Production Considerations
+
+- Health checks: `/health` is used for liveness and `/ready` is used for readiness so Kubernetes only sends traffic to Pods that are actually prepared to serve requests.
+- Rollout safety: `maxUnavailable: 0` and `maxSurge: 1` were chosen to keep capacity available during updates. This was not just theoretical; the strategy was tightened after observing a transient failed request with `maxUnavailable: 1`.
+- Resource limits: `100m/128Mi` requests and `250m/256Mi` limits are reasonable for a lab environment and protect the single-node cluster from noisy-neighbor behavior.
+- Observability: the app exposes `/metrics`, which is ready for Prometheus scraping. `kubectl describe`, rollout history, and event inspection were enough for this lab, but production should add centralized logs and dashboards.
+- Further improvements: use image digests instead of mutable tags, add a `PodDisruptionBudget`, enable `HorizontalPodAutoscaler`, isolate workloads in a dedicated namespace, and place an Ingress with TLS in front of the NodePort service.
+
+### Challenges & Solutions
+
+- `minikube service devops-app-py-service --url` returned a valid NodePort URL, but with the Docker driver that node IP was not directly reachable from the host. I used `kubectl port-forward` for local testing and an in-cluster curl Pod for authoritative service verification.
+- The first zero-downtime test with `maxUnavailable: 1` produced a brief failed request during rollout. Instead of papering over it, I changed the Deployment strategy to `maxUnavailable: 0` and reran the test until the probe showed a clean `200` sequence.
+- Host-side port-forward tests can introduce their own connection artifacts during fast backend replacement. The more reliable method here was probing the Kubernetes Service from inside the cluster.
+
diff --git a/k8s/docs/LAB10.md b/k8s/docs/LAB10.md
new file mode 100644
index 0000000000..f2bbd951b7
--- /dev/null
+++ b/k8s/docs/LAB10.md
@@ -0,0 +1,1064 @@
+# Kubernetes Lab 10 - Helm Package Manager
+
+## Task 1 - Helm Fundamentals
+
+Helm is a package manager for Kubernetes. In practical terms, a chart bundles templates, defaults, and metadata so the same application can be installed as reusable releases instead of copying raw YAML by hand. Repositories distribute those charts, and `values.yaml` provides the layer where environment-specific settings are changed without rewriting templates.
+
+For the fundamentals task, I verified the local Helm installation, refreshed public repositories, searched available Prometheus-related charts, inspected the metadata of the public `prometheus-community/prometheus` chart, and pulled it locally to review the typical chart structure. The public chart layout confirmed the core Helm pattern: `Chart.yaml` for metadata, `values.yaml` and `values.schema.json` for configuration, `_helpers.tpl` for naming/label helpers, and `templates/` for rendered Kubernetes manifests.
+
+
+Task 1 command output
+
+```bash
+$ helm version
+version.BuildInfo{Version:"v4.1.3", GitCommit:"c94d381b03be117e7e57908edbf642104e00eb8f", GitTreeState:"", GoVersion:"go1.26.1-X:nodwarf5", KubeClientVersion:"v1.35"}
+
+$ helm repo add bitnami https://charts.bitnami.com/bitnami
+"bitnami" has been added to your repositories
+
+$ helm repo update
+Hang tight while we grab the latest from your chart repositories...
+...Successfully got an update from the "prometheus-community" chart repository
+...Successfully got an update from the "bitnami" chart repository
+Update Complete. ⎈Happy Helming!⎈
+
+$ helm search repo prometheus
+NAME CHART VERSION APP VERSION DESCRIPTION
+bitnami/kube-prometheus 11.3.10 0.85.0 Prometheus Operator provides easy monitoring de...
+bitnami/prometheus 2.1.23 3.5.0 Prometheus is an open source monitoring and ale...
+bitnami/wavefront-prometheus-storage-adapter 2.3.3 1.0.7 DEPRECATED Wavefront Storage Adapter is a Prome...
+prometheus-community/kube-prometheus-stack 82.16.1 v0.89.0 kube-prometheus-stack collects Kubernetes manif...
+prometheus-community/prometheus 28.15.0 v3.11.0 Prometheus is a monitoring system and time seri...
+prometheus-community/prometheus-adapter 5.3.0 v0.12.0 A Helm chart for k8s prometheus adapter
+prometheus-community/prometheus-blackbox-exporter 11.9.1 v0.28.0 Prometheus Blackbox Exporter
+prometheus-community/prometheus-cloudwatch-expo... 0.28.1 0.16.0 A Helm chart for prometheus cloudwatch-exporter
+prometheus-community/prometheus-conntrack-stats... 0.5.35 v0.4.42 A Helm chart for conntrack-stats-exporter
+prometheus-community/prometheus-consul-exporter 1.1.1 v0.13.0 A Helm chart for the Prometheus Consul Exporter
+prometheus-community/prometheus-couchdb-exporter 1.0.1 1.0 A Helm chart to export the metrics from couchdb...
+prometheus-community/prometheus-druid-exporter 1.2.0 v0.11.0 Druid exporter to monitor druid metrics with Pr...
+prometheus-community/prometheus-elasticsearch-e... 7.2.1 v1.10.0 Elasticsearch stats exporter for Prometheus
+prometheus-community/prometheus-fastly-exporter 0.11.0 v10.2.0 A Helm chart for the Prometheus Fastly Exporter
+prometheus-community/prometheus-ipmi-exporter 0.8.0 v1.10.1 This is an IPMI exporter for Prometheus.
+prometheus-community/prometheus-json-exporter 0.19.2 v0.7.0 Install prometheus-json-exporter
+prometheus-community/prometheus-kafka-exporter 3.0.1 v1.9.0 A Helm chart to export metrics from Kafka in Pr...
+prometheus-community/prometheus-memcached-exporter 0.4.5 v0.15.5 Prometheus exporter for Memcached metrics
+prometheus-community/prometheus-modbus-exporter 0.1.4 0.4.1 A Helm chart for prometheus-modbus-exporter
+prometheus-community/prometheus-mongodb-exporter 3.18.0 0.49.0 A Prometheus exporter for MongoDB metrics
+prometheus-community/prometheus-mysql-exporter 2.13.0 v0.19.0 A Helm chart for prometheus mysql exporter with...
+prometheus-community/prometheus-nats-exporter 2.22.1 0.19.2 A Helm chart for prometheus-nats-exporter
+prometheus-community/prometheus-nginx-exporter 1.20.8 1.5.1 A Helm chart for NGINX Prometheus Exporter
+prometheus-community/prometheus-node-exporter 4.52.2 1.10.2 A Helm chart for prometheus node-exporter
+prometheus-community/prometheus-opencost-exporter 0.1.2 1.108.0 Prometheus OpenCost Exporter
+prometheus-community/prometheus-operator 9.3.2 0.38.1 DEPRECATED - This chart will be renamed. See ht...
+prometheus-community/prometheus-operator-admiss... 0.38.0 0.90.1 Prometheus Operator Admission Webhook
+prometheus-community/prometheus-operator-crds 28.0.1 v0.90.1 A Helm chart that collects custom resource defi...
+prometheus-community/prometheus-pgbouncer-exporter 0.10.0 v0.12.0 A Helm chart for prometheus pgbouncer-exporter
+prometheus-community/prometheus-pingdom-exporter 3.4.2 v0.5.6 A Helm chart for Prometheus Pingdom Exporter
+prometheus-community/prometheus-pingmesh-exporter 0.4.3 v1.2.2 Prometheus Pingmesh Exporter
+prometheus-community/prometheus-postgres-exporter 7.5.2 v0.19.1 A Helm chart for prometheus postgres-exporter
+prometheus-community/prometheus-pushgateway 3.6.0 v1.11.2 A Helm chart for prometheus pushgateway
+prometheus-community/prometheus-rabbitmq-exporter 2.1.2 1.0.0 Rabbitmq metrics exporter for prometheus
+prometheus-community/prometheus-redis-exporter 6.22.0 v1.82.0 Prometheus exporter for Redis metrics
+prometheus-community/prometheus-smartctl-exporter 0.16.0 v0.14.0 A Helm chart for Kubernetes
+prometheus-community/prometheus-snmp-exporter 9.13.1 v0.30.1 Prometheus SNMP Exporter
+prometheus-community/prometheus-sql-exporter 0.5.0 v0.8 Prometheus SQL Exporter
+prometheus-community/prometheus-stackdriver-exp... 4.12.2 v0.18.0 Stackdriver exporter for Prometheus
+prometheus-community/prometheus-statsd-exporter 1.0.0 v0.28.0 A Helm chart for prometheus stats-exporter
+prometheus-community/prometheus-systemd-exporter 0.5.2 0.7.0 A Helm chart for prometheus systemd-exporter
+prometheus-community/prometheus-to-sd 0.5.1 v0.9.2 Scrape metrics stored in prometheus format and ...
+prometheus-community/prometheus-windows-exporter 0.12.6 0.31.6 A Helm chart for prometheus windows-exporter
+prometheus-community/prometheus-yet-another-clo... 0.43.0 v0.64.0 Yace - Yet Another CloudWatch Exporter
+prometheus-community/alertmanager 1.34.0 v0.31.1 The Alertmanager handles alerts sent by client ...
+prometheus-community/alertmanager-snmp-notifier 2.1.0 v2.1.0 The SNMP Notifier handles alerts coming from Pr...
+prometheus-community/jiralert 1.8.2 v1.3.0 A Helm chart for Kubernetes to install jiralert
+prometheus-community/kube-state-metrics 7.2.2 2.18.0 Install kube-state-metrics to generate and expo...
+prometheus-community/prom-label-proxy 0.18.0 v0.12.1 A proxy that enforces a given label in a given ...
+prometheus-community/yet-another-cloudwatch-exp... 0.39.1 v0.62.1 Yace - Yet Another CloudWatch Exporter
+bitnami/grafana-alloy 1.0.7 1.10.2 Grafana Alloy is an open source OpenTelemetry C...
+bitnami/grafana-mimir 3.0.18 2.17.0 Grafana Mimir is an open source, horizontally s...
+bitnami/node-exporter 4.5.19 1.9.1 Prometheus exporter for hardware and OS metrics...
+bitnami/thanos 17.3.1 0.39.2 Thanos is a highly available metrics system tha...
+bitnami/victoriametrics 0.1.31 1.124.0 VictoriaMetrics is a fast, cost-effective, and ...
+bitnami/kube-state-metrics 5.1.0 2.16.0 kube-state-metrics is a simple service that lis...
+bitnami/mariadb 25.0.6 12.2.2 MariaDB is an open source, community-developed ...
+bitnami/mariadb-galera 16.0.1 12.0.2 MariaDB Galera is a multi-primary database clus...
+
+$ helm show chart prometheus-community/prometheus
+annotations:
+ artifacthub.io/license: Apache-2.0
+ artifacthub.io/links: |
+ - name: Chart Source
+ url: https://github.com/prometheus-community/helm-charts
+ - name: Upstream Project
+ url: https://github.com/prometheus/prometheus
+apiVersion: v2
+appVersion: v3.11.0
+dependencies:
+- condition: alertmanager.enabled
+ name: alertmanager
+ repository: https://prometheus-community.github.io/helm-charts
+ version: 1.34.*
+- condition: kube-state-metrics.enabled
+ name: kube-state-metrics
+ repository: https://prometheus-community.github.io/helm-charts
+ version: 7.2.*
+- condition: prometheus-node-exporter.enabled
+ name: prometheus-node-exporter
+ repository: https://prometheus-community.github.io/helm-charts
+ version: 4.52.*
+- condition: prometheus-pushgateway.enabled
+ name: prometheus-pushgateway
+ repository: https://prometheus-community.github.io/helm-charts
+ version: 3.6.*
+description: Prometheus is a monitoring system and time series database.
+home: https://prometheus.io/
+icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png
+keywords:
+- monitoring
+- prometheus
+kubeVersion: '>=1.19.0-0'
+maintainers:
+- email: gianrubio@gmail.com
+ name: gianrubio
+ url: https://github.com/gianrubio
+- email: zanhsieh@gmail.com
+ name: zanhsieh
+ url: https://github.com/zanhsieh
+- email: miroslav.hadzhiev@gmail.com
+ name: Xtigyro
+ url: https://github.com/Xtigyro
+- email: naseem@transit.app
+ name: naseemkullah
+ url: https://github.com/naseemkullah
+- email: rootsandtrees@posteo.de
+ name: zeritti
+ url: https://github.com/zeritti
+name: prometheus
+sources:
+- https://github.com/prometheus/alertmanager
+- https://github.com/prometheus/prometheus
+- https://github.com/prometheus/pushgateway
+- https://github.com/prometheus/node_exporter
+- https://github.com/kubernetes/kube-state-metrics
+type: application
+version: 28.15.0
+
+
+$ helm pull prometheus-community/prometheus --untar --untardir /tmp/lab10-public-chart
+
+$ find /tmp/lab10-public-chart/prometheus -maxdepth 2 -type f
+/tmp/lab10-public-chart/prometheus/README.md
+/tmp/lab10-public-chart/prometheus/.helmignore
+/tmp/lab10-public-chart/prometheus/templates/vpa.yaml
+/tmp/lab10-public-chart/prometheus/templates/serviceaccount.yaml
+/tmp/lab10-public-chart/prometheus/templates/service.yaml
+/tmp/lab10-public-chart/prometheus/templates/rolebinding.yaml
+/tmp/lab10-public-chart/prometheus/templates/pvc.yaml
+/tmp/lab10-public-chart/prometheus/templates/pdb.yaml
+/tmp/lab10-public-chart/prometheus/templates/network-policy.yaml
+/tmp/lab10-public-chart/prometheus/templates/ingress.yaml
+/tmp/lab10-public-chart/prometheus/templates/httproute.yaml
+/tmp/lab10-public-chart/prometheus/templates/headless-svc.yaml
+/tmp/lab10-public-chart/prometheus/templates/extra-manifests.yaml
+/tmp/lab10-public-chart/prometheus/templates/deploy.yaml
+/tmp/lab10-public-chart/prometheus/templates/cm.yaml
+/tmp/lab10-public-chart/prometheus/templates/clusterrolebinding.yaml
+/tmp/lab10-public-chart/prometheus/templates/clusterrole.yaml
+/tmp/lab10-public-chart/prometheus/templates/_helpers.tpl
+/tmp/lab10-public-chart/prometheus/templates/NOTES.txt
+/tmp/lab10-public-chart/prometheus/values.schema.json
+/tmp/lab10-public-chart/prometheus/values.yaml
+/tmp/lab10-public-chart/prometheus/Chart.lock
+/tmp/lab10-public-chart/prometheus/Chart.yaml
+```
+
+
+
+## Task 2 - Create Your Helm Chart
+
+I created the application chart in `k8s/devops-app-py` with the standard Helm structure and converted the existing Lab 9 Deployment and Service into templates. The chart keeps the same application behavior while moving the changeable parts into values: image repository and tag, replica count, rollout strategy, environment variables, resource requests and limits, service settings, and both probes. Naming and labels are centralized in `_helpers.tpl` so the Deployment and Service stay consistent across installs.
+
+The chart was deliberately trimmed to what the lab actually uses. I removed the default scaffold templates for ingress, autoscaling, service accounts, test hooks, and HTTPRoute because they were noise for this app and would have made the chart look more generic than intentional. One practical detail mattered during real installation: the raw Lab 9 service was still occupying `30080`, so I kept the chart default at `30080` but installed the Lab 10 release with `--set service.nodePort=30081` to avoid a collision while preserving the chart defaults required by the lab.
+
+
+Task 2 command output
+
+```bash
+$ helm lint k8s/devops-app-py
+==> Linting k8s/devops-app-py
+[INFO] Chart.yaml: icon is recommended
+
+1 chart(s) linted, 0 chart(s) failed
+
+$ helm template devops-app-py k8s/devops-app-py
+---
+# Source: devops-app-py/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: devops-app-py-service
+ labels:
+ helm.sh/chart: devops-app-py-0.1.0
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: devops-app-py
+ app.kubernetes.io/version: "1.9"
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/part-of: devops-core-s26
+spec:
+ type: NodePort
+ ports:
+ - name: http
+ protocol: TCP
+ port: 80
+ targetPort: 5000
+ nodePort: 30080
+ selector:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: devops-app-py
+---
+# Source: devops-app-py/templates/deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: devops-app-py
+ labels:
+ helm.sh/chart: devops-app-py-0.1.0
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: devops-app-py
+ app.kubernetes.io/version: "1.9"
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/part-of: devops-core-s26
+spec:
+ replicas: 5
+ revisionHistoryLimit: 5
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: devops-app-py
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: devops-app-py
+ app.kubernetes.io/part-of: devops-core-s26
+ spec:
+ containers:
+ - name: devops-app-py
+ image: "localt0aster/devops-app-py:1.9"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: 5000
+ protocol: TCP
+ env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "5000"
+ livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 2
+ readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 2
+ resources:
+ limits:
+ cpu: 250m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
+$ helm install --dry-run --debug test-release k8s/devops-app-py --set service.nodePort=30081
+level=WARN msg="--dry-run is deprecated and should be replaced with '--dry-run=client'"
+level=DEBUG msg="Original chart version" version=""
+level=DEBUG msg="Chart path" path=/home/t0ast/Repos/DevOps-Core-S26/k8s/devops-app-py
+level=DEBUG msg="number of dependencies in the chart" chart=devops-app-py dependencies=0
+NAME: test-release
+LAST DEPLOYED: Thu Apr 2 23:09:12 2026
+NAMESPACE: default
+STATUS: pending-install
+REVISION: 1
+DESCRIPTION: Dry run complete
+TEST SUITE: None
+USER-SUPPLIED VALUES:
+service:
+ nodePort: 30081
+
+COMPUTED VALUES:
+containerPort: 5000
+deployment:
+ revisionHistoryLimit: 5
+ strategy:
+ maxSurge: 1
+ maxUnavailable: 0
+env:
+- name: HOST
+ value: 0.0.0.0
+- name: PORT
+ value: "5000"
+fullnameOverride: ""
+image:
+ pullPolicy: IfNotPresent
+ repository: localt0aster/devops-app-py
+ tag: 1.9
+livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 2
+nameOverride: ""
+partOf: devops-core-s26
+podAnnotations: {}
+podLabels: {}
+readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 2
+replicaCount: 5
+resources:
+ limits:
+ cpu: 250m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+service:
+ nodePort: 30081
+ port: 80
+ targetPort: 5000
+ type: NodePort
+
+HOOKS:
+MANIFEST:
+---
+# Source: devops-app-py/templates/service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: test-release-devops-app-py-service
+ labels:
+ helm.sh/chart: devops-app-py-0.1.0
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: test-release
+ app.kubernetes.io/version: "1.9"
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/part-of: devops-core-s26
+spec:
+ type: NodePort
+ ports:
+ - name: http
+ protocol: TCP
+ port: 80
+ targetPort: 5000
+ nodePort: 30081
+ selector:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: test-release
+---
+# Source: devops-app-py/templates/deployment.yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: test-release-devops-app-py
+ labels:
+ helm.sh/chart: devops-app-py-0.1.0
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: test-release
+ app.kubernetes.io/version: "1.9"
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/part-of: devops-core-s26
+spec:
+ replicas: 5
+ revisionHistoryLimit: 5
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: test-release
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: devops-app-py
+ app.kubernetes.io/instance: test-release
+ app.kubernetes.io/part-of: devops-core-s26
+ spec:
+ containers:
+ - name: devops-app-py
+ image: "localt0aster/devops-app-py:1.9"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: 5000
+ protocol: TCP
+ env:
+ - name: HOST
+ value: "0.0.0.0"
+ - name: PORT
+ value: "5000"
+ livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 2
+ readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 2
+ resources:
+ limits:
+ cpu: 250m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
+NOTES:
+1. Review the release:
+ helm status test-release -n default
+
+2. Forward the service locally:
+ kubectl port-forward svc/test-release-devops-app-py-service 8080:80 -n default
+
+3. Verify the application:
+ curl -fsSL http://127.0.0.1:8080/health
+ curl -fsSL http://127.0.0.1:8080/ready
+
+$ helm install lab10-devops-app-py k8s/devops-app-py --set service.nodePort=30081
+NAME: lab10-devops-app-py
+LAST DEPLOYED: Thu Apr 2 23:09:12 2026
+NAMESPACE: default
+STATUS: deployed
+REVISION: 1
+DESCRIPTION: Install complete
+TEST SUITE: None
+NOTES:
+1. Review the release:
+ helm status lab10-devops-app-py -n default
+
+2. Forward the service locally:
+ kubectl port-forward svc/lab10-devops-app-py-service 8080:80 -n default
+
+3. Verify the application:
+ curl -fsSL http://127.0.0.1:8080/health
+ curl -fsSL http://127.0.0.1:8080/ready
+
+$ kubectl rollout status deployment/lab10-devops-app-py --timeout=240s
+Waiting for deployment "lab10-devops-app-py" rollout to finish: 0 of 5 updated replicas are available...
+Waiting for deployment "lab10-devops-app-py" rollout to finish: 1 of 5 updated replicas are available...
+Waiting for deployment "lab10-devops-app-py" rollout to finish: 2 of 5 updated replicas are available...
+Waiting for deployment "lab10-devops-app-py" rollout to finish: 3 of 5 updated replicas are available...
+Waiting for deployment "lab10-devops-app-py" rollout to finish: 4 of 5 updated replicas are available...
+deployment "lab10-devops-app-py" successfully rolled out
+
+$ helm list -A
+NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
+lab10-devops-app-py default 1 2026-04-02 23:09:12.132768347 +0300 +03 deployed devops-app-py-0.1.0 1.9
+
+$ kubectl get deploy,svc,pods -l app.kubernetes.io/instance=lab10-devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+deployment.apps/lab10-devops-app-py 5/5 5 5 7s devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
+service/lab10-devops-app-py-service NodePort 10.96.60.48 80:30081/TCP 7s app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+pod/lab10-devops-app-py-7b7dbf4648-6k55b 1/1 Running 0 7s 10.244.0.60 minikube
+pod/lab10-devops-app-py-7b7dbf4648-fz8j2 1/1 Running 0 7s 10.244.0.58 minikube
+pod/lab10-devops-app-py-7b7dbf4648-l5fdj 1/1 Running 0 7s 10.244.0.56 minikube
+pod/lab10-devops-app-py-7b7dbf4648-sdklz 1/1 Running 0 7s 10.244.0.57 minikube
+pod/lab10-devops-app-py-7b7dbf4648-zp9dt 1/1 Running 0 7s 10.244.0.59 minikube
+
+$ kubectl port-forward svc/lab10-devops-app-py-service 18082:80
+Forwarding from 127.0.0.1:18082 -> 5000
+Forwarding from [::1]:18082 -> 5000
+
+$ curl -fsSL http://127.0.0.1:18082 | jq .
+{
+ "endpoints": [
+ {
+ "description": "Service information.",
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "description": "Health check.",
+ "method": "GET",
+ "path": "/health"
+ },
+ {
+ "description": "Prometheus metrics.",
+ "method": "GET",
+ "path": "/metrics"
+ },
+ {
+ "description": "Readiness check.",
+ "method": "GET",
+ "path": "/ready"
+ }
+ ],
+ "request": {
+ "client_ip": "127.0.0.1",
+ "method": "GET",
+ "path": "/",
+ "user_agent": "curl/8.19.0"
+ },
+ "runtime": {
+ "human": "0 hours, 0 minutes",
+ "seconds": 12
+ },
+ "service": {
+ "description": "DevOps course info service",
+ "framework": "Flask",
+ "name": "devops-info-service",
+ "version": "1.8.0"
+ },
+ "system": {
+ "architecture": "x86_64",
+ "cpu_count": 8,
+ "hostname": "lab10-devops-app-py-7b7dbf4648-6k55b",
+ "platform": "Linux",
+ "platform_version": "Alpine Linux v3.23",
+ "python_version": "3.14.3"
+ }
+}
+
+$ curl -fsSL http://127.0.0.1:18082/health | jq .
+{
+ "status": "healthy",
+ "timestamp": "2026-04-02T20:09:31.062442+00:00",
+ "uptime_seconds": 12
+}
+
+$ curl -fsSL http://127.0.0.1:18082/ready | jq .
+{
+ "status": "ready",
+ "timestamp": "2026-04-02T20:09:31.100199+00:00",
+ "uptime_seconds": 13
+}
+```
+
+
+
+## Task 3 - Multi-Environment Support
+
+I added two environment-specific values files to the chart: `values-dev.yaml` for a lightweight local deployment and `values-prod.yaml` for a more production-shaped configuration. The dev profile uses a single replica, smaller CPU and memory reservations, `APP_ENV=development`, and the `localt0aster/devops-app-py:1.9-dev` image on a `NodePort` service. The prod profile raises the deployment to 3 replicas, increases resource requests and limits, switches `APP_ENV=production`, uses the `localt0aster/devops-app-py:1.9` image, and changes the service type to `LoadBalancer`.
+
+I tested the environment flow on the real release instead of only rendering templates. First I reinstalled `lab10-devops-app-py` with the dev values, verified the single-replica `1.9-dev` deployment, and then upgraded the same release with the prod values. The service type changed to `LoadBalancer` and the Deployment converged to 3 ready Pods. In this minikube setup the external IP stayed ``, which is expected without cloud load-balancer integration, so I verified the upgraded release with `kubectl port-forward` and `curl ... | jq .` against `/ready`.
+
+
+Task 3 command output
+
+```bash
+$ helm uninstall lab10-devops-app-py
+release "lab10-devops-app-py" uninstalled
+
+$ helm install lab10-devops-app-py k8s/devops-app-py -f k8s/devops-app-py/values-dev.yaml --wait=watcher --wait-for-jobs --timeout 240s
+NAME: lab10-devops-app-py
+LAST DEPLOYED: Fri Apr 3 01:40:19 2026
+NAMESPACE: default
+STATUS: deployed
+REVISION: 1
+DESCRIPTION: Install complete
+TEST SUITE: None
+NOTES:
+1. Review the release:
+ helm status lab10-devops-app-py -n default
+
+2. Forward the service locally:
+ kubectl port-forward svc/lab10-devops-app-py-service 8080:80 -n default
+
+3. Verify the application:
+ curl -fsSL http://127.0.0.1:8080/health | jq
+ curl -fsSL http://127.0.0.1:8080/ready | jq
+
+$ helm get values lab10-devops-app-py --all
+COMPUTED VALUES:
+containerPort: 5000
+deployment:
+ revisionHistoryLimit: 2
+ strategy:
+ maxSurge: 1
+ maxUnavailable: 0
+env:
+- name: HOST
+ value: 0.0.0.0
+- name: PORT
+ value: "5000"
+- name: APP_ENV
+ value: development
+fullnameOverride: ""
+hooks:
+ postInstall:
+ deletePolicy: before-hook-creation,hook-succeeded
+ enabled: true
+ image:
+ pullPolicy: IfNotPresent
+ repository: curlimages/curl
+ tag: 8.12.1
+ maxAttempts: 20
+ retryIntervalSeconds: 3
+ weight: 5
+ preInstall:
+ deletePolicy: before-hook-creation,hook-succeeded
+ enabled: true
+ image:
+ pullPolicy: IfNotPresent
+ repository: busybox
+ tag: 1.37.0
+ weight: -5
+image:
+ pullPolicy: IfNotPresent
+ repository: localt0aster/devops-app-py
+ tag: 1.9-dev
+livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 2
+nameOverride: ""
+partOf: devops-core-s26
+podAnnotations: {}
+podLabels:
+ environment: dev
+readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 3
+ periodSeconds: 5
+ timeoutSeconds: 2
+replicaCount: 1
+resources:
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ requests:
+ cpu: 50m
+ memory: 64Mi
+service:
+ nodePort: 30081
+ port: 80
+ targetPort: 5000
+ type: NodePort
+
+$ kubectl get deploy,svc,pods -l app.kubernetes.io/instance=lab10-devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+deployment.apps/lab10-devops-app-py 1/1 1 1 23s devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
+service/lab10-devops-app-py-service NodePort 10.102.64.255 80:30081/TCP 23s app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+pod/lab10-devops-app-py-7fd54dc44b-5lndl 1/1 Running 0 23s 10.244.0.62 minikube
+
+$ helm upgrade lab10-devops-app-py k8s/devops-app-py -f k8s/devops-app-py/values-prod.yaml --wait=watcher --timeout 240s
+Release "lab10-devops-app-py" has been upgraded. Happy Helming!
+NAME: lab10-devops-app-py
+LAST DEPLOYED: Fri Apr 3 01:40:53 2026
+NAMESPACE: default
+STATUS: deployed
+REVISION: 2
+DESCRIPTION: Upgrade complete
+TEST SUITE: None
+NOTES:
+1. Review the release:
+ helm status lab10-devops-app-py -n default
+
+2. Forward the service locally:
+ kubectl port-forward svc/lab10-devops-app-py-service 8080:80 -n default
+
+3. Verify the application:
+ curl -fsSL http://127.0.0.1:8080/health | jq
+ curl -fsSL http://127.0.0.1:8080/ready | jq
+
+$ kubectl rollout status deployment/lab10-devops-app-py --timeout=240s
+deployment "lab10-devops-app-py" successfully rolled out
+
+$ helm get values lab10-devops-app-py --all
+COMPUTED VALUES:
+containerPort: 5000
+deployment:
+ revisionHistoryLimit: 10
+ strategy:
+ maxSurge: 1
+ maxUnavailable: 0
+env:
+- name: HOST
+ value: 0.0.0.0
+- name: PORT
+ value: "5000"
+- name: APP_ENV
+ value: production
+fullnameOverride: ""
+hooks:
+ postInstall:
+ deletePolicy: before-hook-creation,hook-succeeded
+ enabled: true
+ image:
+ pullPolicy: IfNotPresent
+ repository: curlimages/curl
+ tag: 8.12.1
+ maxAttempts: 20
+ retryIntervalSeconds: 3
+ weight: 5
+ preInstall:
+ deletePolicy: before-hook-creation,hook-succeeded
+ enabled: true
+ image:
+ pullPolicy: IfNotPresent
+ repository: busybox
+ tag: 1.37.0
+ weight: -5
+image:
+ pullPolicy: IfNotPresent
+ repository: localt0aster/devops-app-py
+ tag: "1.9"
+livenessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /health
+ port: http
+ initialDelaySeconds: 30
+ periodSeconds: 5
+ timeoutSeconds: 2
+nameOverride: ""
+partOf: devops-core-s26
+podAnnotations: {}
+podLabels:
+ environment: prod
+readinessProbe:
+ failureThreshold: 3
+ httpGet:
+ path: /ready
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 3
+ timeoutSeconds: 2
+replicaCount: 3
+resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 200m
+ memory: 256Mi
+service:
+ nodePort: 30081
+ port: 80
+ targetPort: 5000
+ type: LoadBalancer
+
+$ kubectl get deploy,svc,pods -l app.kubernetes.io/instance=lab10-devops-app-py -o wide
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+deployment.apps/lab10-devops-app-py 3/3 3 3 65s devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
+service/lab10-devops-app-py-service LoadBalancer 10.102.64.255 80:30081/TCP 65s app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+pod/lab10-devops-app-py-67694d9f5c-57h24 1/1 Running 0 22s 10.244.0.67 minikube
+pod/lab10-devops-app-py-67694d9f5c-7scvn 1/1 Running 0 41s 10.244.0.64 minikube
+pod/lab10-devops-app-py-67694d9f5c-tk2kt 1/1 Running 0 11s 10.244.0.68 minikube
+pod/lab10-devops-app-py-7fd54dc44b-5lndl 1/1 Terminating 0 65s 10.244.0.62 minikube
+
+$ kubectl port-forward svc/lab10-devops-app-py-service 18083:80
+Forwarding from 127.0.0.1:18083 -> 5000
+Forwarding from [::1]:18083 -> 5000
+
+$ curl -fsSL http://127.0.0.1:18083/ready | jq .
+{
+ "status": "ready",
+ "timestamp": "2026-04-02T22:41:38.602170+00:00",
+ "uptime_seconds": 33
+}
+```
+
+
+
+## Task 4 - Chart Hooks
+
+I added two lifecycle hook Jobs under `templates/hooks/`. The pre-install hook is a small validation job based on `busybox`, and the post-install hook is a smoke test based on `curlimages/curl` that checks the release-local Service on `/ready`. Their weights are `-5` and `5` respectively, so the validation step runs first and the smoke test runs after the workload is installed. Both hooks use the deletion policy `before-hook-creation,hook-succeeded` so repeated installs do not accumulate stale Jobs.
+
+Verification happened in two layers. First, a dry run showed both hook manifests rendering under Helm’s `HOOKS:` section with the expected annotations. Then the real dev installation produced the expected Kubernetes events for both Jobs, including `Completed` on pre-install and post-install. After completion, `kubectl get jobs -A` returned no resources and no hook pods remained, which confirmed the cleanup policy worked in practice.
+
+
+Task 4 command output
+
+```bash
+$ helm lint k8s/devops-app-py
+==> Linting k8s/devops-app-py
+[INFO] Chart.yaml: icon is recommended
+
+1 chart(s) linted, 0 chart(s) failed
+
+$ helm install --dry-run=client --debug hook-preview k8s/devops-app-py -f k8s/devops-app-py/values-dev.yaml | rg -n -C 3 'Source: devops-app-py/templates/hooks|kind: Job|name: hook-preview-devops-app-py-(pre-install|post-install)|helm.sh/hook|helm.sh/hook-weight|helm.sh/hook-delete-policy'
+124-
+125-HOOKS:
+126----
+127:# Source: devops-app-py/templates/hooks/post-install-job.yaml
+128-apiVersion: batch/v1
+129:kind: Job
+130-metadata:
+131: name: hook-preview-devops-app-py-post-install
+132- labels:
+133- helm.sh/chart: devops-app-py-0.2.0
+134- app.kubernetes.io/name: devops-app-py
+--
+138- app.kubernetes.io/part-of: devops-core-s26
+139- app.kubernetes.io/component: hook
+140- annotations:
+141: "helm.sh/hook": post-install
+142: "helm.sh/hook-weight": "5"
+143: "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
+144-spec:
+145- backoffLimit: 0
+146- template:
+--
+177- echo "Smoke test failed for ${url}"
+178- exit 1
+179----
+180:# Source: devops-app-py/templates/hooks/pre-install-job.yaml
+181-apiVersion: batch/v1
+182:kind: Job
+183-metadata:
+184: name: hook-preview-devops-app-py-pre-install
+185- labels:
+186- helm.sh/chart: devops-app-py-0.2.0
+187- app.kubernetes.io/name: devops-app-py
+--
+191- app.kubernetes.io/part-of: devops-core-s26
+192- app.kubernetes.io/component: hook
+193- annotations:
+194: "helm.sh/hook": pre-install
+195: "helm.sh/hook-weight": "-5"
+196: "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
+197-spec:
+198- backoffLimit: 0
+199- template:
+
+$ kubectl get events -A --sort-by=.metadata.creationTimestamp | rg 'lab10-devops-app-py-(pre-install|post-install)|Job completed|Created pod: lab10-devops-app-py-(pre-install|post-install)'
+default 112s Normal SuccessfulCreate job/lab10-devops-app-py-pre-install Created pod: lab10-devops-app-py-pre-install-gl882
+default 111s Normal Scheduled pod/lab10-devops-app-py-pre-install-gl882 Successfully assigned default/lab10-devops-app-py-pre-install-gl882 to minikube
+default 111s Normal Pulling pod/lab10-devops-app-py-pre-install-gl882 Pulling image "busybox:1.37.0"
+default 107s Normal Pulled pod/lab10-devops-app-py-pre-install-gl882 Successfully pulled image "busybox:1.37.0" in 4.711s (4.711s including waiting). Image size: 4421246 bytes.
+default 107s Normal Created pod/lab10-devops-app-py-pre-install-gl882 Container created
+default 106s Normal Started pod/lab10-devops-app-py-pre-install-gl882 Container started
+default 101s Normal Completed job/lab10-devops-app-py-pre-install Job completed
+default 84s Normal SuccessfulCreate job/lab10-devops-app-py-post-install Created pod: lab10-devops-app-py-post-install-9jpjt
+default 83s Normal Scheduled pod/lab10-devops-app-py-post-install-9jpjt Successfully assigned default/lab10-devops-app-py-post-install-9jpjt to minikube
+default 83s Normal Pulled pod/lab10-devops-app-py-post-install-9jpjt Container image "curlimages/curl:8.12.1" already present on machine and can be accessed by the pod
+default 83s Normal Created pod/lab10-devops-app-py-post-install-9jpjt Container created
+default 83s Normal Started pod/lab10-devops-app-py-post-install-9jpjt Container started
+default 78s Normal Completed job/lab10-devops-app-py-post-install Job completed
+
+$ kubectl get jobs -A 2>&1
+No resources found
+
+$ kubectl get pods -A | rg 'lab10-devops-app-py-(pre-install|post-install)' || true
+```
+
+
+
+## Task 5 - Documentation
+
+This section completes the documentation requirement for the Helm chart itself. The course asks for `k8s/HELM.md`; in this repo that file is kept as a compatibility entry point, while the detailed write-up lives here in `k8s/docs/LAB10.md` so the module root does not turn into a transcript dump.
+
+### Chart Overview
+
+The chart lives in `k8s/devops-app-py` and is split into a small set of focused files:
+
+- `Chart.yaml`: chart metadata, chart version, and app version.
+- `values.yaml`: common defaults shared by all environments.
+- `values-dev.yaml`: local development override with `1` replica, smaller resources, `NodePort`, and `1.9-dev`.
+- `values-prod.yaml`: production-shaped override with `3` replicas, larger resources, `LoadBalancer`, and `1.9`.
+- `templates/_helpers.tpl`: shared naming and label helpers, including service and hook job names.
+- `templates/deployment.yaml`: the main application Deployment template.
+- `templates/service.yaml`: the Service template, supporting both `NodePort` and `LoadBalancer`.
+- `templates/hooks/pre-install-job.yaml`: validation job that runs before install.
+- `templates/hooks/post-install-job.yaml`: smoke test job that runs after install.
+- `templates/NOTES.txt`: post-install usage hints.
+
+The values strategy is layered: keep sensible defaults in `values.yaml`, then use environment overlays to change only what differs between dev and prod. That keeps templates stable and pushes configuration changes to values files instead of templating conditionals everywhere.
+
+### Configuration Guide
+
+The most important values are:
+
+- `replicaCount`: controls pod count for each environment.
+- `image.repository` and `image.tag`: define which application image is deployed.
+- `service.type`, `service.port`, `service.targetPort`, and `service.nodePort`: define exposure strategy.
+- `resources.requests` and `resources.limits`: shape scheduling and runtime ceilings.
+- `livenessProbe` and `readinessProbe`: keep health checks configurable without removing them.
+- `env`: injects runtime environment variables like `HOST`, `PORT`, and `APP_ENV`.
+- `hooks.preInstall.*` and `hooks.postInstall.*`: configure hook enablement, weight, deletion policy, image, and retry behavior.
+
+Example usage:
+
+```bash
+# Development installation
+helm install lab10-devops-app-py k8s/devops-app-py \
+ -f k8s/devops-app-py/values-dev.yaml \
+ --wait=watcher \
+ --wait-for-jobs
+
+# Upgrade the same release to the production profile
+helm upgrade lab10-devops-app-py k8s/devops-app-py \
+ -f k8s/devops-app-py/values-prod.yaml \
+ --wait=watcher
+
+# Override a specific value without editing files
+helm upgrade lab10-devops-app-py k8s/devops-app-py \
+ -f k8s/devops-app-py/values-prod.yaml \
+ --set replicaCount=4
+```
+
+### Hook Implementation
+
+Two hooks are implemented:
+
+- Pre-install hook: a `busybox` validation job that records the release name, namespace, image tag, and replica count before installation proceeds.
+- Post-install hook: a `curlimages/curl` smoke test job that polls `http:///ready` until it gets HTTP `200` or times out.
+
+Execution order is controlled by weights:
+
+- `pre-install`: weight `-5`
+- `post-install`: weight `5`
+
+Deletion is handled by `before-hook-creation,hook-succeeded`, which means Helm removes old hook resources before recreating them and cleans up successful Jobs afterward. The cluster evidence below confirms that no hook Jobs remain after completion.
+
+### Installation Evidence
+
+
+Current chart and release evidence
+
+```bash
+$ find k8s/devops-app-py -maxdepth 3 -type f | sort
+k8s/devops-app-py/.helmignore
+k8s/devops-app-py/Chart.yaml
+k8s/devops-app-py/templates/NOTES.txt
+k8s/devops-app-py/templates/_helpers.tpl
+k8s/devops-app-py/templates/deployment.yaml
+k8s/devops-app-py/templates/hooks/post-install-job.yaml
+k8s/devops-app-py/templates/hooks/pre-install-job.yaml
+k8s/devops-app-py/templates/service.yaml
+k8s/devops-app-py/values-dev.yaml
+k8s/devops-app-py/values-prod.yaml
+k8s/devops-app-py/values.yaml
+
+$ helm list -A
+NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
+lab10-devops-app-py default 2 2026-04-03 01:40:53.968813438 +0300 +03 deployed devops-app-py-0.2.0 1.9
+
+$ helm history lab10-devops-app-py
+REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
+1 Fri Apr 3 01:40:19 2026 superseded devops-app-py-0.2.0 1.9 Install complete
+2 Fri Apr 3 01:40:53 2026 deployed devops-app-py-0.2.0 1.9 Upgrade complete
+
+$ kubectl get all -l app.kubernetes.io/instance=lab10-devops-app-py -o wide
+NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
+pod/lab10-devops-app-py-67694d9f5c-57h24 1/1 Running 0 14m 10.244.0.67 minikube
+pod/lab10-devops-app-py-67694d9f5c-7scvn 1/1 Running 0 14m 10.244.0.64 minikube
+pod/lab10-devops-app-py-67694d9f5c-tk2kt 1/1 Running 0 14m 10.244.0.68 minikube
+
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
+service/lab10-devops-app-py-service LoadBalancer 10.102.64.255 80:30081/TCP 15m app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
+deployment.apps/lab10-devops-app-py 3/3 3 3 15m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py
+
+NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
+replicaset.apps/lab10-devops-app-py-67694d9f5c 3 3 3 14m devops-app-py localt0aster/devops-app-py:1.9 app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py,pod-template-hash=67694d9f5c
+replicaset.apps/lab10-devops-app-py-7fd54dc44b 0 0 0 15m devops-app-py localt0aster/devops-app-py:1.9-dev app.kubernetes.io/instance=lab10-devops-app-py,app.kubernetes.io/name=devops-app-py,pod-template-hash=7fd54dc44b
+
+$ kubectl get jobs -A 2>&1
+No resources found
+```
+
+
+
+### Operations
+
+1. Install the development profile:
+
+ ```bash
+ helm install lab10-devops-app-py k8s/devops-app-py \
+ -f k8s/devops-app-py/values-dev.yaml \
+ --wait=watcher \
+ --wait-for-jobs \
+ --timeout 240s
+ ```
+
+2. Upgrade to the production profile:
+
+ ```bash
+ helm upgrade lab10-devops-app-py k8s/devops-app-py \
+ -f k8s/devops-app-py/values-prod.yaml \
+ --wait=watcher \
+ --timeout 240s
+ ```
+
+3. Inspect and troubleshoot the release:
+
+ ```bash
+ helm list -A
+ helm history lab10-devops-app-py
+ helm get values lab10-devops-app-py --all
+ kubectl get all -l app.kubernetes.io/instance=lab10-devops-app-py -o wide
+ ```
+
+4. Roll back or remove the release:
+
+ ```bash
+ helm rollback lab10-devops-app-py 1
+ helm uninstall lab10-devops-app-py
+ ```
+
+### Testing & Validation
+
+Validation was performed at several levels:
+
+- Static validation: `helm lint` passed with only the non-blocking `icon is recommended` note.
+- Render validation: `helm template ... -f values-prod.yaml` showed `Service`, `Deployment`, and both hook `Job` resources, with `type: LoadBalancer`, `replicas: 3`, and the expected hook annotations.
+- Dry-run validation: Task 4’s `helm install --dry-run=client --debug` output showed both hooks under the `HOOKS:` section before any cluster changes were applied.
+- Runtime validation: Task 3 verified the dev install, the prod upgrade, and service accessibility via `kubectl port-forward` and `curl ... | jq .`.
+- Hook validation: Task 4 confirmed both Jobs completed and were deleted afterward.
+
+One limitation is specific to the local minikube environment: after the prod upgrade, the `LoadBalancer` service stayed at `EXTERNAL-IP `. That is expected on this cluster without an additional load-balancer implementation, so the authoritative accessibility check remained `kubectl port-forward` instead of a cloud-style public IP.