# Helm Chart Design > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add Helm chart for Kubernetes deployment as an alternative to Docker Compose. **Architecture:** Single Helm chart deploying frontend (nginx) and backend (Node.js) services with PVC for SQLite persistence and Ingress for external access. **Tech Stack:** Helm 3, Kubernetes --- ## Chart Structure ``` helm/cv-app/ ├── Chart.yaml ├── values.yaml ├── templates/ │ ├── _helpers.tpl │ ├── frontend-deployment.yaml │ ├── frontend-service.yaml │ ├── backend-deployment.yaml │ ├── backend-service.yaml │ ├── ingress.yaml │ ├── configmap.yaml │ ├── secret.yaml │ ├── pvc.yaml │ └── NOTES.txt └── .helmignore ``` ## Components ### Frontend - **Deployment**: nginx serving built React static files - **Service**: ClusterIP on port 80 - **Environment**: API URL from ConfigMap ### Backend - **Deployment**: Node.js Express server - **Service**: ClusterIP on port 3001 - **Volume**: PVC mount at `/app/data` for SQLite - **Environment**: Port, DB path, auth config from ConfigMap/Secret ### Ingress - Routes `/` → frontend service - Routes `/api` → backend service - Configurable TLS - Supports nginx, traefik ingress controllers ### Persistence - PVC for SQLite database - Configurable storage class - 1Gi default size ## Values Schema ```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: {} ``` ## Templates ### _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 }} ``` ### 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 }} labels: {{- include "cv-app.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: frontend spec: containers: - name: frontend image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP envFrom: - configMapRef: name: {{ include "cv-app.fullname" . }}-config livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {{- toYaml .Values.frontend.resources | nindent 12 }} ``` ### 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 ``` ### 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 }} labels: {{- include "cv-app.selectorLabels" . | nindent 8 }} app.kubernetes.io/component: backend spec: containers: - name: backend 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 }} ``` ### 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 ``` ### 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 }} ``` ### 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 }} ``` ### 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 }} ``` ### 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 }} ``` ### 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: http{{ if .Values.ingress.tls }}s{{ end }}://{{ (index .Values.ingress.hosts 0).host }}/api/docs ``` ## Usage Examples ### Basic Installation ```bash helm install cv-app ./helm/cv-app ``` ### With Custom Host ```bash helm install cv-app ./helm/cv-app \ --set ingress.hosts[0].host=cv.example.com ``` ### With TLS ```bash helm install cv-app ./helm/cv-app \ --set ingress.hosts[0].host=cv.example.com \ --set ingress.tls[0].hosts[0]=cv.example.com \ --set ingress.tls[0].secretName=cv-app-tls ``` ### With Keycloak ```bash helm install cv-app ./helm/cv-app \ --set backend.auth.mode=keycloak \ --set backend.keycloak.url=https://keycloak.example.com \ --set backend.keycloak.realm=myrealm \ --set backend.keycloak.clientId=cv-app ``` ### With Specific Storage Class ```bash helm install cv-app ./helm/cv-app \ --set persistence.storageClass=local-path \ --set persistence.size=5Gi ```