diff --git a/labs/lab10/k8s/HELM.md b/labs/lab10/k8s/HELM.md new file mode 100644 index 0000000000..d460b20a21 --- /dev/null +++ b/labs/lab10/k8s/HELM.md @@ -0,0 +1,341 @@ +# Lab 10 — Helm Package Manager + +## Chart Overview + +This lab converts the Kubernetes manifests from Lab 09 into a reusable Helm chart for the `devops-info-service` application. + +The Helm chart packages the Kubernetes Deployment and Service into reusable templates with configurable values for different environments. + +Chart location: + +labs/lab10/k8s/devops-info-service/ + + +Chart structure: + +devops-info-service/ +├── Chart.yaml +├── values.yaml +├── values-dev.yaml +├── values-prod.yaml +└── templates/ +├── deployment.yaml +├── service.yaml +├── _helpers.tpl +├── NOTES.txt +└── hooks/ +├── pre-install-job.yaml +└── post-install-job.yaml + + +### Template files + +**deployment.yaml** +Defines the Kubernetes Deployment for the FastAPI application. +Uses values for replicas, image, resources, probes, and security context. + +**service.yaml** +Defines the Kubernetes Service that exposes the application. +Service type and ports are configurable through values. + +**_helpers.tpl** +Contains reusable Helm helper templates for: +- resource naming +- labels +- selectors + +This avoids duplication and follows DRY principles. + +**values.yaml** +Default configuration for the chart. + +**values-dev.yaml** +Overrides for development environment. + +**values-prod.yaml** +Overrides for production environment. + +**hooks/** +Contains Helm lifecycle hook Jobs. + +--- + +## Configuration Guide + +The chart is configurable through Helm values. + +### Replica configuration + +replicaCount + +Controls the number of application Pods. + +### Image configuration + +image.repository +image.tag +image.pullPolicy + + +Defines the Docker image used for the Deployment. + +### Service configuration + +service.type +service.port +service.targetPort +service.nodePort + + +Controls how the application is exposed. + +### Resource configuration + +resources.requests +resources.limits + + +Defines CPU and memory allocation for the container. + +### Health checks + +livenessProbe +readinessProbe + + +Both probes use the `/health` endpoint of the FastAPI application. + +### Security configuration + +securityContext.runAsNonRoot +securityContext.runAsUser +securityContext.allowPrivilegeEscalation + + +Ensures the container runs as a non-root user. + +--- + +## Multi-Environment Configuration + +Two environment configurations were created. + +### Development environment (values-dev.yaml) +- replicaCount: 1 +- Service type: NodePort +- lower resource limits +- faster probe timings +- suitable for local kind cluster + +Install dev environment: + +helm install dev-release . -f values-dev.yaml + + +### Production environment (values-prod.yaml) +- replicaCount: 3 +- Service type: LoadBalancer +- higher resource limits +- production probe timings + +Upgrade to production configuration: + +helm upgrade dev-release devops-info-service -f values-prod.yaml + + +--- + +## Hook Implementation + +Two Helm hooks were implemented. + +### Pre-install Hook + +File: + +templates/hooks/pre-install-job.yaml + + +Purpose: +Runs a validation job before installing the application. + +Hook annotations: + +helm.sh/hook: pre-install +helm.sh/hook-weight: -5 +helm.sh/hook-delete-policy: hook-succeeded + + +### Post-install Hook + +File: + +templates/hooks/post-install-job.yaml + + +Purpose: +Runs a smoke test job after installation. + +Hook annotations: + +helm.sh/hook: post-install +helm.sh/hook-weight: 5 +helm.sh/hook-delete-policy: hook-succeeded + + +### Hook execution order + +1. Pre-install hook runs first +2. Kubernetes resources are installed +3. Post-install hook runs after installation +4. Hook Jobs are deleted after successful execution + +--- + +## Installation Evidence + +### Helm installation + +helm version + + +### Repository exploration + +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + +helm repo update +helm search repo prometheus +helm show chart prometheus-community/prometheus +helm show values prometheus-community/prometheus + + +### Chart validation + +helm lint . +helm template mychart . +helm install --dry-run --debug test-release . + + +### Install release + +helm install dev-release . -f values-dev.yaml + + +### Verify resources + +helm list +kubectl get pods +kubectl get svc +kubectl get deployment + + +### Application test + +kubectl port-forward service/dev-release-devops-info-service 8080:80 +curl http://localhost:8080 + +curl http://localhost:8080/health + + +Both endpoints returned successful responses. + +--- + +## Operations + +### Install + +helm install dev-release . -f values-dev.yaml + + +### Upgrade to production + +helm upgrade dev-release . -f values-prod.yaml + + +### Release history + +helm history dev-release + + +### Rollback + +helm rollback dev-release 1 + + +### Uninstall + +helm uninstall dev-release + + +--- + +## Testing & Validation + +The chart was validated using: + +### Lint + +helm lint . + + +### Template rendering + +helm template . + + +### Dry-run installation + +helm install --dry-run --debug test-release . + + +### Runtime validation + +kubectl get pods +kubectl get svc +kubectl port-forward +curl / +curl /health + + +All tests passed successfully. + +--- + +## Challenges & Solutions + +### ImagePullBackOff +The kind cluster had intermittent connectivity issues to Docker Hub. + +Solution: +Retried deployment and verified image availability. + +### Security Context Issue +Kubernetes could not verify non-root execution because the image used a named user. + +Solution: +Added numeric UID: + +runAsNonRoot: true +runAsUser: 1000 + + +### Default Helm Templates +The initial chart included unnecessary templates such as httproute and ingress. + +Solution: +Removed unused templates and kept only required resources. + +--- + +## What I Learned + +In this lab I learned: + +- how Helm packages Kubernetes applications into reusable charts +- how to convert static manifests into Helm templates +- how to use values.yaml for configuration +- how to manage multiple environments using values files +- how Helm hooks work +- how to install, upgrade, rollback, and uninstall Helm releases +- how Helm simplifies Kubernetes application management diff --git a/labs/lab10/k8s/devops-info-service/.helmignore b/labs/lab10/k8s/devops-info-service/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/.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/labs/lab10/k8s/devops-info-service/Chart.yaml b/labs/lab10/k8s/devops-info-service/Chart.yaml new file mode 100644 index 0000000000..ad4efa474e --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: devops-info-service +description: Helm chart for the DevOps course FastAPI info service +type: application +version: 0.1.0 +appVersion: "1.0.0" + +keywords: + - fastapi + - python + - kubernetes + - helm + +maintainers: + - name: Fayzullin + +sources: + - https://github.com/inno-devops-labs/DevOps-Core-Course diff --git a/labs/lab10/k8s/devops-info-service/templates/NOTES.txt b/labs/lab10/k8s/devops-info-service/templates/NOTES.txt new file mode 100644 index 0000000000..cfd6d4d3bb --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/NOTES.txt @@ -0,0 +1,10 @@ +1. Get the application URL by running these commands: + +{{- if eq .Values.service.type "NodePort" }} + kubectl port-forward service/{{ include "devops-info-service.fullname" . }} 8080:{{ .Values.service.port }} + curl http://127.0.0.1:8080 +{{- else if eq .Values.service.type "LoadBalancer" }} + kubectl get svc {{ include "devops-info-service.fullname" . }} +{{- else }} + kubectl port-forward service/{{ include "devops-info-service.fullname" . }} 8080:{{ .Values.service.port }} +{{- end }} diff --git a/labs/lab10/k8s/devops-info-service/templates/_helpers.tpl b/labs/lab10/k8s/devops-info-service/templates/_helpers.tpl new file mode 100644 index 0000000000..65e0dc68bc --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/_helpers.tpl @@ -0,0 +1,43 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "devops-info-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "devops-info-service.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Chart name and version +*/}} +{{- define "devops-info-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "devops-info-service.labels" -}} +helm.sh/chart: {{ include "devops-info-service.chart" . }} +{{ include "devops-info-service.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "devops-info-service.selectorLabels" -}} +app.kubernetes.io/name: {{ include "devops-info-service.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/labs/lab10/k8s/devops-info-service/templates/deployment.yaml b/labs/lab10/k8s/devops-info-service/templates/deployment.yaml new file mode 100644 index 0000000000..8c28093ddc --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "devops-info-service.fullname" . }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + {{- include "devops-info-service.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "devops-info-service.selectorLabels" . | nindent 8 }} + spec: + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - containerPort: {{ .Values.service.targetPort }} + protocol: TCP + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + httpGet: + path: {{ .Values.livenessProbe.httpGet.path }} + port: {{ .Values.livenessProbe.httpGet.port }} + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.readinessProbe.enabled }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.httpGet.path }} + port: {{ .Values.readinessProbe.httpGet.port }} + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + {{- end }} diff --git a/labs/lab10/k8s/devops-info-service/templates/hooks/post-install-job.yaml b/labs/lab10/k8s/devops-info-service/templates/hooks/post-install-job.yaml new file mode 100644 index 0000000000..85b4f09777 --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/hooks/post-install-job.yaml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ include "devops-info-service.fullname" . }}-post-install" + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + metadata: + name: "{{ include "devops-info-service.fullname" . }}-post-install" + spec: + restartPolicy: Never + containers: + - name: post-install-job + image: {{ .Values.hookJobs.image }} + imagePullPolicy: {{ .Values.hookJobs.pullPolicy }} + command: + - sh + - -c + - | + echo "Post-install smoke test started" + sleep 5 + echo "Post-install smoke test completed" diff --git a/labs/lab10/k8s/devops-info-service/templates/hooks/pre-install-job.yaml b/labs/lab10/k8s/devops-info-service/templates/hooks/pre-install-job.yaml new file mode 100644 index 0000000000..76695e8ed9 --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/hooks/pre-install-job.yaml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{ include "devops-info-service.fullname" . }}-pre-install" + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + metadata: + name: "{{ include "devops-info-service.fullname" . }}-pre-install" + spec: + restartPolicy: Never + containers: + - name: pre-install-job + image: {{ .Values.hookJobs.image }} + imagePullPolicy: {{ .Values.hookJobs.pullPolicy }} + command: + - sh + - -c + - | + echo "Pre-install validation started" + sleep 5 + echo "Pre-install validation completed" diff --git a/labs/lab10/k8s/devops-info-service/templates/service.yaml b/labs/lab10/k8s/devops-info-service/templates/service.yaml new file mode 100644 index 0000000000..ddb8e07756 --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "devops-info-service.fullname" . }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "devops-info-service.selectorLabels" . | nindent 4 }} + ports: + - protocol: TCP + port: {{ .Values.service.port }} + targetPort: {{ .Values.service.targetPort }} + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} diff --git a/labs/lab10/k8s/devops-info-service/values-dev.yaml b/labs/lab10/k8s/devops-info-service/values-dev.yaml new file mode 100644 index 0000000000..27a7731343 --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/values-dev.yaml @@ -0,0 +1,26 @@ +replicaCount: 1 + +image: + tag: "latest" + +service: + type: NodePort + port: 80 + targetPort: 5000 + nodePort: 30081 + +resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "100m" + memory: "128Mi" + +livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 10 + +readinessProbe: + initialDelaySeconds: 3 + periodSeconds: 5 diff --git a/labs/lab10/k8s/devops-info-service/values-prod.yaml b/labs/lab10/k8s/devops-info-service/values-prod.yaml new file mode 100644 index 0000000000..634150c7f3 --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/values-prod.yaml @@ -0,0 +1,25 @@ +replicaCount: 3 + +image: + tag: "latest" + +service: + type: LoadBalancer + port: 80 + targetPort: 5000 + +resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + +livenessProbe: + initialDelaySeconds: 20 + periodSeconds: 5 + +readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 5 diff --git a/labs/lab10/k8s/devops-info-service/values.yaml b/labs/lab10/k8s/devops-info-service/values.yaml new file mode 100644 index 0000000000..544a71398a --- /dev/null +++ b/labs/lab10/k8s/devops-info-service/values.yaml @@ -0,0 +1,50 @@ +replicaCount: 3 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: fayzullin/devops-info-service + tag: "latest" + pullPolicy: Always + +service: + type: NodePort + port: 80 + targetPort: 5000 + nodePort: 30080 + +resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "200m" + memory: "256Mi" + +livenessProbe: + enabled: true + httpGet: + path: /health + port: 5000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + +readinessProbe: + enabled: true + httpGet: + path: /health + port: 5000 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + +hookJobs: + image: busybox + pullPolicy: IfNotPresent + +podSecurityContext: {} +