DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

One Helm template, eight Deployments: how range over values.yaml collapses your k8s YAML

Hand-write the Kubernetes manifests for a microservice platform and you end up with a pile of near-identical files: eight Deployments that differ only in name, image, port, replicas, and a couple of env values, plus eight matching Services. That duplication is exactly what Helm removes. The trick is to split the manifests into two halves — the shape goes into templates, the data goes into values.yaml — and let one template range over the data.

The data half: a services map

values.yaml holds everything the flat YAML used to hard-code, now as values. The star is the services: map, keyed by service name, with the shared probes factored out once instead of repeated eight times:

probes:                       # ONE copy, stamped on all 8 apps
  readiness: { path: /actuator/health/readiness, initialDelaySeconds: 10, periodSeconds: 10 }
  liveness:  { path: /actuator/health/liveness,  initialDelaySeconds: 20, periodSeconds: 15 }

services:                     # ← the map the templates range over
  order-service:
    image: orderhub/order-service
    tag: "0.1.0"
    port: 8082
    replicas: 2
    profile: "prod,k8s"
    resources: { requests: {cpu: 250m, memory: 448Mi}, limits: {cpu: "1", memory: 768Mi} }
  # + config-server, eureka-server, api-gateway, inventory, payment, shipping, notification
Enter fullscreen mode Exit fullscreen mode

The shape half: one range renders all eight

This is the heart of the templatization. {{- range $name, $svc := .Values.services }} loops the entire Deployment block once per service:

{{- range $name, $svc := .Values.services }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ $name }}
  labels:
    {{- include "orderhub.selectorLabels" (dict "name" $name) | nindent 4 }}
    {{- include "orderhub.commonLabels" $ | nindent 4 }}
spec:
  replicas: {{ $svc.replicas }}
  template:
    spec:
      containers:
        - name: {{ $name }}
          image: "{{ include "orderhub.image" (dict "svc" $svc "root" $) }}"
          ports: [ { containerPort: {{ $svc.port }} } ]
          envFrom:
            - configMapRef: { name: {{ $.Values.config.name }} }
            - secretRef:    { name: {{ $.Values.secrets.name }} }
          readinessProbe: { httpGet: { path: {{ $.Values.probes.readiness.path }}, port: {{ $svc.port }} } }
          livenessProbe:  { httpGet: { path: {{ $.Values.probes.liveness.path }},  port: {{ $svc.port }} } }
{{- end }}
Enter fullscreen mode Exit fullscreen mode

Two Helm mechanics make it work. Inside a range the dot rebinds to the current service, so you reach the release/chart/global values through $ — the root — as in $.Values.config.name and $.Values.probes, and the per-item values through $svc / $name. The Day-43 probes are kept verbatim but parameterised: their paths come from .Values.probes, their port from $svc.port. A service.yaml uses the identical loop to render all eight ClusterIP Services, their selectors sharing the Deployment's labels via a helper so they can never drift.

The new front door: an Ingress that routes by path

The flat manifests left every Service ClusterIP — internal only. The Ingress finally opens the stack to the world, gated by a single value:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
  ingressClassName: {{ .Values.ingress.className }}     # nginx
  defaultBackend:                                       # catch-all → gateway
    service: { name: api-gateway, port: { number: 8080 } }
  rules:
    - host: {{ .Values.ingress.host | quote }}          # orderhub.local
      http:
        paths:
          {{- range .Values.ingress.routes }}
          - path: {{ .path }}                           # /api/orders, /api/inventory, …
            pathType: {{ .pathType | default "Prefix" }}
            backend:
              service: { name: {{ .service }}, port: { number: {{ .port }} } }
          {{- end }}
{{- end }}
Enter fullscreen mode Exit fullscreen mode

Each /api/* prefix maps to its service by Prefix match; anything unmatched falls through to api-gateway as the default backend. Because the whole object is wrapped in {{- if .Values.ingress.enabled }}, flipping the front door off is a one-value --set, not YAML surgery.

The payoff

helm install orderhub ./helm/orderhub renders 18 objects from about seven template files, and retuning anything is now a value override:

helm upgrade orderhub ./helm/orderhub --set services.order-service.replicas=4
helm rollback orderhub 1 -n orderhub          # undo, atomically
Enter fullscreen mode Exit fullscreen mode

The topology hasn't changed from the hand-written manifests — but it's now generated, versioned, and roll-back-able as a single release.

Run helm install and watch one template range over the services map, then curl the Ingress to see it route, live at: https://dev48v.infy.uk/orderhub/day44-helm-ingress.html

Top comments (0)