Skip to content

Helm Charts

Helm charts package your module for deployment to Kubernetes. This guide covers chart creation and customization.

Chart Structure

charts/my-module/
├── Chart.yaml           # Chart metadata
├── values.yaml          # Default values
├── templates/
│   ├── _helpers.tpl     # Template helpers
│   ├── deployment.yaml  # Deployment manifest
│   ├── service.yaml     # Service manifest
│   ├── serviceaccount.yaml
│   ├── rbac.yaml        # RBAC rules
│   └── NOTES.txt        # Post-install notes
└── crds/                # Custom Resource Definitions (optional)
    ├── tinymodule.yaml
    ├── tinynode.yaml
    └── tinysignal.yaml

Chart.yaml

yaml
apiVersion: v2
name: my-module
description: My TinySystems module
type: application
version: 1.0.0        # Chart version
appVersion: "1.0.0"   # Module version

keywords:
  - tinysystems
  - workflow
  - automation

home: https://github.com/myorg/my-module
sources:
  - https://github.com/myorg/my-module

maintainers:
  - name: My Name
    email: me@example.com

dependencies: []

values.yaml

yaml
# Image configuration
image:
  repository: ghcr.io/myorg/my-module
  tag: ""  # Defaults to appVersion
  pullPolicy: IfNotPresent

imagePullSecrets: []

# Replica count
replicaCount: 2

# Module settings
module:
  name: my-module
  namespace: tinysystems

# Service configuration
service:
  type: ClusterIP
  grpcPort: 50051

# Resource limits
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

# Pod settings
podAnnotations: {}
podSecurityContext: {}
securityContext: {}

# Node selection
nodeSelector: {}
tolerations: []
affinity: {}

# Service account
serviceAccount:
  create: true
  name: ""
  annotations: {}

# RBAC
rbac:
  create: true

# Leader election
leaderElection:
  enabled: true
  leaseDuration: 15s
  renewDeadline: 10s
  retryPeriod: 2s

# Observability
metrics:
  enabled: true
  port: 8080

# Probes
probes:
  liveness:
    enabled: true
    initialDelaySeconds: 10
    periodSeconds: 10
  readiness:
    enabled: true
    initialDelaySeconds: 5
    periodSeconds: 5

Templates

deployment.yaml

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-module.fullname" . }}
  labels:
    {{- include "my-module.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-module.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "my-module.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "my-module.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: grpc
              containerPort: {{ .Values.service.grpcPort }}
            {{- if .Values.metrics.enabled }}
            - name: metrics
              containerPort: {{ .Values.metrics.port }}
            {{- end }}
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: MODULE_NAME
              value: {{ .Values.module.name }}
            {{- if .Values.leaderElection.enabled }}
            - name: LEADER_ELECTION_ENABLED
              value: "true"
            - name: LEADER_ELECTION_LEASE_DURATION
              value: {{ .Values.leaderElection.leaseDuration }}
            - name: LEADER_ELECTION_RENEW_DEADLINE
              value: {{ .Values.leaderElection.renewDeadline }}
            - name: LEADER_ELECTION_RETRY_PERIOD
              value: {{ .Values.leaderElection.retryPeriod }}
            {{- end }}
          {{- if .Values.probes.liveness.enabled }}
          livenessProbe:
            httpGet:
              path: /healthz
              port: metrics
            initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
            periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
          {{- end }}
          {{- if .Values.probes.readiness.enabled }}
          readinessProbe:
            httpGet:
              path: /readyz
              port: metrics
            initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
            periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

service.yaml

yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-module.fullname" . }}
  labels:
    {{- include "my-module.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.grpcPort }}
      targetPort: grpc
      protocol: TCP
      name: grpc
    {{- if .Values.metrics.enabled }}
    - port: {{ .Values.metrics.port }}
      targetPort: metrics
      protocol: TCP
      name: metrics
    {{- end }}
  selector:
    {{- include "my-module.selectorLabels" . | nindent 4 }}

rbac.yaml

yaml
{{- if .Values.rbac.create }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: {{ include "my-module.fullname" . }}
rules:
  - apiGroups: ["operator.tinysystems.io"]
    resources: ["tinynodes", "tinymodules", "tinysignals"]
    verbs: ["*"]
  - apiGroups: [""]
    resources: ["services", "configmaps", "secrets"]
    verbs: ["*"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["*"]
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: {{ include "my-module.fullname" . }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: {{ include "my-module.fullname" . }}
subjects:
  - kind: ServiceAccount
    name: {{ include "my-module.serviceAccountName" . }}
    namespace: {{ .Release.Namespace }}
{{- end }}

_helpers.tpl

yaml
{{/*
Expand the name of the chart.
*/}}
{{- define "my-module.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "my-module.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "my-module.labels" -}}
helm.sh/chart: {{ include "my-module.chart" . }}
{{ include "my-module.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "my-module.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-module.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Service account name
*/}}
{{- define "my-module.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "my-module.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Installation

Install Chart

bash
# From local directory
helm install my-module ./charts/my-module

# With custom values
helm install my-module ./charts/my-module -f custom-values.yaml

# Override specific values
helm install my-module ./charts/my-module \
  --set replicaCount=3 \
  --set image.tag=1.0.0

Upgrade

bash
helm upgrade my-module ./charts/my-module --set image.tag=1.1.0

Uninstall

bash
helm uninstall my-module

Packaging

Package Chart

bash
helm package ./charts/my-module
# Creates my-module-1.0.0.tgz

Publish to Repository

bash
# Add to Helm repo
helm repo index . --url https://charts.example.com

# Push to OCI registry
helm push my-module-1.0.0.tgz oci://registry.io/charts

Best Practices

1. Use Semantic Versioning

yaml
# Chart.yaml
version: 1.0.0      # Chart version - bump for chart changes
appVersion: "1.2.3" # Module version

2. Provide Sensible Defaults

yaml
# values.yaml
replicaCount: 2
resources:
  requests:
    cpu: 100m
    memory: 128Mi

3. Document Values

yaml
# values.yaml with comments
# -- Number of replicas
replicaCount: 2

# -- Container image settings
image:
  # -- Image repository
  repository: ghcr.io/myorg/my-module
  # -- Image tag (defaults to appVersion)
  tag: ""

4. Test Chart

bash
# Lint
helm lint ./charts/my-module

# Template locally
helm template my-module ./charts/my-module

# Dry run
helm install my-module ./charts/my-module --dry-run

Next Steps

Build flow-based applications on Kubernetes