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: {} + diff --git a/labs/lab11/k8s/SECRETS.md b/labs/lab11/k8s/SECRETS.md new file mode 100644 index 0000000000..60306f19ee --- /dev/null +++ b/labs/lab11/k8s/SECRETS.md @@ -0,0 +1,327 @@ +# Lab 11 — Kubernetes Secrets & HashiCorp Vault + +## 1. Kubernetes Secrets + +### Secret creation with kubectl + +A Kubernetes Secret named `app-credentials` was created using the imperative command: + +```bash +kubectl create secret generic app-credentials \ + --from-literal=username=devuser \ + --from-literal=password=devpass123 +``` + +### Viewing the secret + +```bash +kubectl get secret app-credentials -o yaml +``` + +Example output: + +apiVersion: v1 +data: + password: ZGV2cGFzczEyMw== + username: ZGV2dXNlcg== +kind: Secret +metadata: + name: app-credentials +type: Opaque + +### Decoding the values + +```bash +echo "ZGV2dXNlcg==" | base64 -d +echo +echo "ZGV2cGFzczEyMw==" | base64 -d +echo +``` + +Decoded values: + +username = devuser +password = devpass123 + +### Base64 encoding vs encryption + +Kubernetes Secrets store values in base64-encoded format. +Base64 is only encoding, not encryption. + +This means: + +anyone with access to the Secret object can decode the values +Secrets are not automatically strongly protected just because they are stored as Secret resources +Security implications + +For production environments: + +RBAC should restrict access to Secrets +encryption at rest should be enabled for etcd +external secret managers such as Vault are recommended for stronger security + +## 2. Helm Secret Integration + +### Chart structure + +The Helm chart was extended with secret management: + +labs/lab11/k8s/devops-info-service/ +├── Chart.yaml +├── values.yaml +├── values-dev.yaml +├── values-prod.yaml +└── templates/ + ├── deployment.yaml + ├── service.yaml + ├── secrets.yaml + ├── serviceaccount.yaml + ├── _helpers.tpl + ├── NOTES.txt + └── hooks/ + +### Secret template + +A new template file was added: + +templates/secrets.yaml + +This template creates a Kubernetes Secret using values from Helm configuration. + +Secret values + +Secret values are defined in: + +values.yaml with placeholder defaults +values-dev.yaml with development values +values-prod.yaml with placeholder production values +Secret consumption in Deployment + +The Deployment consumes the Secret using: + +envFrom: + - secretRef: + name: + + +### Verification inside the pod + +The Helm release was installed: + +```bash +helm install secrets-release . -f values-dev.yaml +``` + +The created Secret: + +```bash +kubectl get secrets +``` + +Example output included: + +app-credentials +secrets-release-devops-info-service-secret + +Environment variables inside the pod were verified with: + +```bash +kubectl exec -it secrets-release-devops-info-service-7b8848dbcd-7vrb2 -- env | grep -i -E 'username|password' +``` + +Output: + +password=devpass123 +username=devuser + +### Pod description verification + +```bash +kubectl describe pod secrets-release-devops-info-service-7b8848dbcd-7vrb2 +``` + +The pod description showed: + +Environment Variables from: + secrets-release-devops-info-service-secret Secret Optional: false + +The actual secret values were not shown in kubectl describe pod, only the reference to the Secret. + +## 3. Resource Management + +### Configured resources + +The Deployment includes configurable CPU and memory requests/limits. + +Example configuration: + +resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "100m" + memory: "128Mi" + +### Requests vs limits + +Requests define the minimum amount of CPU and memory required for scheduling +Limits define the maximum amount of CPU and memory the container is allowed to use + +### Choosing values + +For this lab: + +lower values were used in development for local cluster efficiency +the chart still follows Kubernetes resource management best practices +values remain configurable through Helm + +## 4. Vault Integration + +### Vault installation + +Vault was installed using Helm: + +```bash +helm repo add hashicorp https://helm.releases.hashicorp.com +helm repo update + +helm install vault hashicorp/vault \ + --set "server.dev.enabled=true" \ + --set "injector.enabled=true" +``` + +### Vault installation verification + +```bash +kubectl get pods +``` + +Relevant running resources: + +vault-0 +vault-agent-injector-... + +### Secret creation in Vault + +Inside the Vault pod, a secret was written to the KV path: + +```bash +vault kv put secret/devops-info-service/config username="vaultuser" password="vaultpass123" +``` + +### Kubernetes auth configuration + +Vault Kubernetes authentication was configured with: + +Kubernetes auth method +policy devops-info-service +role devops-info-service + +The role was bound to the application service account: + +vault-release-devops-info-service +Vault Agent injection + +Vault annotations were enabled in the Deployment template. + +Verified annotations from the pod: + +vault.hashicorp.com/agent-inject: true +vault.hashicorp.com/agent-inject-secret-config: secret/data/devops-info-service/config +vault.hashicorp.com/agent-inject-status: injected +vault.hashicorp.com/role: devops-info-service + + +### Sidecar injection pattern + +The injected pod contained: + +init container: vault-agent-init +application container: devops-info-service +sidecar container: vault-agent + +This demonstrates the Vault Agent sidecar injection pattern: + +the init container prepares authentication and secret rendering +the sidecar agent keeps Vault integration active +the application reads secrets from files mounted into the pod + +### Proof of secret injection + +Secrets were verified inside the pod: + +```bash +kubectl exec -it vault-release-devops-info-service-64db8d7688-xlnhp -c devops-info-service -- ls -R /vault/secrets +kubectl exec -it vault-release-devops-info-service-64db8d7688-xlnhp -c devops-info-service -- cat /vault/secrets +``` + +config + +Output: + +/vault/secrets: +config + +Rendered content: + +data: map[password:vaultpass123 username:vaultuser] +metadata: map[created_time:2026-04-09T08:46:08.785957176Z custom_metadata: deletion_time: destroyed:false version:1] + +This confirms that Vault successfully injected the application secret into the pod filesystem. + +## 5. Security Analysis + +### Kubernetes Secrets + +Advantages + +built into Kubernetes +easy to create and use +simple integration with pods via env vars or mounted volumes + +Disadvantages + +values are only base64-encoded +not strongly protected without etcd encryption at rest +secret lifecycle management is limited +not ideal for larger production environments +HashiCorp Vault + +Advantages + +centralized secret management +policy-based access control +Kubernetes authentication support +sidecar injection pattern for secret delivery +better production-oriented security model + +Disadvantages + +more complex to install and configure +additional operational overhead +requires extra components and maintenance +When to use each approach + +Use Kubernetes Secrets when: + +the application is simple +the environment is small +native Kubernetes integration is sufficient + +Use Vault when: + +stronger security controls are needed +multiple applications need centralized secret management +policy-based access control is required +production-grade secret handling is needed +Production recommendations + +For production environments: + +never commit real secrets to Git +use placeholder values in Helm files +enable etcd encryption at rest +restrict access to Secrets with RBAC +prefer Vault or another external secret manager for sensitive workloads +avoid using the default service account for Vault-authenticated workloads diff --git a/labs/lab11/k8s/devops-info-service/.helmignore b/labs/lab11/k8s/devops-info-service/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/Chart.yaml b/labs/lab11/k8s/devops-info-service/Chart.yaml new file mode 100644 index 0000000000..ad4efa474e --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/templates/NOTES.txt b/labs/lab11/k8s/devops-info-service/templates/NOTES.txt new file mode 100644 index 0000000000..cfd6d4d3bb --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/templates/_helpers.tpl b/labs/lab11/k8s/devops-info-service/templates/_helpers.tpl new file mode 100644 index 0000000000..4fbef766cc --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/templates/_helpers.tpl @@ -0,0 +1,54 @@ +{{/* +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 }} + +{{/* +Service account name +*/}} +{{- define "devops-info-service.serviceAccountName" -}} +{{- if .Values.serviceAccount.name }} +{{- .Values.serviceAccount.name }} +{{- else }} +{{- include "devops-info-service.fullname" . }} +{{- end }} +{{- end }} diff --git a/labs/lab11/k8s/devops-info-service/templates/deployment.yaml b/labs/lab11/k8s/devops-info-service/templates/deployment.yaml new file mode 100644 index 0000000000..c16125d672 --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/templates/deployment.yaml @@ -0,0 +1,66 @@ +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 }} + {{- if .Values.vault.enabled }} + annotations: + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/role: {{ .Values.vault.role | quote }} + vault.hashicorp.com/agent-inject-secret-{{ .Values.vault.injectFileName }}: {{ .Values.vault.secretPath | quote }} + {{- end }} + spec: + serviceAccountName: {{ include "devops-info-service.serviceAccountName" . }} + 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.secret.enabled }} + envFrom: + - secretRef: + name: {{ include "devops-info-service.fullname" . }}-{{ .Values.secret.nameSuffix }} + {{- end }} + {{- 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/lab11/k8s/devops-info-service/templates/hooks/post-install-job.yaml b/labs/lab11/k8s/devops-info-service/templates/hooks/post-install-job.yaml new file mode 100644 index 0000000000..85b4f09777 --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/templates/hooks/pre-install-job.yaml b/labs/lab11/k8s/devops-info-service/templates/hooks/pre-install-job.yaml new file mode 100644 index 0000000000..76695e8ed9 --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/templates/secrets.yaml b/labs/lab11/k8s/devops-info-service/templates/secrets.yaml new file mode 100644 index 0000000000..8bf4e1d833 --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- if .Values.secret.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "devops-info-service.fullname" . }}-{{ .Values.secret.nameSuffix }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +type: Opaque +stringData: + username: {{ .Values.secret.username | quote }} + password: {{ .Values.secret.password | quote }} +{{- end }} diff --git a/labs/lab11/k8s/devops-info-service/templates/service.yaml b/labs/lab11/k8s/devops-info-service/templates/service.yaml new file mode 100644 index 0000000000..ddb8e07756 --- /dev/null +++ b/labs/lab11/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/lab11/k8s/devops-info-service/templates/serviceaccount.yaml b/labs/lab11/k8s/devops-info-service/templates/serviceaccount.yaml new file mode 100644 index 0000000000..10279cd57d --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "devops-info-service.serviceAccountName" . }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +{{- end }} diff --git a/labs/lab11/k8s/devops-info-service/values-dev.yaml b/labs/lab11/k8s/devops-info-service/values-dev.yaml new file mode 100644 index 0000000000..e58a79851a --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/values-dev.yaml @@ -0,0 +1,36 @@ +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 + +secret: + username: "devuser" + password: "devpass123" + +vault: + enabled: true + role: "devops-info-service" + secretPath: "secret/data/devops-info-service/config" + injectFileName: "config" diff --git a/labs/lab11/k8s/devops-info-service/values-prod.yaml b/labs/lab11/k8s/devops-info-service/values-prod.yaml new file mode 100644 index 0000000000..d0893ff843 --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/values-prod.yaml @@ -0,0 +1,32 @@ +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 + +secret: + username: "prod-placeholder-user" + password: "prod-placeholder-password" + +vault: + enabled: false diff --git a/labs/lab11/k8s/devops-info-service/values.yaml b/labs/lab11/k8s/devops-info-service/values.yaml new file mode 100644 index 0000000000..b3defdeb4d --- /dev/null +++ b/labs/lab11/k8s/devops-info-service/values.yaml @@ -0,0 +1,65 @@ +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: {} + +secret: + enabled: true + nameSuffix: secret + username: "placeholder-user" + password: "placeholder-password" + +serviceAccount: + create: true + name: "" + +vault: + enabled: false + role: "devops-info-service" + secretPath: "secret/data/devops-info-service/config" + injectFileName: "config" diff --git a/labs/lab12/k8s/CONFIGMAPS.md b/labs/lab12/k8s/CONFIGMAPS.md new file mode 100644 index 0000000000..85f9e41fee --- /dev/null +++ b/labs/lab12/k8s/CONFIGMAPS.md @@ -0,0 +1,126 @@ +# Lab 12 — ConfigMaps & Persistent Volumes + +## 1. Application Changes + +The application was updated to support a persistent visits counter. + +### Changes: +- A visits counter is stored in `/data/visits` +- Each request to `/` increments the counter +- A new endpoint `/visits` returns the current counter +- The file is read on startup (default = 0 if not exists) + +### Endpoints: +- `GET /` — increments visits counter +- `GET /visits` — returns current visits count + +### Local Docker Test + +```bash +docker run --rm -p 5000:5000 -v "$(pwd)/data:/data" fayzullin/devops-info-service:latest +``` + +### Verification: + +Counter increased after requests +Value persisted after container restart + +## 2. ConfigMap Implementation +File-based ConfigMap + +Config file: + +{ + "appName": "devops-info-service", + "environment": "dev", + "featureFlag": true +} + +Mounted inside pod: + +```bash +kubectl exec -it -- cat /config/config.json +``` + +Output: + +{ + "appName": "devops-info-service", + "environment": "dev", + "featureFlag": true +} +Environment Variables via ConfigMap +kubectl exec -it -- printenv | grep APP_ +kubectl exec -it -- printenv | grep LOG_LEVEL + +Output: + +APP_ENV=dev +LOG_LEVEL=debug + +## 3. Persistent Volume + +### PVC + +```bash +kubectl get pvc +``` + +Output: + +lab12-release-devops-info-service-data Bound 100Mi RWO +Volume Mount + +PVC is mounted at: + +/data +Persistence Verification + +Before pod restart: + +cat /data/visits + +Output: + +2 + +After pod recreation: + +kubectl delete pod +kubectl get pods +kubectl exec -it -- cat /data/visits + +Output: + +2 + +After new request: + +curl localhost:8080/visits + +Output: + +{"visits":3} + +✅ Data persisted across pod restart + +## 4. ConfigMap vs Secret +ConfigMap Secret +Non-sensitive data Sensitive data +App config Passwords, tokens +Plain text Base64 encoded +Example: APP_ENV Example: DB_PASSWORD + +## 5. Summary + +In this lab: + +ConfigMaps were used for configuration (file + env vars) +PersistentVolumeClaim was used for data storage +Application data persisted across pod restarts +/visits endpoint confirmed correct behavior + +The application is now production-ready with: + +externalized configuration +persistent storage diff --git a/labs/lab12/k8s/devops-info-service/.helmignore b/labs/lab12/k8s/devops-info-service/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/labs/lab12/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/lab12/k8s/devops-info-service/Chart.yaml b/labs/lab12/k8s/devops-info-service/Chart.yaml new file mode 100644 index 0000000000..ad4efa474e --- /dev/null +++ b/labs/lab12/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/lab12/k8s/devops-info-service/files/config.json b/labs/lab12/k8s/devops-info-service/files/config.json new file mode 100644 index 0000000000..6c5b067a26 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/files/config.json @@ -0,0 +1,5 @@ +{ + "appName": "devops-info-service", + "environment": "dev", + "featureFlag": true +} diff --git a/labs/lab12/k8s/devops-info-service/templates/NOTES.txt b/labs/lab12/k8s/devops-info-service/templates/NOTES.txt new file mode 100644 index 0000000000..cfd6d4d3bb --- /dev/null +++ b/labs/lab12/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/lab12/k8s/devops-info-service/templates/_helpers.tpl b/labs/lab12/k8s/devops-info-service/templates/_helpers.tpl new file mode 100644 index 0000000000..4fbef766cc --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/_helpers.tpl @@ -0,0 +1,54 @@ +{{/* +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 }} + +{{/* +Service account name +*/}} +{{- define "devops-info-service.serviceAccountName" -}} +{{- if .Values.serviceAccount.name }} +{{- .Values.serviceAccount.name }} +{{- else }} +{{- include "devops-info-service.fullname" . }} +{{- end }} +{{- end }} diff --git a/labs/lab12/k8s/devops-info-service/templates/configmap-env.yaml b/labs/lab12/k8s/devops-info-service/templates/configmap-env.yaml new file mode 100644 index 0000000000..6d2e9a8e42 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/configmap-env.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "devops-info-service.fullname" . }}-env + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +data: + APP_ENV: {{ .Values.environment | quote }} + LOG_LEVEL: {{ .Values.logLevel | quote }} diff --git a/labs/lab12/k8s/devops-info-service/templates/configmap.yaml b/labs/lab12/k8s/devops-info-service/templates/configmap.yaml new file mode 100644 index 0000000000..03dbfb3109 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "devops-info-service.fullname" . }}-config + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +data: + config.json: |- +{{ .Files.Get "files/config.json" | indent 4 }} diff --git a/labs/lab12/k8s/devops-info-service/templates/deployment.yaml b/labs/lab12/k8s/devops-info-service/templates/deployment.yaml new file mode 100644 index 0000000000..a8b777aa1e --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/deployment.yaml @@ -0,0 +1,80 @@ +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 }} + {{- if .Values.vault.enabled }} + annotations: + vault.hashicorp.com/agent-inject: "true" + vault.hashicorp.com/role: {{ .Values.vault.role | quote }} + vault.hashicorp.com/agent-inject-secret-{{ .Values.vault.injectFileName }}: {{ .Values.vault.secretPath | quote }} + {{- end }} + spec: + serviceAccountName: {{ include "devops-info-service.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + volumes: + - name: config-volume + configMap: + name: {{ include "devops-info-service.fullname" . }}-config + - name: data-volume + persistentVolumeClaim: + claimName: {{ include "devops-info-service.fullname" . }}-data + 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 }} + envFrom: + - configMapRef: + name: {{ include "devops-info-service.fullname" . }}-env + {{- if .Values.secret.enabled }} + - secretRef: + name: {{ include "devops-info-service.fullname" . }}-{{ .Values.secret.nameSuffix }} + {{- end }} + volumeMounts: + - name: config-volume + mountPath: /config + - name: data-volume + mountPath: /data + {{- 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/lab12/k8s/devops-info-service/templates/hooks/post-install-job.yaml b/labs/lab12/k8s/devops-info-service/templates/hooks/post-install-job.yaml new file mode 100644 index 0000000000..85b4f09777 --- /dev/null +++ b/labs/lab12/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/lab12/k8s/devops-info-service/templates/hooks/pre-install-job.yaml b/labs/lab12/k8s/devops-info-service/templates/hooks/pre-install-job.yaml new file mode 100644 index 0000000000..76695e8ed9 --- /dev/null +++ b/labs/lab12/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/lab12/k8s/devops-info-service/templates/pvc.yaml b/labs/lab12/k8s/devops-info-service/templates/pvc.yaml new file mode 100644 index 0000000000..862717f000 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "devops-info-service.fullname" . }}-data + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass | quote }} + {{- end }} +{{- end }} diff --git a/labs/lab12/k8s/devops-info-service/templates/secrets.yaml b/labs/lab12/k8s/devops-info-service/templates/secrets.yaml new file mode 100644 index 0000000000..8bf4e1d833 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- if .Values.secret.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "devops-info-service.fullname" . }}-{{ .Values.secret.nameSuffix }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +type: Opaque +stringData: + username: {{ .Values.secret.username | quote }} + password: {{ .Values.secret.password | quote }} +{{- end }} diff --git a/labs/lab12/k8s/devops-info-service/templates/service.yaml b/labs/lab12/k8s/devops-info-service/templates/service.yaml new file mode 100644 index 0000000000..c855680e79 --- /dev/null +++ b/labs/lab12/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 .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} diff --git a/labs/lab12/k8s/devops-info-service/templates/serviceaccount.yaml b/labs/lab12/k8s/devops-info-service/templates/serviceaccount.yaml new file mode 100644 index 0000000000..10279cd57d --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "devops-info-service.serviceAccountName" . }} + labels: + {{- include "devops-info-service.labels" . | nindent 4 }} +{{- end }} diff --git a/labs/lab12/k8s/devops-info-service/values-dev.yaml b/labs/lab12/k8s/devops-info-service/values-dev.yaml new file mode 100644 index 0000000000..9bbb909d71 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/values-dev.yaml @@ -0,0 +1,38 @@ +replicaCount: 1 + +image: + tag: "latest" + +service: + type: NodePort + port: 80 + targetPort: 5000 + +resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "100m" + memory: "128Mi" + +livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 10 + +readinessProbe: + initialDelaySeconds: 3 + periodSeconds: 5 + +secret: + username: "devuser" + password: "devpass123" + +vault: + enabled: true + role: "devops-info-service" + secretPath: "secret/data/devops-info-service/config" + injectFileName: "config" + +environment: "dev" +logLevel: "debug" diff --git a/labs/lab12/k8s/devops-info-service/values-prod.yaml b/labs/lab12/k8s/devops-info-service/values-prod.yaml new file mode 100644 index 0000000000..dd487179c9 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/values-prod.yaml @@ -0,0 +1,35 @@ +replicaCount: 3 + +image: + tag: "latest" + +service: + type: ClusterIP + 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 + +secret: + username: "prod-placeholder-user" + password: "prod-placeholder-password" + +vault: + enabled: false + +environment: "prod" +logLevel: "info" diff --git a/labs/lab12/k8s/devops-info-service/values.yaml b/labs/lab12/k8s/devops-info-service/values.yaml new file mode 100644 index 0000000000..ff1393f5d6 --- /dev/null +++ b/labs/lab12/k8s/devops-info-service/values.yaml @@ -0,0 +1,73 @@ +replicaCount: 3 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: fayzullin/devops-info-service + tag: "latest" + pullPolicy: IfNotPresent + +service: + type: NodePort + port: 80 + targetPort: 5000 + nodePort: null + +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: {} + +secret: + enabled: true + nameSuffix: secret + username: "placeholder-user" + password: "placeholder-password" + +serviceAccount: + create: true + name: "" + +vault: + enabled: false + role: "devops-info-service" + secretPath: "secret/data/devops-info-service/config" + injectFileName: "config" + +environment: "dev" +logLevel: "info" + +persistence: + enabled: true + size: 100Mi + storageClass: "" diff --git a/labs/lab13/k8s/ARGOCD.md b/labs/lab13/k8s/ARGOCD.md new file mode 100644 index 0000000000..6ae5ecd6cb --- /dev/null +++ b/labs/lab13/k8s/ARGOCD.md @@ -0,0 +1,320 @@ +# Lab 13 — GitOps with ArgoCD + +## 1. ArgoCD Setup + +ArgoCD was installed into a dedicated namespace using Helm. + +Commands used: + +```bash +kubectl create namespace argocd +helm install argocd argo/argo-cd -n argocd \ + --set redis.image.repository=docker.io/library/redis \ + --set redis.image.tag=8.2.3-alpine +``` + +Verification: + +```bash +kubectl get pods -n argocd +``` + +All ArgoCD components were running, including: + +argocd-server +argocd-repo-server +argocd-application-controller +argocd-redis +argocd-applicationset-controller + +The UI was accessed using port-forward: + +```bash +kubectl port-forward svc/argocd-server -n argocd 8081:443 +``` + +URL: + +https://localhost:8081 + +Username: + +admin + +The initial password was retrieved with: + +```bash +kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath="{.data.password}" | base64 -d +``` + +### ArgoCD CLI + +The ArgoCD CLI was installed: + +```bash +curl -sSL -o argocd \ +https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 + +chmod +x argocd +sudo mv argocd /usr/local/bin/ +``` + +Login via CLI: + +argocd login localhost:8083 --insecure + +Verification: + +argocd app list +argocd app get devops-info-app-dev + +## 2. Application Configuration + +ArgoCD Application manifests were created in: + +labs/lab13/k8s/argocd/ + +Files: + +application.yaml +application-dev.yaml +application-prod.yaml + +The application source points to the GitHub repository: + +https://github.com/fayz131/DevOps-Core-Course.git + +Target revision: + +lab13 + +Helm chart path: + +labs/lab12/k8s/devops-info-service + +The Application deploys the Helm chart from Git into Kubernetes, following the GitOps model. + +## 3. Initial Application Deployment + +The base application was applied using: + +kubectl apply -f labs/lab13/k8s/argocd/application.yaml + +ArgoCD detected the application and synced it to the cluster. + +Verification: + +kubectl get applications -n argocd + +Example output: + +NAME SYNC STATUS HEALTH STATUS +devops-info-app Synced Healthy + +Application resources were created in the default namespace. + +## 4. Multi-Environment Deployment + +Two additional ArgoCD Applications were created: + +devops-info-app-dev +devops-info-app-prod +Dev environment + +Namespace: + +dev + +Values file: + +values-dev.yaml + +Sync policy: + +automated: + prune: true + selfHeal: true + +This means dev automatically syncs changes from Git and self-heals manual drift. + +### Prod environment + +Namespace: + +prod + +Values file: + +values-prod.yaml + +Sync policy: + +manual + +Production remains manual to allow controlled releases and review before deployment. + +Verification: + +```bash +kubectl get pods -n dev +kubectl get pods -n prod +kubectl get applications -n argocd +``` + + +Output: + +devops-info-app-dev Synced Healthy +devops-info-app-prod Synced Progressing + +Prod pods were running: + +devops-info-app-prod-devops-info-service-... 1/1 Running +devops-info-app-prod-devops-info-service-... 1/1 Running +devops-info-app-prod-devops-info-service-... 1/1 Running + +## 5. GitOps Workflow + +The Helm chart is stored in Git and ArgoCD reads it from the lab13 branch. + +When configuration changes are committed and pushed to Git, ArgoCD detects the difference between: + +desired state in Git +actual state in the Kubernetes cluster + +If the cluster does not match Git, ArgoCD marks the application as OutOfSync. + +Manual sync or auto-sync then applies the Git-defined state to the cluster. + +## 6. Self-Healing Evidence + +### Manual scale drift test + +The dev Deployment was manually scaled to 5 replicas: + +```bash +kubectl scale deployment devops-info-app-dev-devops-info-service -n dev --replicas=5 +kubectl get deployment -n dev +``` + + +Output immediately after manual change: + +NAME READY UP-TO-DATE AVAILABLE +devops-info-app-dev-devops-info-service 1/5 1 1 + +After 30 seconds, ArgoCD self-healing reverted the Deployment back to the Git-defined state: + +```bash +kubectl get deployment -n dev +``` + +Output: + +NAME READY UP-TO-DATE AVAILABLE +devops-info-app-dev-devops-info-service 1/1 1 1 + +This proves ArgoCD detected configuration drift and restored the desired Git state. + +### Pod deletion test + +A pod was manually deleted: + +```bash +kubectl delete pod -n dev -l app.kubernetes.io/name=devops-info-service +kubectl get pods -n dev -w +``` + +Output: + +pod "devops-info-app-dev-devops-info-service-6bc8d7dbfc-r5d5h" deleted +devops-info-app-dev-devops-info-service-6bc8d7dbfc-wjlww 0/1 Running +devops-info-app-dev-devops-info-service-6bc8d7dbfc-wjlww 1/1 Running + +This demonstrates Kubernetes self-healing. The Deployment controller recreated the deleted pod automatically. + +## 7. Kubernetes Self-Healing vs ArgoCD Self-Healing + +Kubernetes self-healing: + +recreates deleted pods +keeps ReplicaSets and Deployments at their desired replica count +works inside the cluster + +ArgoCD self-healing: + +compares cluster state with Git state +reverts manual changes that drift from Git +keeps Kubernetes configuration aligned with the repository + +In this lab: + +pod deletion was fixed by Kubernetes +manual scaling to 5 replicas was reverted by ArgoCD +## 8. Sync Policy Explanation + +Dev uses auto-sync because it is suitable for rapid iteration and testing. + +Prod uses manual sync because production deployments should be controlled, reviewed, and released intentionally. + +This separation is a common GitOps best practice. + +## 9. Challenges and Solutions +Redis image pull issue + +The default ArgoCD Redis image was pulled from AWS ECR and failed due to network issues. + +Solution: + +helm upgrade argocd argo/argo-cd -n argocd \ + --set redis.image.repository=docker.io/library/redis \ + --set redis.image.tag=8.2.3-alpine +Application image pull issue + +The kind cluster had intermittent network issues when pulling from Docker Hub. + +Solution: + +loaded the local image into kind +changed image pull policy to IfNotPresent +kind load docker-image fayzullin/devops-info-service:latest --name lab9 +NodePort conflict + +The Service initially failed because a fixed NodePort was already allocated. + +Solution: + +made nodePort optional in the Helm template +set nodePort: null in values + +## 10. Summary + +This lab implemented GitOps continuous delivery using ArgoCD. + +Completed: + +ArgoCD installed via Helm +UI accessed through port-forward +Applications deployed from Git +Helm chart synced by ArgoCD +dev and prod environments configured +dev auto-sync enabled +prod manual sync configured +self-healing tested +Kubernetes pod recovery tested + +Git is now the source of truth for Kubernetes application deployment. + +## 11. Screenshots + +The following screenshots were captured from ArgoCD UI: + +- `labs/lab13/screenshots/argocd-overview.png` — all applications (dev, prod) +- `labs/lab13/screenshots/dev-app.png` — dev application details +- `labs/lab13/screenshots/prod-app.png` — prod application details + +These screenshots show: +- Sync status (Synced) +- Health status (Healthy) +- Deployed resources diff --git a/labs/lab13/k8s/argocd/application-dev.yaml b/labs/lab13/k8s/argocd/application-dev.yaml new file mode 100644 index 0000000000..8cbf3177a2 --- /dev/null +++ b/labs/lab13/k8s/argocd/application-dev.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: devops-info-app-dev + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/fayz131/DevOps-Core-Course.git + targetRevision: lab13 + path: labs/lab12/k8s/devops-info-service + helm: + valueFiles: + - values-dev.yaml + destination: + server: https://kubernetes.default.svc + namespace: dev + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/labs/lab13/k8s/argocd/application-prod.yaml b/labs/lab13/k8s/argocd/application-prod.yaml new file mode 100644 index 0000000000..7798abd4e1 --- /dev/null +++ b/labs/lab13/k8s/argocd/application-prod.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: devops-info-app-prod + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/fayz131/DevOps-Core-Course.git + targetRevision: lab13 + path: labs/lab12/k8s/devops-info-service + helm: + valueFiles: + - values-prod.yaml + destination: + server: https://kubernetes.default.svc + namespace: prod + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/labs/lab13/k8s/argocd/application.yaml b/labs/lab13/k8s/argocd/application.yaml new file mode 100644 index 0000000000..6b676b922d --- /dev/null +++ b/labs/lab13/k8s/argocd/application.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: devops-info-app + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/fayz131/DevOps-Core-Course.git + targetRevision: lab13 + path: labs/lab12/k8s/devops-info-service + helm: + valueFiles: + - values-dev.yaml + destination: + server: https://kubernetes.default.svc + namespace: default + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/labs/lab13/screenshots/argocd-overview.png b/labs/lab13/screenshots/argocd-overview.png new file mode 100644 index 0000000000..960121829b Binary files /dev/null and b/labs/lab13/screenshots/argocd-overview.png differ diff --git a/labs/lab13/screenshots/dev-app.png b/labs/lab13/screenshots/dev-app.png new file mode 100644 index 0000000000..2f2ef6501b Binary files /dev/null and b/labs/lab13/screenshots/dev-app.png differ diff --git a/labs/lab13/screenshots/prod-app.png b/labs/lab13/screenshots/prod-app.png new file mode 100644 index 0000000000..d438755d60 Binary files /dev/null and b/labs/lab13/screenshots/prod-app.png differ