Helm¶
Purpose¶
Install, upgrade, and understand the helm/akshayabazaar chart that deploys every layer of the
application to any Kubernetes cluster - local k3d or real staging/production.
Prerequisites¶
helmonPATH(or the containerized fallback:docker run --rm -v "${PWD}/helm/akshayabazaar:/chart" alpine/helm:3.14.4 ...)kubectlpointed at the target cluster- The target namespace's app Secret already applied (see Secrets below - the chart never creates it for you)
Chart structure¶
helm/akshayabazaar/
├── Chart.yaml
├── values.yaml # base defaults + comments explaining every non-obvious choice
├── values-local-staging.yaml # local k3d overrides
├── values-dev.yaml
├── values-staging.yaml
├── values-production.yaml
└── templates/
├── backend-deployment.yaml, backend-service.yaml, backend-configmap.yaml, backend-hpa.yaml
├── frontend-deployment.yaml, frontend-service.yaml, frontend-nginx-configmap.yaml, frontend-hpa.yaml
├── migration-job.yaml
├── ingress.yaml
├── networkpolicy.yaml
└── monitoring/, backup/
Base values.yaml intentionally holds nothing secret - every credential comes from a Secret
referenced by name (backend.existingSecretName), never a literal in any values-*.yaml.
Secrets¶
The chart never creates the application Secret - by design, so a helm install/upgrade
fails loudly (CreateContainerConfigError) rather than silently starting with missing config if
you forget to apply it first. Shape (see helm/akshayabazaar/examples/secret.example.yaml):
apiVersion: v1
kind: Secret
metadata:
name: akshayabazaar-<env>-secrets # must match backend.existingSecretName in your values file
type: Opaque
stringData:
ConnectionStrings__DefaultConnection: "Server=<DB_HOST>;Port=3306;Database=ecommerce_db;User=<DB_USER>;Password=<MYSQL_PASSWORD>;"
ConnectionStrings__Redis: "<REDIS_HOST>:6379,password=<REDIS_PASSWORD>"
Jwt__SecretKey: "<JWT_SECRET_MIN_32_CHARS>"
Razorpay__KeySecret: "<RAZORPAY_KEY_SECRET>"
# Notification provider credentials - leave empty to keep that channel in Simulated mode
Notifications__Providers__Email__ApiKey: ""
Every environment uses a distinct Secret name (akshayabazaar-staging-secrets /
akshayabazaar-production-secrets / akshayabazaar-local-secrets) - a deliberate speed bump
against ever copy-pasting one environment's filled-in Secret into another namespace.
Install / upgrade¶
# Local
helm upgrade --install akshaya-local helm/akshayabazaar `
-f helm/akshayabazaar/values-local-staging.yaml `
--set backend.image.tag=local `
--set frontend.image.tag=local `
--namespace staging --create-namespace `
--wait --timeout 5m
# Staging (real cluster, KUBECONFIG pointed at it)
helm upgrade --install akshayabazaar helm/akshayabazaar `
-f helm/akshayabazaar/values-staging.yaml `
--set backend.image.tag=<commit-sha> `
--set frontend.image.tag=<commit-sha> `
--namespace akshayabazaar-staging --create-namespace `
--wait --timeout 5m --atomic
--atomic (used for staging/production by Jenkins) automatically rolls the release back to its
previous revision if the rollout doesn't reach Ready within --timeout - a first line of rollback
defense before any manual helm rollback. Prefer the wrapper scripts over typing this by hand:
scripts/deploy-local.ps1 (local), scripts/deploy-staging.ps1 (staging) - see
Deployment.
Migrations¶
migration-job.yaml is a pre-install,pre-upgrade Helm hook Job - it runs before the
Deployments' new pods start, every single install/upgrade, using the same image with args:
["--migrate-only"] (a dedicated startup path in Program.cs that runs
Database.MigrateAsync() then seeds, and returns - never starting the normal web host).
backoffLimit: 2 (3 total attempts). A failed Job is deliberately left in place (not
auto-cleaned) so kubectl logs/kubectl describe on it stays available for debugging after
helm upgrade reports failure; a succeeded Job is cleaned up automatically before the next
attempt (hook-delete-policy: before-hook-creation,hook-succeeded).
kubectl get pods -n staging -l job-name=akshaya-local-backend-migrate
kubectl logs -n staging <migrate-pod-name> --all-containers --tail=200
Backend/frontend Deployments¶
| Backend | Frontend | |
|---|---|---|
| Rolling update | maxSurge: 1, maxUnavailable: 0 - capacity never drops mid-deploy |
same |
securityContext |
runAsNonRoot: true, runAsUser/Group: 1000, readOnlyRootFilesystem: true, all capabilities dropped |
runAsNonRoot: true, runAsUser: 101, readOnlyRootFilesystem: true |
| Startup probe | GET /health/live, every 5s, 30 failures allowed (generous - migrations run separately, this just waits for the process itself) |
GET /, every 3s, 10 failures allowed |
| Liveness probe | GET /health/live (no DB check - only fails if the process itself is wedged), every 10s, 3 failures |
GET /, every 10s, 3 failures |
| Readiness probe | GET /health/ready (bounded 3s DB + Redis check), every 10s, 3 failures |
GET /, every 10s, 3 failures |
| Config change picked up automatically | Yes - checksum/config pod annotation forces a restart on any ConfigMap change, even without a new image tag |
same pattern |
preStop hook |
sleep before the container actually stops, so the Service's endpoint list catches up before the app refuses new connections |
n/a |
Services¶
Neither Service is a LoadBalancer/NodePort - both are plain ClusterIP, reached only via
Ingress (frontend) or in-cluster DNS (backend, from the frontend's nginx only - see
Network Policy).
Health checks (what backs each probe)¶
/health/live- process-up only, registers no dependency checks (Predicate = _ => false)./health/readyand/health- the full check:AppDbContext.CanConnectAsync()(a customBoundedDbContextHealthCheckwith an explicit 3s timeout, independent of EF Core's own retry policy) and Redis (AddRedis, whenConnectionStrings__Redisis configured).
Verification¶
.\scripts\verify-staging.ps1 -Context k3d-akshaya-staging -Namespace staging -ReleaseName akshaya-local -IngressHost staging.akshaya.local -IngressPort 80
Rollback¶
Or.\scripts\rollback-staging.ps1 (dry-run by default, -Yes to actually roll back). Full
detail, including what a Helm rollback does not undo (database migrations, Secret rotations):
Operations → Rollback.
Troubleshooting¶
See Kubernetes → Troubleshooting.
Security considerations¶
- No
values-*.yamlfile has ever contained a real secret - verified by repository-wide scan. existingSecretNameis the only mechanism the chart uses to reach credentials; it never provisions or reads a cloud secret manager directly.- Every container in the chart runs as a pinned non-root numeric UID with a read-only root filesystem and all Linux capabilities dropped.