Skip to content

navashiva/helm-fullstack-webapp

Repository files navigation

WebApp Helm Chart

Enterprise-grade Helm chart for deploying a full-stack web application on Kubernetes: React (Nginx) frontend + Python FastAPI backend + PostgreSQL via CloudNativePG.

Built and versioned with Gradle.


Table of Contents


Architecture

                        ┌─────────────────────────────────────┐
                        │           Kubernetes Cluster         │
                        │                                      │
Internet ──► Ingress ──►│─► Frontend (Nginx, :80)             │
                        │         │                            │
                        │         └──► Backend (FastAPI, :8000)│
                        │                    │                 │
                        │                    └──► PostgreSQL   │
                        │                    (CloudNativePG)   │
                        └─────────────────────────────────────┘
Component Default Image Port
Frontend nginx:stable-alpine 80
Backend python:3.11-slim 8000
Database ghcr.io/cloudnative-pg/postgresql:18 5432

The chart creates all Kubernetes resources needed for a production deployment: Services, Deployments, ConfigMap, ServiceAccount, Ingress, HPA, PodDisruptionBudget, NetworkPolicy, and a CloudNativePG Cluster.


Prerequisites

Cluster

  • Kubernetes 1.20+

  • CloudNativePG operator installed before this chart:

    helm repo add cnpg https://cloudnative-pg.github.io/charts
    helm upgrade --install cnpg cnpg/cloudnative-pg \
      --namespace cnpg-system --create-namespace --wait
  • (Optional) An Ingress controller if ingress.enabled: true or per-component ingresses are used. For Docker Desktop:

    kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.12.1/deploy/static/provider/cloud/deploy.yaml

Build toolchain

Tool Version
Java (JDK) 21 (Temurin)
Gradle 9.0.0 (via wrapper — no install needed)
Helm 3.x
helm-unittest plugin latest

Install the helm-unittest plugin once:

helm plugin install https://github.com/helm-unittest/helm-unittest

Quick Start

# 1. Validate and test
./gradlew check

# 2. Package
./gradlew helmPackage
# → build/helm/webapp-<version>.tgz

# 3. Install
helm install my-app build/helm/webapp-*.tgz \
  --namespace my-app --create-namespace

Building

The build system is Gradle with custom plugins in buildSrc/. All Helm operations are wrapped as Gradle tasks.

Task reference

Task Description
helmLint helm lint — validates chart syntax
helmTest helm template --debug — renders all templates, fails on errors
helmUnitTest helm unittest — runs the unit test suites in src/test/helm/
helmPackage Packages the chart → build/helm/webapp-<version>.tgz
helmSourceArchive Creates a source zip → build/helm/webapp-<version>-source.zip
helmPublish Uploads the chart to Artifactory
helmPublishGitHub Pushes the chart to GitHub Packages (ghcr.io)
helmPublishLocal Copies the chart to a local file-based Helm repo
helmTag Creates an annotated git tag at the resolved version
helmRelease Creates a GitHub Release with the .tgz and source zip attached

Gradle lifecycle integration

./gradlew check    →  helmLint + helmTest + helmUnitTest
./gradlew assemble →  helmPackage
./gradlew build    →  check + assemble  (full quality gate)

Task dependency graph

helmLint ──► helmTest ──────────────────► helmPackage ──┬──► helmPublish
    │             │                                      ├──► helmPublishGitHub
    │         helmUnitTest ───────────────► helmPackage  ├──► helmPublishLocal
    │                                                    ├──► helmTag
    └─────────────────────────────────────────────────── └──► helmRelease ◄── helmSourceArchive

Testing

Tests live in src/test/helm/webapp/tests/ and are run with helm-unittest.

./gradlew helmUnitTest

Test suites:

Suite What it covers
backend_deployment_test.yaml Backend Deployment — image, env vars, probes, security context
frontend_deployment_test.yaml Frontend Deployment — image, probes, security context
configmap_test.yaml Backend ConfigMap — all config keys rendered correctly
database_cluster_test.yaml CloudNativePG Cluster — instances, storage, bootstrap config
migrations_job_test.yaml Migration Job hook — lifecycle, command, resource inheritance
networkpolicy_test.yaml NetworkPolicy — ingress/egress selectors
serviceaccount_test.yaml ServiceAccount — creation toggle, token auto-mount

Publishing

Artifactory

./gradlew helmPublish \
  -PartifactoryUrl=https://art.example.com \
  -PartifactoryUsername=deployer \
  -PartifactoryPassword=<api-key>

Or set environment variables ARTIFACTORY_URL, ARTIFACTORY_USERNAME, ARTIFACTORY_PASSWORD.

GitHub Packages (OCI)

./gradlew helmPublishGitHub \
  -PgithubToken=ghp_xxx \
  -PgithubOwner=my-org

Install from OCI:

helm install my-app oci://ghcr.io/my-org/webapp --version <version>

Local file-based repo

./gradlew helmPublishLocal
helm install my-app webapp --repo file://~/.helm/repository/local

GitHub Release

./gradlew helmRelease \
  -PgithubToken=ghp_xxx \
  -PgithubRepo=my-org/my-repo

Creates a GitHub Release and uploads webapp-<version>.tgz and webapp-<version>-source.zip.

Tagging (advances the version counter)

./gradlew helmTag -PhelmTagPush=true

Creates an annotated git tag and pushes it to origin. The next build auto-increments past this tag.


Installing the Chart

Minimal install

helm install my-app ./build/helm/webapp-<version>.tgz \
  --namespace my-app --create-namespace

Create the backend secret first

The backend requires SECRET_KEY and FIRST_SUPERUSER_PASSWORD at runtime. Create the secret before installing:

kubectl create secret generic my-backend-secrets \
  --from-literal=SECRET_KEY=$(openssl rand -hex 32) \
  --from-literal=FIRST_SUPERUSER_PASSWORD='<strong-password>' \
  --namespace my-app

Then reference it in values:

backend:
  existingSecret: my-backend-secrets

Upgrade

helm upgrade my-app ./build/helm/webapp-<version>.tgz \
  --namespace my-app --values my-values.yaml

If migrations.enabled: true, the migration Job runs as a pre-upgrade hook before new backend pods roll out.

Uninstall

helm uninstall my-app --namespace my-app

PVCs created by CloudNativePG are not deleted by Helm uninstall. Delete them manually if no longer needed.


Configuration

All options are documented in src/main/helm/webapp/values.yaml. Key sections are summarised below.

Global

Key Default Description
global.imageRegistry "" Registry prefix applied to all component images
global.imagePullSecrets [] Pull secrets applied to all pods
global.storageClass "" Default StorageClass for all PVCs
global.nameOverride "" Override chart name used in resource names
global.fullnameOverride "" Override fully-qualified release name

Frontend

Key Default Description
frontend.enabled true Deploy the frontend
frontend.image.repository nginx Image repository
frontend.image.tag stable-alpine Image tag
frontend.replicaCount 1 Number of pods
frontend.resources 50m/64Mi → 200m/128Mi CPU/memory requests and limits
frontend.autoscaling.enabled false Enable HPA
frontend.pdb.enabled false Enable PodDisruptionBudget
frontend.service.port 80 Service port
frontend.ingress.enabled false Enable per-component Ingress

Backend

Key Default Description
backend.enabled true Deploy the backend
backend.image.repository python Image repository
backend.image.tag 3.11-slim Image tag
backend.replicaCount 1 Number of pods
backend.resources 100m/256Mi → 500m/512Mi CPU/memory requests and limits
backend.autoscaling.enabled false Enable HPA
backend.pdb.enabled false Enable PodDisruptionBudget
backend.service.port 8000 Service port
backend.existingSecret "" Secret with SECRET_KEY and FIRST_SUPERUSER_PASSWORD
backend.config.projectName Web Application PROJECT_NAME env var
backend.config.environment production ENVIRONMENT env var (local/staging/production)
backend.config.apiV1Str /api/v1 API_V1_STR env var
backend.config.corsOrigins "" BACKEND_CORS_ORIGINS env var (comma-separated)
backend.config.frontendHost http://localhost FRONTEND_HOST env var
backend.config.firstSuperuser admin@example.com FIRST_SUPERUSER env var
backend.ingress.enabled false Enable per-component Ingress

Database (CloudNativePG)

Key Default Description
database.enabled true Deploy the CloudNativePG Cluster
database.instances 1 Number of PostgreSQL instances (use 3 for HA)
database.imageName ghcr.io/cloudnative-pg/postgresql:18 PostgreSQL image
database.storage.size 10Gi Data PVC size
database.storage.storageClass "" StorageClass for data PVC
database.walStorage.enabled false Dedicated WAL PVC (recommended for production)
database.walStorage.size 2Gi WAL PVC size
database.bootstrap.initdb.database app Database name created on bootstrap
database.bootstrap.initdb.owner app Database owner role
database.monitoring.enablePodMonitor false Emit PodMonitor (requires Prometheus Operator)

The operator auto-generates a secret <cluster-name>-app with keys host, port, username, password, dbname, uri, jdbc-uri. The backend reads these automatically.

Combined Ingress (single domain, path-based routing)

Use this when both services share the same hostname. /api routes to the backend; / routes to the frontend.

Key Default Description
ingress.enabled false Enable the combined Ingress
ingress.className nginx IngressClass name
ingress.host example.com Hostname
ingress.annotations {} Ingress annotations
ingress.tls [] TLS configuration

Example:

ingress:
  enabled: true
  className: nginx
  host: app.example.com
  tls:
    - secretName: app-tls
      hosts:
        - app.example.com

Migrations

Key Default Description
migrations.enabled false Run migration Job on install/upgrade
migrations.command ["bash", "scripts/prestart.sh"] Command to execute
migrations.activeDeadlineSeconds 600 Max Job runtime
migrations.backoffLimit 2 Pod restart attempts before failure

The Job is a Helm hook (post-install, pre-upgrade) using the backend image, so it shares the same DB environment variables and ConfigMap.

Network Policies

networkPolicy:
  enabled: true  # default: false

When enabled, enforces least-privilege traffic:

  • PostgreSQL port 5432: only backend and migrations pods may connect
  • Backend port: only frontend pods and Ingress controller traffic may connect
  • Backend egress: DNS and database only

ServiceAccount

Key Default Description
serviceAccount.create true Create a dedicated ServiceAccount
serviceAccount.automountServiceAccountToken false Disable token auto-mount
serviceAccount.annotations {} e.g. IRSA (AWS) or Workload Identity (GCP) annotations

Production example

global:
  imageRegistry: my-registry.example.com

frontend:
  image:
    repository: my-org/frontend
    tag: "1.2.3"
  replicaCount: 2
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10

backend:
  image:
    repository: my-org/backend
    tag: "1.2.3"
  replicaCount: 2
  autoscaling:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
  existingSecret: my-backend-secrets
  config:
    projectName: "My App"
    environment: production
    corsOrigins: "https://app.example.com"
    frontendHost: "https://app.example.com"

database:
  instances: 3
  storage:
    size: 50Gi
  walStorage:
    enabled: true
    size: 20Gi

ingress:
  enabled: true
  className: nginx
  host: app.example.com
  tls:
    - secretName: app-tls
      hosts:
        - app.example.com

migrations:
  enabled: true

networkPolicy:
  enabled: true

Versioning

Chart versions are resolved automatically from baseVersion in gradle.properties and existing git tags. No manual version bumping is needed.

How it works

The number of components in baseVersion controls which SemVer part is auto-incremented:

baseVersion Increments Example sequence
1 MINOR 1.0.0 → 1.1.0 → 1.2.0
0.2 PATCH 0.2.0 → 0.2.1 → 0.2.2
0.1.0 PATCH (floor-clamped) 0.1.0 → 0.1.1 → 0.1.2

Resolution rules:

  • No matching git tags → version equals baseVersion
  • Matching tags found → max(tag) + 1
  • main / release/* branches → clean version (e.g. 0.1.3)
  • Any other branch → snapshot with short git SHA (e.g. 0.1.3-a1b2c3d)

To start a new version line, change baseVersion — the counter resets automatically.

To pin a one-off version:

./gradlew helmPackage -PchartVersion=0.1.99

Release workflow

./gradlew helmPackage        # resolves version, packages chart
./gradlew helmPublishGitHub  # publishes to ghcr.io
./gradlew helmTag -PhelmTagPush=true  # creates git tag → next build auto-increments
./gradlew helmRelease        # creates GitHub Release with artifacts

CI/CD

The GitHub Actions workflow at .github/workflows/helm-ci.yml runs on every push.

Jobs

check — runs on all branches:

  1. Checks out repository with full git history (required for tag scanning)
  2. Sets up Java 21 and Helm with helm-unittest
  3. Runs ./gradlew check (lint + template render + unit tests)

publish — runs on main and release/* branches only, after check passes:

  1. Packages the chart and source archive
  2. Publishes to GitHub Packages (ghcr.io)
  3. Creates an annotated git tag and pushes to origin
  4. Creates a GitHub Release with chart and source attachments

Required secrets

Secret Used by
GITHUB_TOKEN Auto-supplied by GitHub Actions (push tags, create releases, push to ghcr.io)
ARTIFACTORY_URL helmPublish task (optional — only if publishing to Artifactory)
ARTIFACTORY_USERNAME helmPublish task
ARTIFACTORY_PASSWORD helmPublish task

Project Layout

.
├── build.gradle.kts              # Root build — applies all helm-* plugins
├── settings.gradle.kts           # Project name: webapp-helm
├── gradle.properties             # baseVersion, helmChartDir, helmTestDir, linting flags
├── buildSrc/                     # Custom Gradle plugins (Kotlin DSL)
│   └── src/main/kotlin/
│       ├── HelmContext.kt        # Extension holding chart metadata and resolved version
│       ├── HelmHelpers.kt        # Utilities: git, versioning, property resolution
│       ├── helm-base.gradle.kts
│       ├── helm-lint.gradle.kts
│       ├── helm-test.gradle.kts
│       ├── helm-package.gradle.kts
│       ├── helm-publish.gradle.kts
│       ├── helm-publish-github.gradle.kts
│       ├── helm-publish-local.gradle.kts
│       ├── helm-release.gradle.kts
│       └── helm-tag.gradle.kts
├── src/
│   ├── main/helm/webapp/         # Chart source
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   └── templates/
│   │       ├── _helpers.tpl
│   │       ├── configmap.yaml
│   │       ├── serviceaccount.yaml
│   │       ├── networkpolicy.yaml
│   │       ├── ingress.yaml      # Combined path-based ingress
│   │       ├── frontend/         # Deployment, Service, Ingress, HPA, PDB
│   │       ├── backend/          # Deployment, Service, Ingress, HPA, PDB
│   │       ├── database/         # CloudNativePG Cluster
│   │       └── migrations/       # Helm hook Job
│   └── test/helm/webapp/tests/   # helm-unittest suites
└── .github/workflows/
    └── helm-ci.yml