Helm, Kustomize & Package Management
Raw YAML does not scale past a handful of microservices. Helm packages third-party and shared apps as versioned charts with templated values; Kustomize layers environment-specific patches over plain manifests without a templating language. On OpenShift, legacy Template objects and OLM operators extend packaging into full lifecycle management. This chapter covers chart anatomy, production hardening, overlay patterns, when to pick each tool, and OperatorHub install flows.
Helm
Helm is the de facto package manager for Kubernetes. A chart is a directory of templated manifests plus metadata; a release is a chart instance installed into a namespace with a specific values set. Helm 3 is client-only—no cluster-side Tiller—and stores release state in Secrets (default) or ConfigMaps.
Chart structure
A standard chart follows the Helm project layout. Templates live under templates/; default configuration is in values.yaml. Files beginning with _ are not rendered as manifests.
my-app/
├── Chart.yaml # chart metadata (name, version, dependencies)
├── values.yaml # default values — overridden at install/upgrade
├── values.schema.json # optional JSON Schema validation for values
├── charts/ # subchart dependencies (vendored or from helm dependency)
├── templates/
│ ├── _helpers.tpl # named template definitions (include/define)
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── pdb.yaml
│ ├── networkpolicy.yaml
│ ├── rbac.yaml
│ ├── NOTES.txt # post-install instructions (helm install output)
│ └── tests/ # helm test hook pods
└── .helmignore
apiVersion: v2
name: payments-api
description: Payments API — production Helm chart
type: application # application | library
version: 2.4.1 # chart version (semver) — bump on template/values changes
appVersion: "1.18.3" # app version label — informational, not used for upgrades
kubeVersion: ">=1.25.0-0"
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
maintainers:
- name: platform-team
email: platform@example.com
Go templates
Helm renders templates with Go's text/template plus Sprig functions. Values are accessed as .Values.replicaCount; chart metadata as .Chart.Name and .Release.Name. Use {{ include "my-app.fullname" . }} from _helpers.tpl for consistent naming.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "payments-api.fullname" . }}
labels:
{{- include "payments-api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "payments-api.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "payments-api.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "payments-api.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources:
{{- toYaml .Values.resources | nindent 12 }}
Install, upgrade, rollback
helm install creates a release (revision 1). helm upgrade applies a new chart/values set and increments revision. helm rollback reverts to a prior revision without re-running CI—useful for fast incident recovery. Use --atomic to auto-rollback on failure and --wait to block until resources are ready.
stateDiagram-v2 [*] --> Rev1: helm install Rev1 --> Rev2: helm upgrade (new image tag) Rev2 --> Rev3: helm upgrade (values change) Rev3 --> Rev2: helm rollback 2 Rev2 --> [*]: helm uninstall
$ helm repo add bitnami https://charts.bitnami.com/bitnami $ helm repo update $ helm install payments ./charts/payments-api \ --namespace payments --create-namespace \ -f values/prod.yaml --set image.tag=1.18.3 $ helm upgrade payments ./charts/payments-api \ -f values/prod.yaml --atomic --wait --timeout 5m $ helm history payments -n payments $ helm rollback payments 3 -n payments $ helm template payments ./charts/payments-api -f values/prod.yaml | kubectl apply -f - --dry-run=server $ kubectl get secret -n payments -l owner=helm$ helm install payments ./charts/payments-api \ --namespace payments --create-namespace \ -f values/ocp-prod.yaml → ocp-prod.yaml may set route.enabled, securityContextConstraints $ oc get route -n payments $ helm upgrade payments ./charts/payments-api --atomic --wait $ oc adm policy scc-to-user anyuid -z payments-api -n payments → only when chart requires non-default UID; prefer restricted SCC $ helm history payments -n payments
Releases & values override
Release names are unique per namespace. Values merge in order: chart defaults → parent values → -f files (last wins) → --set / --set-string. For GitOps, render with helm template and commit manifests, or use ArgoCD's native Helm source.
| Override mechanism | Use when | Example |
|---|---|---|
| values.yaml | Sensible defaults in the chart | replicaCount: 2 |
| -f env/prod.yaml | Environment-specific config in git | Higher replicas, prod ingress host |
| --set key=val | CI/CD one-off overrides (image tag) | --set image.tag=$GIT_SHA |
| --set-file | Large blobs (certs, config files) | --set-file tls.crt=./cert.pem |
Chart repositories
helm repo add registers an HTTP index (index.yaml). OCI registries (Harbor, ECR, GHCR) are first-class in Helm 3.8+: helm install my-app oci://registry.example.com/charts/my-app --version 1.2.0. Pin chart versions in CI; helm search repo lists available versions.
Helm Secrets & SOPS
Never commit plaintext Secrets to git. Patterns:
- helm-secrets plugin — decrypts SOPS-encrypted value files at render time (helm secrets install -f secrets.prod.yaml)
- External Secrets Operator / Sealed Secrets — chart references ExternalSecret CR; cluster controller materializes Secrets
- Mozilla SOPS — encrypt YAML/JSON with KMS (AWS, GCP, Azure) or age/PGP keys; .sops.yaml defines creation rules
# secrets.prod.yaml — encrypted with sops; safe in git
database:
password: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
sops:
kms:
- arn: arn:aws:kms:us-east-1:123456789:key/abc
created_at: "2026-01-15T10:00:00Z"
enc: AQICAHh...base64...
lastmodified: "2026-01-15T10:00:00Z"
version: 3.9.0
Helm stores release metadata (including rendered values) in cluster Secrets by default. If you pass secrets via --set, they land in the release Secret in plaintext. Use External Secrets, Sealed Secrets, or ensure values in release history are non-sensitive.
Hooks
Annotate resources with helm.sh/hook to run at specific lifecycle points. Hook pods/jobs run outside the normal upgrade order—critical for DB migrations and cache warmers.
| Hook | When it runs | Typical use |
|---|---|---|
| pre-install | Before any resources created | Validate prerequisites, create namespace-scoped RBAC |
| post-install | After all resources deployed | Smoke test Job, register with service discovery |
| pre-upgrade | Before upgrade applies | DB schema migration Job |
| post-upgrade | After upgrade completes | Cache invalidation, integration test |
| pre-delete | Before uninstall | Drain connections, backup snapshot |
| test | helm test <release> | Chart test Pod in templates/tests/ |
Set helm.sh/hook-weight for ordering and helm.sh/hook-delete-policy (hook-succeeded, before-hook-creation) to control hook resource cleanup.
Library charts
Charts with type: library in Chart.yaml provide shared templates only—no installable release. Parent charts import them via dependencies and call {{ include "common.labels" . }}. Bitnami's common chart is the canonical example: standard labels, affinities, image helpers, and compatibility snippets across dozens of application charts.
Helmfile
Helmfile declaratively orchestrates multiple Helm releases—repos, environments, value file layering, and diff/apply in one manifest. Ideal for platform teams managing 20+ third-party charts (ingress, cert-manager, prometheus, external-dns) with per-environment overrides.
repositories:
- name: ingress-nginx
url: https://kubernetes.github.io/ingress-nginx
- name: jetstack
url: https://charts.jetstack.io
environments:
prod:
values:
- environments/prod.yaml
releases:
- name: ingress-nginx
namespace: ingress
chart: ingress-nginx/ingress-nginx
version: 4.10.0
values:
- values/ingress-nginx.yaml
- name: cert-manager
namespace: cert-manager
chart: jetstack/cert-manager
version: v1.14.4
needs:
- ingress/ingress-nginx
values:
- values/cert-manager.yaml
$ helmfile -e prod diff → preview changes before apply (requires helm-diff plugin) $ helmfile -e prod apply $ helmfile -l name=cert-manager destroy$ helmfile -e prod -f helmfile-ocp.yaml apply → ocp overlay may swap Ingress for Route templates $ oc get co ingress
Helm 3 stores each release revision as a Secret (default) named sh.helm.release.v1.<release>.v<N> containing gzipped JSON: chart manifest, rendered templates, values, and status. The API server is the source of truth—not a local ~/.helm cache.
"Helm 2 vs 3?" — Tiller (cluster-admin gRPC server) was removed. RBAC is per-user via kubeconfig. Release secrets live in the install namespace. Three-way merge upgrades use live state, last release manifest, and new manifest.
Writing Production Helm Charts
A chart that deploys on minikube is not a production chart. Production charts encode resource governance, autoscaling, disruption tolerance, network segmentation, RBAC least privilege, and operator-friendly post-install guidance—without forcing consumers to fork templates.
Requests & limits
Always expose resources.requests and resources.limits in values with sensible defaults. Requests drive scheduling and HPA/VPA calculations; limits prevent noisy-neighbor OOM cascades. Document expected CPU/memory profiles in values.yaml comments.
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
# Allow disabling limits for JVM workloads that need burstable memory
resourcesPolicy:
enforceLimits: true
Gate limits behind a value when targeting OpenShift namespaces with LimitRange defaults—your chart should not fight platform-enforced minimums. Expose .Values.resources via toYaml for full override flexibility.
HPA, PDB, NetworkPolicy, RBAC in chart
Ship optional templates gated by values flags. Consumers enable what their cluster supports without editing raw YAML.
| Resource | Values flag | Production rationale |
|---|---|---|
| HPA | autoscaling.enabled | Scale on CPU, memory, or custom metrics (Prometheus adapter) |
| PDB | pdb.enabled | Protect quorum during node drains and cluster upgrades |
| NetworkPolicy | networkPolicy.enabled | Default-deny egress/ingress except declared peers |
| ServiceAccount + RBAC | serviceAccount.create, rbac.create | Dedicated identity; Role not ClusterRole unless required |
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "payments-api.fullname" . }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "payments-api.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
Platform teams often require networkPolicy.enabled: true in their values policy. Charts that ship without NetworkPolicy templates get rejected in PR review—add the template even if default is false.
NOTES.txt
templates/NOTES.txt prints after helm install—the first thing an operator sees. Include: how to reach the app (URL/port-forward), how to check health, where logs live, and links to runbooks.
1. Get the application URL:
{{- if .Values.ingress.enabled }}
https://{{ .Values.ingress.host }}
{{- else }}
export POD=$(kubectl get pods -l app.kubernetes.io/name={{ include "payments-api.name" . }} -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward $POD 8080:8080
echo "Visit http://127.0.0.1:8080"
{{- end }}
2. Check rollout status:
kubectl rollout status deployment/{{ include "payments-api.fullname" . }} -n {{ .Release.Namespace }}
_helpers.tpl
Centralize naming and labels in templates/_helpers.tpl to satisfy recommended labels and avoid 63-character DNS label truncation bugs.
{{- define "payments-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "payments-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "payments-api.labels" -}}
helm.sh/chart: {{ include "payments-api.chart" . }}
{{ include "payments-api.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
Semver: chart version vs app version
version in Chart.yaml is the chart semver—bump on any template, default values, or dependency change. appVersion is informational (container image tag label)—it does not trigger upgrades by itself. CI should bump version on every chart publish; tie appVersion to the application release tag.
| Field | Bump when | Consumed by |
|---|---|---|
| version | Template fix, new optional resource, dependency pin | Helm repos, helm.sh/chart label |
| appVersion | Application release (v1.19.0) | app.kubernetes.io/version label, docs |
Run helm lint ./charts/payments-api and chart-testing (ct lint) in CI. Add kubeconform or helm template | kubectl apply --dry-run=server to catch deprecated API versions before deploy.
Highly parameterized charts (50+ values keys) maximize flexibility but hurt discoverability. Group values in values.yaml with comments; ship values.schema.json for IDE validation. Prefer sane defaults over required template panics for optional features.
Kustomize
Kustomize is built into kubectl (kubectl apply -k). It composes plain Kubernetes YAML without templating—bases define shared manifests, overlays patch per environment. Ideal for first-party services where teams already own the Deployment YAML in git.
kustomization.yaml fields
The kustomization.yaml file at each directory root declares how to build a set of resources.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payments-prod
resources:
- ../../base
- ingress.yaml
commonLabels:
env: prod
team: payments
commonAnnotations:
app.kubernetes.io/managed-by: kustomize
namePrefix: prod-
images:
- name: registry.example.com/payments-api
newTag: v1.18.3
configMapGenerator:
- name: payments-config
behavior: merge
literals:
- LOG_LEVEL=info
- FEATURE_FLAGS=ledger-v2
secretGenerator:
- name: payments-tls
files:
- tls.crt
- tls.key
type: kubernetes.io/tls
patches:
- path: patch-replicas.yaml
- target:
kind: Deployment
name: payments-api
patch: |-
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 2Gi
replicas:
- name: payments-api
count: 6
| Field | Purpose |
|---|---|
| resources | Local files, remote URLs, or other kustomization directories (bases) |
| bases (deprecated) | Use resources: [../../base] instead |
| patches / patchesStrategicMerge / patchesJson6902 | Modify resources without copying full YAML |
| images | Rewrite image tags/digests without editing Deployment YAML |
| configMapGenerator / secretGenerator | Generate ConfigMaps/Secrets from files or literals |
| replicas | Override replica count for named Deployments |
| components | Reusable optional feature packs (Kustomize components) |
| helmCharts (v5.1+) | Embed Helm charts natively in kustomization (rendered at build) |
Base + overlays
The canonical layout keeps a base with environment-agnostic manifests and overlays per environment (dev, staging, prod). Overlays reference the base and apply deltas only—DRY without a templating language.
flowchart TB
subgraph base["base/"]
B1["deployment.yaml"]
B2["service.yaml"]
B3["kustomization.yaml"]
end
subgraph dev["overlays/dev/"]
D1["kustomization.yaml"]
D2["patch-resources.yaml"]
end
subgraph prod["overlays/prod/"]
P1["kustomization.yaml"]
P2["patch-replicas.yaml"]
P3["ingress.yaml"]
end
dev --> base
prod --> base
prod -->|"kubectl apply -k overlays/prod"| API["API server"]
deploy/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml # resources: [deployment.yaml, service.yaml]
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patch-replicas.yaml
└── prod/
├── kustomization.yaml
├── patch-replicas.yaml
└── ingress.yaml
Patches: strategic merge vs JSON6902
Strategic merge patches use partial resource YAML—Kustomize merges by apiVersion/kind/name using the OpenAPI schema's merge strategy (e.g. append to lists with $patch: replace). JSON6902 (RFC 6902) patches target precise JSON paths—better for surgical edits (replace one env var, one limit field).
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
spec:
replicas: 6
template:
spec:
containers:
- name: payments-api
env:
- name: LOG_LEVEL
value: warn
# in kustomization.yaml patches:
- target:
group: apps
version: v1
kind: Deployment
name: payments-api
patch: |-
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.monitoring:4317
Strategic merge on lists (env, volumeMounts, containers) can duplicate entries if patch keys don't match merge keys. Use JSON6902 replace on indexed paths, or patchesJson6902 with $patch: delete before add.
Images transformer
The images field rewrites container image references across all resources—CI can inject newTag or digest without sed scripts. Pair with Kustomize's replacements or ArgoCD Image Updater for automated tag promotion.
Generators & hash suffix
configMapGenerator and secretGenerator create ConfigMaps/Secrets from files or literals. By default, Kustomize appends a content hash suffix to names (e.g. payments-config-7f3k2m) so Pod specs change when content changes—triggering rolling restarts. Disable with generatorOptions.disableNameSuffixHash: true only when you accept manual rollout triggers.
The hash suffix is computed from generated content. Kustomize rewrites references in Deployments to point at the hashed name automatically via configMapGenerator name references—no manual envFrom updates.
kubectl apply -k
Build and apply in one step, or build to stdout for GitOps commit pipelines:
$ kubectl kustomize deploy/overlays/prod > /tmp/rendered.yaml $ kubectl apply -k deploy/overlays/prod $ kubectl diff -k deploy/overlays/prod → requires diff plugin; preview before apply $ kustomize build deploy/overlays/prod | kubeconform -summary $ kubectl get deploy -n payments-prod -l env=prod$ oc apply -k deploy/overlays/ocp-prod → ocp-prod overlay adds Route, SCC-compatible securityContext $ oc kustomize deploy/overlays/ocp-prod | oc apply -f - --dry-run=server $ oc get route payments-api -n payments-prod
ArgoCD and Flux natively understand Kustomize—point the Application/ Kustomization CR at deploy/overlays/prod. For PR previews, kustomize edit set image in CI then kubectl apply -k to ephemeral namespaces.
Helm vs Kustomize
Helm and Kustomize solve different problems. Mature platforms use both: Helm for curated third-party packages, Kustomize for first-party microservices—sometimes chained in a render pipeline.
| Dimension | Helm | Kustomize |
|---|---|---|
| Templating | Go templates + values.yaml | Patch/transform plain YAML—no logic language |
| Packaging unit | Chart (versioned tarball/OCI artifact) | Directory tree (base + overlays) |
| Lifecycle | Release revisions, rollback, hooks | Git-native; rollback = git revert + re-apply |
| Best for | nginx-ingress, Prometheus, Postgres operators | Your team's Deployments, Services, ConfigMaps |
| Ecosystem | Artifact Hub, OCI registries, helmfile | Built into kubectl; ArgoCD/Flux native |
| Learning curve | Sprig functions, subcharts, hooks | Lower—if you already know K8s YAML |
When to use each
- Helm for third-party — You don't own the upstream manifests. Consume versioned charts from Artifact Hub with values overrides. Upgrades are helm upgrade with changelog review.
- Kustomize for in-house — Your services are already plain YAML in git. Overlays express env deltas without forking a chart. Developers read real Deployment specs, not {{ .Values.foo }}.
- Helm when you need hooks & packaging — DB migration Jobs on upgrade, chart dependencies, OCI distribution to customers.
- Kustomize when GitOps purity matters — Flux/ArgoCD render exactly what's in git; no cluster-side release state beyond applied resources.
Platform bootstrap: helmfile installs cert-manager, ingress, external-dns, monitoring stack. Application teams ship deploy/overlays/prod Kustomize dirs consumed by ArgoCD Applications. Never Helm-wrap your own microservice unless you're publishing it to multiple external tenants.
helm template | kustomize pattern
Combine both: render third-party Helm charts to static YAML, then Kustomize-patch for cluster-specific labels, NetworkPolicies, or OCP Routes. Commit the rendered output to git for auditable GitOps, or render in CI and push to a manifests repo.
flowchart LR HC["Helm chart\n(third-party)"] --> HT["helm template"] HT --> RAW["rendered/"] RAW --> KU["kustomize overlay\n(cluster patches)"] KU --> GIT["git commit"] GIT --> ARGO["ArgoCD / Flux"] ARGO --> CLUSTER["Cluster"]
$ helm template prometheus prometheus-community/kube-prometheus-stack \ -f values/prometheus.yaml \ --namespace monitoring > platform/prometheus-rendered.yaml $ # Move into kustomize resources/ then overlay cluster labels $ kubectl kustomize platform/overlays/prod | kubectl apply -f - --dry-run=server $ kubectl apply -k platform/overlays/prod$ helm template prometheus prometheus-community/kube-prometheus-stack \ -f values/prometheus-ocp.yaml > platform/prometheus-rendered.yaml $ oc apply -k platform/overlays/ocp-prod → ocp overlay adds cluster-monitoring label requirements
Rendering Helm to static YAML loses helm rollback and hook execution on upgrade—you own migration Jobs in Kustomize or CI. The trade is worth it when GitOps auditability and PR-reviewable manifests outweigh Helm's release ergonomics.
"Why not Helm everything?" — Helm charts hide complexity behind values but become opaque at scale. Kustomize keeps manifests readable for code review. Production answer: Helm for platform dependencies, Kustomize for apps, render pipeline when you need both.
OpenShift Templates & Operators
Before Helm dominated, OpenShift used Template objects—parameterized manifests processed by oc process. Today, OperatorHub and the Operator Lifecycle Manager (OLM) install operators that reconcile CRDs continuously— the preferred model for databases, messaging, and storage on OCP.
oc process — legacy Templates
Template is an OpenShift-specific API (template.openshift.io/v1). Parameters substitute into embedded objects. Still found in quick-start examples and legacy CI; new work should prefer Helm or Kustomize.
apiVersion: template.openshift.io/v1
kind: Template
metadata:
name: payments-api
parameters:
- name: IMAGE_TAG
description: Container image tag
required: true
- name: REPLICAS
value: "2"
objects:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
spec:
replicas: ${{REPLICAS}}
template:
spec:
containers:
- name: api
image: image-registry.openshift-image-registry.svc:5000/payments/api:${{IMAGE_TAG}}
$ # Templates are OpenShift-specific — not available on vanilla K8s $ kubectl api-resources | grep -i template → no template.openshift.io on vanilla clusters$ oc process -f template.yaml -p IMAGE_TAG=v1.18.3 -p REPLICAS=3 | oc apply -f - $ oc get template -n openshift $ oc new-app --template=payments-api -p IMAGE_TAG=v1.18.3 → shorthand wrapper around process + apply
Templates lack Helm's release history and Kustomize's overlay composability. Red Hat documentation steers new projects to Helm charts or GitOps. Templates remain in the catalog for backward compatibility and S2I quick starts.
OperatorHub
OperatorHub is the OpenShift console and CLI marketplace for operators. Sources include: Red Hat-operated catalogs (platform + certified partners), community operators, and custom catalogs you publish. Each operator is distributed as a ClusterServiceVersion (CSV) bundle in an index image.
OLM: CatalogSource, Subscription, InstallPlan, CSV
OLM reconciles operator installs through a pipeline of CRs. Understanding this chain is essential for debugging "operator stuck in InstallPlanPending" incidents.
flowchart TB CS["CatalogSource\n(index image gRPC)"] --> PKG["Package manifest"] SUB["Subscription\n(channel, source)"] --> IP["InstallPlan\n(approved)"] IP --> CSV["ClusterServiceVersion\n(operator + CRDs + RBAC)"] CSV --> DEP["Operator Deployment"] DEP --> CR["Custom Resources\n(user intent)"] CR --> DEP
| CR | Role |
|---|---|
| CatalogSource | Points to operator index image (gRPC registry); feeds PackageManifest to console |
| Subscription | Declares desired operator package, channel, and catalog; triggers InstallPlan |
| InstallPlan | Lists CSV and dependencies to install; requires approval if manual strategy |
| ClusterServiceVersion | Operator metadata, owned CRDs, deployment spec, permissions, webhook defs |
| OperatorGroup | Scopes operators to namespaces; defines targetNamespaces |
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: postgres-operator
namespace: operators
spec:
channel: stable
name: crunchy-postgres-operator
source: redhat-operators
sourceNamespace: openshift-marketplace
installPlanApproval: Automatic
startingCSV: postgresoperator.v5.5.0
Install flow
- Cluster admin creates OperatorGroup in target namespace(s)
- Create Subscription referencing catalog source and channel
- OLM resolves CSV from CatalogSource gRPC; creates InstallPlan
- InstallPlan approved (Automatic or Manual) → OLM applies CSV, CRDs, RBAC, operator Deployment
- CSV phase transitions: Installing → Succeeded
- User creates application CRs (e.g. PostgresCluster) → operator reconciles operands
$ # OLM is OpenShift / Operator Framework — limited on vanilla K8s $ kubectl get csv -A 2>/dev/null || echo "Install OLM via operator-framework" $ operator-sdk run bundle quay.io/operator/bundle:v1.0.0$ oc get packagemanifest -n openshift-marketplace | grep postgres $ oc apply -f operatorgroup.yaml -f subscription.yaml $ oc get installplan -n operators $ oc get csv -n operators → PHASE must be Succeeded; check STATUS for requirement failures $ oc describe csv postgresoperator.v5.5.0 -n operators $ oc get catalogsource -n openshift-marketplace $ oc get subscription postgres-operator -n operators -o yaml
Red Hat certified operators
Red Hat OpenShift Certified operators pass interoperability, security, and support lifecycle tests. They appear in the redhat-operators CatalogSource with predictable upgrade paths tied to OCP versions. Certified operators include Crunchy Postgres, AMQ Streams, OpenShift GitOps (ArgoCD), OpenShift Pipelines (Tekton), and ODF (storage).
| Catalog | Content | Support |
|---|---|---|
| redhat-operators | Red Hat + certified ISV operators | Red Hat support when consumed via supported OCP subscription |
| redhat-marketplace | Commercial marketplace listings | Vendor + Red Hat billing integration |
| community-operators | Community-maintained | Best-effort; verify before production |
| Custom CatalogSource | Your index image (internal operators) | Your team |
Pin startingCSV or use installPlanApproval: Manual in change-controlled environments. OCP upgrades may auto-bump certified operator channels—review oc get subscription -o wide before platform upgrades.
"Helm vs Operator?" — Helm installs static manifests; upgrades are template re-renders. Operators watch CRs and reconcile continuously (backup, failover, version upgrades). Use operators for stateful platform services (DB, Kafka, storage); Helm/Kustomize for stateless apps.
Production OCP clusters typically run: GitOps operator (ArgoCD) via OLM, cert-manager or OpenShift cert stack, observability via Cluster Monitoring Operator, and data services (Postgres/Kafka) via certified operators—not raw Helm for stateful tiers.