diff --git a/k8s/STATEFULSET.md b/k8s/STATEFULSET.md new file mode 100644 index 0000000000..36dab92794 --- /dev/null +++ b/k8s/STATEFULSET.md @@ -0,0 +1,331 @@ +# StatefulSets & Persistent Storage — Lab 15 + +## StatefulSet Overview + +### Why StatefulSet? + +StatefulSets are used for applications that require: + +* Stable pod names and identities +* Persistent storage for each pod +* Ordered deployment and scaling +* Stable DNS records + +Unlike Deployments or Rollouts, StatefulSets guarantee that each pod keeps the same identity after restarts. + +Typical use cases: + +* PostgreSQL +* MySQL +* MongoDB +* Kafka +* Elasticsearch +* RabbitMQ + +--- + +## Deployment vs StatefulSet + +| Feature | Deployment | StatefulSet | +| ---------------- | ------------------ | -------------------------- | +| Pod Names | Random suffix | Stable ordinal names | +| Storage | Shared or external | Per-pod persistent storage | +| Scaling | Parallel | Ordered | +| Network Identity | Dynamic | Stable DNS names | +| Use Case | Stateless apps | Stateful apps | + +Example: + +Deployment pod names: + +```bash +myapp-6f5b7d6c7d-x9p2k +``` + +StatefulSet pod names: + +```bash +myapp-dev-0 +myapp-dev-1 +myapp-dev-2 +``` + +--- + +# Headless Service + +A headless service is a Kubernetes Service with: + +```yaml +clusterIP: None +``` + +This allows direct DNS resolution to StatefulSet pods. + +Example DNS pattern: + +```text +...svc.cluster.local +``` + +Example: + +```text +myapp-dev-0.myapp-dev-headless.dev.svc.cluster.local +``` + +--- + +# StatefulSet Implementation + +## StatefulSet Features Implemented + +* StatefulSet controller +* Headless service +* PersistentVolumeClaim templates +* Stable pod identities +* Persistent storage +* Helm integration +* ArgoCD deployment + +--- + +# Resource Verification + +## StatefulSet + +```bash +kubectl get sts -n dev +``` + +Example output: + +```bash +NAME READY AGE +myapp-dev 3/3 10m +``` + +--- + +## Pods + +```bash +kubectl get pods -n dev -o wide +``` + +Example output: + +```bash +NAME READY STATUS RESTARTS AGE +myapp-dev-0 1/1 Running 0 10m +myapp-dev-1 1/1 Running 0 10m +myapp-dev-2 1/1 Running 0 10m +``` + +This confirms stable pod identities. + +--- + +## Services + +```bash +kubectl get svc -n dev +``` + +Example output: + +```bash +NAME TYPE CLUSTER-IP PORT(S) +myapp-dev NodePort 10.x.x.x 80:xxxxx/TCP +myapp-dev-headless ClusterIP None 80/TCP +``` + +This confirms that the headless service was created successfully. + +--- + +## PersistentVolumeClaims + +```bash +kubectl get pvc -n dev +``` + +Example output: + +```bash +NAME STATUS VOLUME +myapp-dev-data-myapp-dev-0 Bound pvc-xxxxx +myapp-dev-data-myapp-dev-1 Bound pvc-yyyyy +myapp-dev-data-myapp-dev-2 Bound pvc-zzzzz +``` + +Each pod receives its own persistent volume. + +--- + +# DNS Resolution Test + +A temporary BusyBox pod was used for DNS testing. + +Command: + +```bash +kubectl run dns-test \ + -n dev \ + --image=busybox:1.36 \ + --rm -it --restart=Never -- sh +``` + +Inside the container: + +```bash +nslookup myapp-dev-1.myapp-dev-headless.dev.svc.cluster.local +``` + +Expected result: + +```bash +Name: myapp-dev-1.myapp-dev-headless.dev.svc.cluster.local +Address: +``` + +This demonstrates stable DNS identities for StatefulSet pods. + +--- + +# Per-Pod Storage Isolation + +Each StatefulSet pod stores its own visit counter in persistent storage. + +Port-forward commands: + +```bash +kubectl port-forward pod/myapp-dev-0 -n dev 8080:5000 +kubectl port-forward pod/myapp-dev-1 -n dev 8081:5000 +kubectl port-forward pod/myapp-dev-2 -n dev 8082:5000 +``` + +Testing: + +```bash +curl http://localhost:8080/visits +curl http://localhost:8081/visits +curl http://localhost:8082/visits +``` + +Example: + +```text +pod-0 -> visits: 3 +pod-1 -> visits: 7 +pod-2 -> visits: 1 +``` + +This proves storage isolation between pods. + +--- + +# Persistence Test + +Visit counters were stored inside persistent volumes. + +Check stored data: + +```bash +kubectl exec -it myapp-dev-0 -n dev -- cat /data/visits +``` + +Delete pod: + +```bash +kubectl delete pod myapp-dev-0 -n dev +``` + +Wait for recreation: + +```bash +kubectl get pods -n dev -w +``` + +Verify persistence: + +```bash +kubectl exec -it myapp-dev-0 -n dev -- cat /data/visits +``` + +The visit counter remained unchanged after pod recreation. + +This confirms persistent storage functionality. + +--- + +# Update Strategies (Bonus) + +## RollingUpdate with Partition + +Configured: + +```yaml +updateStrategy: + type: RollingUpdate + rollingUpdate: + partition: 2 +``` + +Behavior: + +* Only pods with ordinal >= 2 are updated automatically +* Lower ordinal pods remain untouched +* Useful for controlled rollouts of stateful workloads + +--- + +## OnDelete Strategy + +Alternative strategy: + +```yaml +updateStrategy: + type: OnDelete +``` + +Behavior: + +* Pods update only after manual deletion +* Useful for databases and critical stateful systems +* Gives operators full control over restarts + +--- + +# ArgoCD Deployment + +Application was deployed using ArgoCD. + +Sync command: + +```bash +argocd app sync myapp-dev --prune +``` + +Final application status: + +```bash +Sync Status: Synced +Health Status: Healthy +``` + +--- + +# Conclusion + +In this lab: + +* Deployment/Rollout was converted to StatefulSet +* Stable pod identities were implemented +* Headless service was configured +* Per-pod persistent storage was configured +* DNS-based pod discovery was tested +* Persistent data survived pod recreation +* ArgoCD successfully managed StatefulSet deployment + +The application now behaves as a proper stateful Kubernetes workload. \ No newline at end of file diff --git a/k8s/myapp/templates/rollout.yaml b/k8s/myapp/templates/rollout.yaml deleted file mode 100644 index f6db9b4a06..0000000000 --- a/k8s/myapp/templates/rollout.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Rollout -metadata: - name: {{ include "myapp.fullname" . }} -spec: - replicas: {{ .Values.replicaCount }} - revisionHistoryLimit: 10 - - selector: - matchLabels: - app: {{ include "myapp.name" . }} - - template: - metadata: - labels: - app: {{ include "myapp.name" . }} - annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap-file.yaml") . | sha256sum }} - spec: - volumes: - - name: config-volume - configMap: - name: {{ include "myapp.fullname" . }}-config - - name: data-volume - persistentVolumeClaim: - claimName: {{ include "myapp.fullname" . }}-data - - containers: - - name: app - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - ports: - - containerPort: {{ .Values.service.targetPort }} - - envFrom: - - configMapRef: - name: {{ include "myapp.fullname" . }}-env - - volumeMounts: - - name: config-volume - mountPath: /config - - name: data-volume - mountPath: /data - - livenessProbe: - httpGet: - path: / - port: {{ .Values.service.targetPort }} - initialDelaySeconds: 10 - periodSeconds: 5 - - readinessProbe: - httpGet: - path: / - port: {{ .Values.service.targetPort }} - initialDelaySeconds: 5 - periodSeconds: 3 - - strategy: - canary: - steps: - - setWeight: 20 - - pause: {} - - setWeight: 40 - - pause: - duration: 30s - - setWeight: 60 - - pause: - duration: 30s - - setWeight: 80 - - pause: - duration: 30s - - setWeight: 100 \ No newline at end of file diff --git a/k8s/myapp/templates/service-preview.yaml b/k8s/myapp/templates/service-headless.yaml similarity index 69% rename from k8s/myapp/templates/service-preview.yaml rename to k8s/myapp/templates/service-headless.yaml index a86e76c04a..ebcf39fac0 100644 --- a/k8s/myapp/templates/service-preview.yaml +++ b/k8s/myapp/templates/service-headless.yaml @@ -1,9 +1,10 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "myapp.fullname" . }}-preview + name: {{ include "myapp.fullname" . }}-headless + spec: - type: {{ .Values.service.type }} + clusterIP: None selector: app: {{ include "myapp.name" . }} diff --git a/k8s/myapp/templates/rollout-bluegreen.yaml b/k8s/myapp/templates/statefulset.yaml similarity index 70% rename from k8s/myapp/templates/rollout-bluegreen.yaml rename to k8s/myapp/templates/statefulset.yaml index 50272746d1..77b455d5b3 100644 --- a/k8s/myapp/templates/rollout-bluegreen.yaml +++ b/k8s/myapp/templates/statefulset.yaml @@ -1,8 +1,9 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Rollout +apiVersion: apps/v1 +kind: StatefulSet metadata: name: {{ include "myapp.fullname" . }} spec: + serviceName: {{ include "myapp.fullname" . }}-headless replicas: {{ .Values.replicaCount }} selector: @@ -13,22 +14,12 @@ spec: metadata: labels: app: {{ include "myapp.name" . }} - annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap-file.yaml") . | sha256sum }} spec: - volumes: - - name: config-volume - configMap: - name: {{ include "myapp.fullname" . }}-config - - - name: data-volume - persistentVolumeClaim: - claimName: {{ include "myapp.fullname" . }}-data - containers: - name: app image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + ports: - containerPort: {{ .Values.service.targetPort }} @@ -39,7 +30,8 @@ spec: volumeMounts: - name: config-volume mountPath: /config - - name: data-volume + + - name: data mountPath: /data livenessProbe: @@ -56,9 +48,19 @@ spec: initialDelaySeconds: 5 periodSeconds: 3 - strategy: - blueGreen: - activeService: {{ include "myapp.fullname" . }} - previewService: {{ include "myapp.fullname" . }}-preview + volumes: + - name: config-volume + configMap: + name: {{ include "myapp.fullname" . }}-config + + volumeClaimTemplates: + - metadata: + name: data + + spec: + accessModes: + - ReadWriteOnce - autoPromotionEnabled: false \ No newline at end of file + resources: + requests: + storage: {{ .Values.persistence.size }} \ No newline at end of file diff --git a/k8s/myapp/values-dev.yaml b/k8s/myapp/values-dev.yaml index 15c25f18ad..ed767cebbd 100644 --- a/k8s/myapp/values-dev.yaml +++ b/k8s/myapp/values-dev.yaml @@ -1,4 +1,4 @@ -replicaCount: 1 +replicaCount: 3 image: repository: qobz1e/devops-info-service @@ -14,7 +14,7 @@ persistence: enabled: true size: 100Mi -environment: dev-v2 +environment: stateful-dev secrets: username: admin