Designing a Scalable Helm Platform with Subcharts: A Practical, Production-Ready Approach

Introduction

Helm is often introduced as a simple templating tool for Kubernetes. In practice, however, its real power lies elsewhere:

Helm is a system composition and versioning tool for infrastructure.

In this article, we will walk through a complete, production-grade approach to designing a multi-service platform using Helm subcharts, focusing on:

  • Clean architecture and separation of concerns
  • Proper value hierarchy (global vs service-specific)
  • Naming strategies to avoid collisions
  • Reusable, generic templates
  • Packaging and distributing charts via Nexus

The system consists of three independent services:

  • devops-api
  • iplocation-api
  • totp-api

Each service is developed in Go and versioned independently.

https://github.com/faustobranco/devops-db/tree/master/infrastructure/resources

https://github.com/faustobranco/devops-db/tree/master/infrastructure/helms


1. Service Layer: Independent Applications

Each service lives in its own repository:

https://gitlab.devops-db.internal/infrastructure/resources/devops-api
https://gitlab.devops-db.internal/infrastructure/resources/iplocation-api
https://gitlab.devops-db.internal/infrastructure/resources/totp-api

These services expose simple HTTP endpoints (/health, /version, etc.), which makes them ideal for demonstrating platform composition.


2. Helm Charts per Service

Each service has its own Helm chart:

https://gitlab.devops-db.internal/infrastructure/helms/devops-api
https://gitlab.devops-db.internal/infrastructure/helms/iplocation-api
https://gitlab.devops-db.internal/infrastructure/helms/totp-api

2.1 Chart.yaml: Versioning Strategy

Example:

apiVersion: v2
name: devops-api
type: application
version: 1.0.4
appVersion: "1.1.2"

Important Distinction

  • version → Helm chart version (controls deployment logic)
  • appVersion → Application version (informational)

Helm does not deploy appVersion — it deploys what your templates define (e.g., image.tag).


2.2 Template Design Philosophy

Each chart is:

  • Generic
  • Reusable
  • Driven entirely by values

Example: Deployment (simplified)

metadata:
  name: {{ printf "%s-%s" .Release.Name .Chart.Name }}

labels:
  app: {{ .Chart.Name }}

3. Naming Strategy (Critical)

When using subcharts, all resources share the same:

.Release.Name

Without proper naming, this leads to collisions.


Problem

devops-api → creates Service "devops-platform"
totp-api → creates Service "devops-platform"

💥 Conflict.


Solution

name: {{ printf "%s-%s-%s" .Release.Name .Chart.Name $name }}

Result

devops-platform-devops-api
devops-platform-iplocation-api
devops-platform-totp-api

Rule

metadata.name → must be unique
labels → should represent service identity

4. Values Hierarchy

This is one of the most important aspects of Helm architecture.


4.1 Platform values.yaml

global:
  environment: prod

devops-api:
  replicaCount: 1
  ingress:
    enabled: true
    host: devops-api.devops-db.internal
  image:
    repository: nexus.devops-db.internal/devops_images/devops-api
    pullPolicy: IfNotPresent
    tag: "1.1.2"
  service:
    port: 80
  secrets:
    devops-api:
      type: Opaque
      data:    
        GITLAB_TOKEN: Z2xwYXQtdWN0WEhTcDN6cW5MQUsxQmJSMXRhRzg2TVFwMU9qWUguMDEuMHcxaXJydWly
        POSTGRES_PASSWORD: MTIzNHF3ZXI=
        POSTGRES_USER: ZGV2b3BzX2FwaQ==  
  env:
    DEVOPSAPI_VERSION: v1.1.2  
  configMaps:
    db-hosts:
      data:
        customers_dev: postgresql.devops-db.internal
        inventory_dev: postgresql.devops-db.internal
        orders_dev: postgresql.devops-db.internal
        payment_dev: postgresql.devops-db.internal
        reporting_dev: postgresql.devops-db.internal
        customers_staging: postgresql.devops-db.internal
        inventory_staging: postgresql.devops-db.internal
        orders_staging: postgresql.devops-db.internal
        payment_staging: postgresql.devops-db.internal
        reporting_staging: postgresql.devops-db.internal
        customers_prod: postgresql.devops-db.internal
        inventory_prod: postgresql.devops-db.internal
        orders_prod: postgresql.devops-db.internal
        payment_prod: postgresql.devops-db.internal
        reporting_prod: postgresql.devops-db.internal    

iplocation-api:
  replicaCount: 1
  ingress:
    enabled: true
    host: iplocation-api.devops-db.internal
  image:
    repository: nexus.devops-db.internal/devops_images/iplocation
    pullPolicy: IfNotPresent
    tag: "1.0.0"
  service:
    port: 80
  env:
    IPLOCATOR_VERSION: v1.0.0  

totp-api:
  replicaCount: 1
  ingress:
    enabled: true
    host: totp-api.devops-db.internal
  image:
    repository: nexus.devops-db.internal/devops_images/totp-validator
    pullPolicy: IfNotPresent
    tag: "1.0.0"
  service:
    port: 80
  env:
    TOTPVALIDATOR_VERSION: v1.0.0  

4.2 How Values Flow

platform values
   ↓
subchart scope
   ↓
templates

Key Insight

.Values.devops-api → only visible inside devops-api chart
.Values.global → visible everywhere

5. Dynamic Secrets and ConfigMaps


5.1 Secret Template (Reusable)

{{- if .Values.secrets }}
{{- range $name, $secret := .Values.secrets }}
---
apiVersion: v1
kind: Secret
metadata:
  name: {{ printf "%s-%s-%s" $.Release.Name $.Chart.Name $name }}
  namespace: {{ $.Release.Namespace }}
type: {{ $secret.type }}
data:
{{- range $key, $value := $secret.data }}
  {{ $key }}: {{ $value }}
{{- end }}
{{- end }}
{{- end }}

5.2 ConfigMap Template

{{- if .Values.configMaps }}
{{- range $name, $config := .Values.configMaps }}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ printf "%s-%s-%s" $.Release.Name $.Chart.Name $name }}
  namespace: {{ $.Release.Namespace }}
data:
{{- range $key, $value := $config.data }}
  {{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}

5.3 Why This Matters

This design allows:

✔ zero hardcoded environment variables
✔ dynamic configuration
✔ reusable charts
✔ consistent structure

6. Packaging and Publishing Charts


6.1 Package Charts

Each service chart must be packaged before being published.

helm package devops-api/.
helm package iplocation-api/.
helm package totp-api/.

This command generates a .tgz archive for each chart:

devops-api-1.0.4.tgz
iplocation-api-1.0.5.tgz
totp-api-1.0.6.tgz

Why this matters

The generated .tgz files are:

  • Versioned artifacts based on Chart.yaml
  • The unit of distribution for Helm charts
  • The only files that should be uploaded to a repository (e.g., Nexus)

Helm uses the version field from Chart.yaml to name and manage these packages:

version: 1.0.4

This means:

Chart version → defines the package version
.tgz file → immutable artifact

Key Principle

Never push raw chart directories to a repository.
Only packaged (.tgz) charts should be published.

This ensures:

  • Reproducibility
  • Version traceability
  • Compatibility with Helm repositories


6.2 Upload to Nexus

curl -u usr_jenkins_nexus:1234qwer --upload-file devops-api-1.0.4.tgz https://nexus.devops-db.internal/repository/helm-devops-db/

curl -u usr_jenkins_nexus:1234qwer --upload-file iplocation-api-1.0.5.tgz https://nexus.devops-db.internal/repository/helm-devops-db/

curl -u usr_jenkins_nexus:1234qwer --upload-file totp-api-1.0.6.tgz https://nexus.devops-db.internal/repository/helm-devops-db/

6.3 Add Repository

helm repo add devops-db https://nexus.devops-db.internal/repository/helm-devops-db/
helm repo update
helm search repo devops-db

6.4 Result

"devops-db" has been added to your repositories
Hang tight while we grab the latest from your chart repositories...
Update Complete. ⎈Happy Helming!⎈
NAME                    	CHART VERSION	APP VERSION	DESCRIPTION
devops-db/devops-api    	1.0.4        	1.1.2      	A Helm chart for Kubernetes
devops-db/iplocation-api	1.0.5        	1.0.0      	A Helm chart for iplocation api
devops-db/totp-api      	1.0.6        	1.0.0      	A Helm chart for TOTP API Validator

7. Platform Chart (Composition Layer)

Repository:

https://gitlab.devops-db.internal/infrastructure/helms/devops-platform

7.1 Chart.yaml

apiVersion: v2
name: devops-platform
description: DevOps Platform - multi-service deployment
type: application
version: 1.0.0

dependencies:
  - name: devops-api
    version: 1.0.4
    repository: https://nexus.devops-db.internal/repository/helm-devops-db/

  - name: iplocation-api
    version: 1.0.5
    repository: https://nexus.devops-db.internal/repository/helm-devops-db/

  - name: totp-api
    version: 1.0.6
    repository: https://nexus.devops-db.internal/repository/helm-devops-db/

7.2 Resolve Dependencies

helm dependency update .

Hang tight while we grab the latest from your chart repositories...
Update Complete. ⎈Happy Helming!⎈
Saving 3 charts
Downloading devops-api from repo https://nexus.devops-db.internal/repository/helm-devops-db/
Downloading iplocation-api from repo https://nexus.devops-db.internal/repository/helm-devops-db/
Downloading totp-api from repo https://nexus.devops-db.internal/repository/helm-devops-db/
Deleting outdated charts


7.3 Verify

helm dependency list

NAME          	VERSION	REPOSITORY                                                 	STATUS
devops-api    	1.0.4  	https://nexus.devops-db.internal/repository/helm-devops-db/	ok
iplocation-api	1.0.5  	https://nexus.devops-db.internal/repository/helm-devops-db/	ok
totp-api      	1.0.6  	https://nexus.devops-db.internal/repository/helm-devops-db/	ok



8. Deployment

helm upgrade --install devops-platform . -n devops-api --create-namespace

8.1 Result

STATUS: deployed
REVISION: 1

8.2 Pods

kubectl get pods -n devops-api
devops-platform-devops-api
devops-platform-iplocation-api
devops-platform-totp-api

9. Validation


TOTP API

curl "http://totp-api.devops-db.internal/totp?secret=TRKQWWZUCB2ZK6WMAJQ7GVJNVVNGHWF4"
{"totp":"535745"}

IP Location

curl "http://iplocation-api.devops-db.internal/location?ip=149.90.79.135"
{"query":"149.90.79.135","country":"Portugal","city":"Porto"}

DevOps API

curl -s "http://devops-api.devops-db.internal/tags?repo=services/reporting&sort=date" | jq

[
  "v0.1.0",
  "v0.1.1",
  "v0.2.0",
  "v0.2.1",
  "v0.2.2",
  "v0.3.0",
  "v0.3.1",
  "v0.4.0",
  "v0.4.1",
  "v0.5.0",
  "v0.6.0",
  "v0.7.0",
  "v0.8.0",
  "v0.9.0",
  "v0.9.1",
  "v1.0.0",
  "v1.1.0",
  "v1.2.0",
  "v1.2.1",
  "v1.3.0"
]

All services respond correctly, proving that:

✔ dependencies resolved
✔ values applied correctly
✔ templates executed as expected

10. Final Architecture Insights


10.1 Subcharts Are Not YAML Reuse

They are independently versioned components
composed into a system

10.2 Naming Strategy Is Critical

Without consistent naming → collisions
With proper naming → scalable architecture

10.3 Values Drive Everything

Templates are static
Values define behavior

10.4 Platform Chart Role

Defines system composition
Controls versions
Manages shared configuration

Conclusion

This architecture transforms Helm from a simple templating tool into:

a system composition engine for Kubernetes platforms

By combining:

  • Independent service charts
  • A central platform chart
  • A proper naming strategy
  • Structured values

✔ scalability
✔ reusability
✔ maintainability
✔ production readiness

Leave a Reply

Your email address will not be published. Required fields are marked *