# Helm Chart Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add Helm chart for Kubernetes deployment with frontend, backend, ingress, and persistence. **Architecture:** Single Helm chart with templates for Deployments, Services, Ingress, ConfigMap, Secret, and PVC. **Tech Stack:** Helm 3, Kubernetes --- ### Task 1: Create Helm chart structure **Files:** - Create: `helm/cv-app/Chart.yaml` - Create: `helm/cv-app/.helmignore` **Step 1: Create helm directory** ```bash mkdir -p helm/cv-app/templates ``` **Step 2: Create Chart.yaml** ```yaml apiVersion: v2 name: cv-app description: A Helm chart for CV application type: application version: 0.1.0 appVersion: "1.0.0" maintainers: - name: Tuan-Dat Tran email: tuan-dat.tran@tudattr.dev ``` **Step 3: Create .helmignore** ``` # Patterns to ignore when building packages. .git/ .github/ .vscode/ .idea/ # Test files *_test.go tests/ # Documentation *.md !README.md docs/ # CI/CD .github/ ``` **Step 4: Commit** ```bash git add helm/cv-app/Chart.yaml helm/cv-app/.helmignore git commit -m "feat(helm): add chart structure and metadata" ``` --- ### Task 2: Create helper templates **Files:** - Create: `helm/cv-app/templates/_helpers.tpl` **Step 1: Create _helpers.tpl** ```yaml {{- define "cv-app.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{- define "cv-app.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{- define "cv-app.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{- define "cv-app.labels" -}} helm.sh/chart: {{ include "cv-app.chart" . }} {{ include "cv-app.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{- define "cv-app.selectorLabels" -}} app.kubernetes.io/name: {{ include "cv-app.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{- define "cv-app.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "cv-app.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/_helpers.tpl git commit -m "feat(helm): add template helpers" ``` --- ### Task 3: Create default values **Files:** - Create: `helm/cv-app/values.yaml` **Step 1: Create values.yaml** ```yaml frontend: replicaCount: 1 image: repository: username/cv-app tag: latest pullPolicy: IfNotPresent resources: {} backend: replicaCount: 1 image: repository: username/cv-app-backend tag: latest pullPolicy: IfNotPresent auth: mode: simple keycloak: url: "" realm: "" clientId: "" resources: {} persistence: enabled: true size: 1Gi storageClass: "" ingress: enabled: true className: "" annotations: {} hosts: - host: cv.local paths: - path: / service: frontend - path: /api service: backend tls: [] imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: create: true annotations: {} name: "" podAnnotations: {} podSecurityContext: {} securityContext: {} nodeSelector: {} tolerations: [] affinity: {} ``` **Step 2: Commit** ```bash git add helm/cv-app/values.yaml git commit -m "feat(helm): add default values configuration" ``` --- ### Task 4: Create ConfigMap template **Files:** - Create: `helm/cv-app/templates/configmap.yaml` **Step 1: Create configmap.yaml** ```yaml apiVersion: v1 kind: ConfigMap metadata: name: {{ include "cv-app.fullname" . }}-config labels: {{- include "cv-app.labels" . | nindent 4 }} data: PORT: "3001" DB_PATH: "/app/data/cv.db" AUTH_MODE: {{ .Values.backend.auth.mode | quote }} {{- if eq .Values.backend.auth.mode "keycloak" }} KEYCLOAK_URL: {{ .Values.backend.keycloak.url | quote }} KEYCLOAK_REALM: {{ .Values.backend.keycloak.realm | quote }} KEYCLOAK_CLIENT_ID: {{ .Values.backend.keycloak.clientId | quote }} {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/configmap.yaml git commit -m "feat(helm): add ConfigMap template" ``` --- ### Task 5: Create Secret template **Files:** - Create: `helm/cv-app/templates/secret.yaml` **Step 1: Create secret.yaml** ```yaml apiVersion: v1 kind: Secret metadata: name: {{ include "cv-app.fullname" . }}-secret labels: {{- include "cv-app.labels" . | nindent 4 }} type: Opaque data: {{- if eq .Values.backend.auth.mode "simple" }} JWT_SECRET: {{ randAlphaNum 32 | b64enc | quote }} {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/secret.yaml git commit -m "feat(helm): add Secret template for JWT" ``` --- ### Task 6: Create PVC template **Files:** - Create: `helm/cv-app/templates/pvc.yaml` **Step 1: Create pvc.yaml** ```yaml {{- if .Values.persistence.enabled -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ include "cv-app.fullname" . }}-data labels: {{- include "cv-app.labels" . | nindent 4 }} spec: accessModes: - ReadWriteOnce {{- if .Values.persistence.storageClass }} storageClassName: {{ .Values.persistence.storageClass | quote }} {{- end }} resources: requests: storage: {{ .Values.persistence.size }} {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/pvc.yaml git commit -m "feat(helm): add PersistentVolumeClaim template" ``` --- ### Task 7: Create backend Deployment and Service **Files:** - Create: `helm/cv-app/templates/backend-deployment.yaml` - Create: `helm/cv-app/templates/backend-service.yaml` **Step 1: Create backend-deployment.yaml** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "cv-app.fullname" . }}-backend labels: {{- include "cv-app.labels" . | nindent 4 }} app.kubernetes.io/component: backend spec: replicas: {{ .Values.backend.replicaCount }} selector: matchLabels: {{- include "cv-app.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: backend template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "cv-app.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: backend spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "cv-app.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: backend securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.backend.image.pullPolicy }} ports: - name: http containerPort: 3001 protocol: TCP envFrom: - configMapRef: name: {{ include "cv-app.fullname" . }}-config - secretRef: name: {{ include "cv-app.fullname" . }}-secret volumeMounts: - name: data mountPath: /app/data livenessProbe: httpGet: path: /health port: http readinessProbe: httpGet: path: /health port: http resources: {{- toYaml .Values.backend.resources | nindent 12 }} volumes: - name: data {{- if .Values.persistence.enabled }} persistentVolumeClaim: claimName: {{ include "cv-app.fullname" . }}-data {{- else }} emptyDir: {} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ``` **Step 2: Create backend-service.yaml** ```yaml apiVersion: v1 kind: Service metadata: name: {{ include "cv-app.fullname" . }}-backend labels: {{- include "cv-app.labels" . | nindent 4 }} app.kubernetes.io/component: backend spec: type: ClusterIP ports: - port: 3001 targetPort: http protocol: TCP name: http selector: {{- include "cv-app.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: backend ``` **Step 3: Commit** ```bash git add helm/cv-app/templates/backend-deployment.yaml helm/cv-app/templates/backend-service.yaml git commit -m "feat(helm): add backend Deployment and Service templates" ``` --- ### Task 8: Create frontend Deployment and Service **Files:** - Create: `helm/cv-app/templates/frontend-deployment.yaml` - Create: `helm/cv-app/templates/frontend-service.yaml` **Step 1: Create frontend-deployment.yaml** ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "cv-app.fullname" . }}-frontend labels: {{- include "cv-app.labels" . | nindent 4 }} app.kubernetes.io/component: frontend spec: replicas: {{ .Values.frontend.replicaCount }} selector: matchLabels: {{- include "cv-app.selectorLabels" . | nindent 6 }} app.kubernetes.io/component: frontend template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "cv-app.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: frontend spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "cv-app.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: frontend securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {{- toYaml .Values.frontend.resources | nindent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ``` **Step 2: Create frontend-service.yaml** ```yaml apiVersion: v1 kind: Service metadata: name: {{ include "cv-app.fullname" . }}-frontend labels: {{- include "cv-app.labels" . | nindent 4 }} app.kubernetes.io/component: frontend spec: type: ClusterIP ports: - port: 80 targetPort: http protocol: TCP name: http selector: {{- include "cv-app.selectorLabels" . | nindent 4 }} app.kubernetes.io/component: frontend ``` **Step 3: Commit** ```bash git add helm/cv-app/templates/frontend-deployment.yaml helm/cv-app/templates/frontend-service.yaml git commit -m "feat(helm): add frontend Deployment and Service templates" ``` --- ### Task 9: Create Ingress template **Files:** - Create: `helm/cv-app/templates/ingress.yaml` **Step 1: Create ingress.yaml** ```yaml {{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include "cv-app.fullname" . }} labels: {{- include "cv-app.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.className }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: Prefix backend: service: name: {{ include "cv-app.fullname" $ }}-{{ .service }} port: number: {{ if eq .service "frontend" }}80{{ else }}3001{{ end }} {{- end }} {{- end }} {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/ingress.yaml git commit -m "feat(helm): add Ingress template" ``` --- ### Task 10: Create NOTES.txt and update documentation **Files:** - Create: `helm/cv-app/templates/NOTES.txt` - Modify: `docs/architecture.md` **Step 1: Create NOTES.txt** ``` Your CV application has been deployed! Access your application at: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }} {{- end }} {{- else }} Frontend: http://{{ include "cv-app.fullname" . }}-frontend.{{ .Release.Namespace }}.svc.cluster.local Backend API: http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001 {{- end }} {{- if eq .Values.backend.auth.mode "simple" }} Get the admin password: kubectl logs deployment/{{ include "cv-app.fullname" . }}-backend | grep "ADMIN PASSWORD" {{- end }} API Documentation: {{- if .Values.ingress.enabled }} {{- if .Values.ingress.tls }} https://{{ (index .Values.ingress.hosts 0).host }}/api/docs {{- else }} http://{{ (index .Values.ingress.hosts 0).host }}/api/docs {{- end }} {{- else }} http://{{ include "cv-app.fullname" . }}-backend.{{ .Release.Namespace }}.svc.cluster.local:3001/api/docs {{- end }} ``` **Step 2: Commit** ```bash git add helm/cv-app/templates/NOTES.txt git commit -m "feat(helm): add post-install notes" ```