diff --git a/docs/plans/2026-02-23-helm-chart-implementation.md b/docs/plans/2026-02-23-helm-chart-implementation.md new file mode 100644 index 0000000..2759418 --- /dev/null +++ b/docs/plans/2026-02-23-helm-chart-implementation.md @@ -0,0 +1,636 @@ +# 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" +```