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.

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/health which 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 to application.* 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:

  1. Chart-managedvalues.config.existingsecret is empty; chart creates a Secret from values.config.* fields. Fine for dev; never for prod (secrets in values are secrets in git).

  2. Pre-existingvalues.config.existingsecret: <name>; operator creates the Secret out-of-band (via kubectl, 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.

7.1. OCI chart push

helm push target/helm/event-admin-portal-2.3.31-RELEASE.tgz \
    oci://registry-1.docker.io/christhonie

ArgoCD’s Application spec references the chart as repoURL: registry-1.docker.io, chart: christhonie/event-admin-portal, targetRevision: 2.3.31-RELEASE.

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.yamlname: 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 to admin.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

admin-service/src/main/helm/

Reference chart for a backend service

registration-portal/src/main/helm/

Reference chart for a portal

parent-pom/src/main/helm/

Shared template helpers / defaults (if any at parent level)

~/dev/idl-xnl-jhb-rc01/argocd/event-admin-service-prod.yml

Production ArgoCD values — full example

12. Change History

Date Change

2026-04-24

Initial draft. Grounded in admin-service and registration-portal Helm charts.