Skip to content

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

  • helm on PATH (or the containerized fallback: docker run --rm -v "${PWD}/helm/akshayabazaar:/chart" alpine/helm:3.14.4 ...)
  • kubectl pointed 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: ""
kubectl create namespace staging
kubectl apply -n staging -f your-filled-in-secret.yaml

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/ready and /health - the full check: AppDbContext.CanConnectAsync() (a custom BoundedDbContextHealthCheck with an explicit 3s timeout, independent of EF Core's own retry policy) and Redis (AddRedis, when ConnectionStrings__Redis is configured).

Verification

.\scripts\verify-staging.ps1 -Context k3d-akshaya-staging -Namespace staging -ReleaseName akshaya-local -IngressHost staging.akshaya.local -IngressPort 80
Full detail: Operations → Verification.

Rollback

helm history akshaya-local -n staging
helm rollback akshaya-local <REVISION> -n staging
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-*.yaml file has ever contained a real secret - verified by repository-wide scan.
  • existingSecretName is 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.