Helm Chart Structure
1. Overview
Every EMS Spring Boot service ships its own Helm chart under src/main/helm/. The chart is per-service, not per-environment — values files (managed by ArgoCD, see ArgoCD Deployment Patterns) provide environment-specific overrides.
The chart produces a single Kubernetes Deployment + Service + optional Ingress + ConfigMap + Secret references. No CRDs, no operators, no chart dependencies. Keep it minimal.
This page covers the directory layout, the values schema, template conventions, and how the chart gets published.
See also: Jib Docker Build, ArgoCD Deployment Patterns.
2. Directory Layout
<service>/src/main/helm/
├── Chart.yaml # chart metadata
├── values.yaml # default values (dev-friendly)
└── templates/
├── deployment.yaml # the one Deployment
├── service.yaml # the one Service
├── ingress.yaml # optional, per values.ingress.enabled
├── configmap.yaml # non-sensitive config surfacing values.config.*
├── secret.yaml # optional — if not using existingSecret
├── _helpers.tpl # standard label / selector helpers
└── NOTES.txt # post-install user instructions (brief)
Reference: [admin-service/src/main/helm/](admin-service/src/main/helm/), [registration-portal/src/main/helm/](registration-portal/src/main/helm/).
3. Chart.yaml
apiVersion: v2
name: event-admin-portal
description: Helm chart for the EMS Admin Portal gateway.
type: application
version: 0.0.0
appVersion: 0.0.0
version and appVersion are overwritten at build time by the helm-maven-plugin using project.version. The committed 0.0.0 is a placeholder — never rely on it as the real version.
4. values.yaml Schema
The standard schema (inherited from the JHipster convention, extended for EMS):
config:
profiles: "dev" # comma-separated Spring profiles
existingsecret: "" # name of a pre-existing K8s secret; if set, chart does not create a Secret
db:
url: "" # no jdbc:/r2dbc: prefix
username: ""
password: ""
security:
jwt:
encryptionkey: ""
oauth2:
enabled: false
issuer: ""
clientid: ""
clientsecret: ""
mail:
host: ""
port: 25
from: ""
username: ""
password: ""
protocol: smtp
tls: false
properties.mail.smtp: {}
liquibase:
contexts: "prod"
services:
apikey: "" # admin-service X-API-KEY for portals
eventadminservice: "" # base URL (in-cluster http://prod-event-admin-service)
logging:
level:
ROOT: INFO
otel:
enabled: false
url: ""
hazelcast:
clusterName: ""
serviceName: "" # K8s Service name for peer discovery
image:
repository: docker.io/christhonie/event-admin-portal
pullPolicy: IfNotPresent
# tag defaults to appVersion
imagePullSecrets:
- name: christhonie-docker
ingress:
enabled: false
className: "nginx"
hosts: []
tls: []
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
memory: 1Gi
replicaCount: 1
Keep the schema stable across services — operators should be able to copy a values file between services and mostly see the same keys.
5. Template Conventions
5.1. Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "chart.fullname" . }}
labels: {{- include "chart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels: {{- include "chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels: {{- include "chart.selectorLabels" . | nindent 8 }}
spec:
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 12506
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: {{ .Values.config.profiles | quote }}
{{- if .Values.config.existingsecret }}
- name: APPLICATION_ADMIN_SERVICE_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.config.existingsecret }}
key: apikey
{{- end }}
# ... other env mappings ...
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 30
livenessProbe:
httpGet:
path: /livez
port: http
initialDelaySeconds: 60
resources: {{- toYaml .Values.resources | nindent 12 }}
Key conventions:
-
Port name is
http, regardless of numeric port. -
Readiness probe on
/readyz— a lightweight actuator endpoint that returns 200 once the service is ready to accept traffic. Distinct from/actuator/healthwhich is more expensive. -
Liveness probe on
/livez— even lighter, returns 200 if the JVM is responsive. -
Env vars use Spring’s
SPRING_*binding convention.APPLICATION_*maps toapplication.*config tree.
5.2. Service
Standard ClusterIP service on port 80 → 12506. Name matches the release name so ArgoCD’s naming produces e.g. prod-event-admin-portal (Service) consumed by other in-cluster clients as http://prod-event-admin-portal.
5.3. Ingress
Optional per values.ingress.enabled. When on, defaults to className: nginx with TLS via cert-manager-annotated secrets. Example production values:
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: 10m
hosts:
- host: admin.event.idealogic.co.za
paths:
- path: /
pathType: Prefix
tls:
- secretName: admin-event-idealogic-co-za-tls
hosts:
- admin.event.idealogic.co.za
Multi-tenant portals host tenant-specific subdomains. The hosts list grows with tenant onboarding — managed by ArgoCD values, not by chart changes.
6. Secrets
Two modes:
-
Chart-managed —
values.config.existingsecretis empty; chart creates a Secret fromvalues.config.*fields. Fine for dev; never for prod (secrets in values are secrets in git). -
Pre-existing —
values.config.existingsecret: <name>; operator creates the Secret out-of-band (viakubectl, SealedSecrets, or External Secrets Operator), chart references keys by name. Always used in prod.
Prod secret creation is a runbook item — see ArgoCD Deployment Patterns.
7. helm-maven-plugin Build
<plugin>
<groupId>io.kokuwa.maven</groupId>
<artifactId>helm-maven-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>init</goal>
<goal>lint</goal>
<goal>template</goal>
<goal>package</goal>
</goals>
<configuration>
<chartDirectory>${project.basedir}/src/main/helm</chartDirectory>
<chartVersion>${project.version}</chartVersion>
<appVersion>${project.version}</appVersion>
<outputDirectory>${project.build.directory}/helm</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
mvn package produces target/helm/event-admin-portal-<version>.tgz.
Publishing to a chart repository: push via helm push to OCI registry or upload to a static S3/GCS repo. Current practice: OCI-style push to the same Docker Hub repo used for the image.
8. Chart Versioning
version = project.version at build time. Every push gets a unique chart version. ArgoCD pins to a specific targetRevision per environment; promotion means bumping the revision in the prod ArgoCD Application manifest.
Do not use floating chart versions in prod ArgoCD manifests. Stage may float to latest-dev for convenience; prod is always pinned.
9. admin-portal Specifics
Create admin-portal/src/main/helm/ with:
-
Chart.yaml—name: event-admin-portal, placeholders for version/appVersion -
values.yaml— defaults leaning dev-friendly -
templates/— copy from registration-portal, adjust port to 12506, ingress host pattern toadmin.event.idealogic.co.za
Initial production values (in the ArgoCD manifest, see ArgoCD Deployment Patterns):
-
config.profiles:"prod,kubernetes,api-docs,otlp" -
config.services.eventadminservice:http://prod-event-admin-service -
config.security.oauth2: enabled with the staff IdP client details -
config.otel: enabled with the in-cluster collector URL
Secrets: create Kubernetes secret event-admin-portal with:
-
apikey— the portal’s X-API-KEY -
oidcclientsecret— OIDC client secret -
jwtencryptionkey— not used by admin-portal if it doesn’t mint JWTs itself (admin-service does); keep for future compatibility
10. Reference
| File | Role |
|---|---|
|
Reference chart for a backend service |
|
Reference chart for a portal |
|
Shared template helpers / defaults (if any at parent level) |
|
Production ArgoCD values — full example |