Helmsman Deep Dive: Declarative Helm at Scale
Stop scripting Helm. Start declaring it like terraform.
Helmsman Deep Dive: Declarative Helm at Scale
Stop scripting Helm. Start declaring it like terraform.

You’ve automated your Kubernetes deployments with Helm. Now you have 47
helm upgrade --installcommands scattered across shell scripts, CI pipelines, and tribal knowledge Slack messages. Congratulations — you've built technical debt at scale.
There’s a better way. It’s called Helmsman, and it treats your Helm releases the way Terraform treats infrastructure: as a desired state to be declared, compared, and applied — not a sequence of imperative commands to be executed and forgotten.
This is not a quickstart. This is the deep dive that turns you from a Helmsman user into a Helmsman architect.
1. 🧠 The Mental Model (Most Important Section)
Don’t skim this. Everything else in the article depends on you internalizing this model first.

Helmsman is a Desired State Engine
If you’ve used Terraform, you already understand the paradigm:
DECLARE what you want → COMPARE with what exists → PLAN the diff → APPLY the changes
Helmsman applies this exact model to Helm releases. Not to Kubernetes resources directly — to Helm releases as the unit of abstraction.

Unlike helm upgrade --install scripts that blindly execute on every CI run, Helmsman only acts on the delta between your declared state and the reality in the cluster.
The Three Pillars of the Mental Model
Pillar 1: The Desired State File (DSF) is truth.
Your DSF is the single authoritative description of the Helm releases it manages. Helmsman uses the context field to scope its ownership. If a release is in the DSF, it enforces it. If it's not in the DSF but exists in the cluster, Helmsman ignores it (unless it shares the same context). It doesn't blindly wipe your cluster.
Pillar 2: Helmsman reconciles, it doesn’t script.
Helmsman reads the DSF, queries the live Helm state, computes the difference, and builds a minimal execution plan. If your release is already at the right version with the right values — Helmsman does nothing. Under the hood, it seamlessly orchestrates both helm and kubectl apply (for hooks) to make the state a reality.
Pillar 3: Idempotency is a guarantee, not a coincidence. Run Helmsman ten times against an unchanged DSF. The cluster state stays identical. This makes it safe to run continuously — in CI/CD, on a schedule, or in a GitOps loop.
Why This Matters
With imperative Helm scripts, state drift is invisible. A manual helm upgrade run by an engineer last Tuesday isn't tracked anywhere. With Helmsman, the DSF is the state. If reality diverges from the DSF, the next run fixes it automatically.
This is the same mental shift that made Terraform revolutionary. Helmsman brings it to Helm.
Installation
# Set desired version (see latest release link below)
VERSION="4.0.5"
# on Linux
curl -L https://github.com/mkubaczyk/helmsman/releases/download/v${VERSION}/helmsman_${VERSION}_linux_amd64.tar.gz | tar zx
# on MacOS
curl -L https://github.com/mkubaczyk/helmsman/releases/download/v${VERSION}/helmsman_${VERSION}_darwin_amd64.tar.gz | tar zx
mv helmsman /usr/local/bin/helmsman
#You can also install Helmsman using Homebrew
brew install helmsman
2. ⚙️ Internal Architecture
Understanding Helmsman’s internals helps you predict its behavior and debug unexpected plan outputs.

Key Internal Components
DSF Loader — Parses YAML/TOML, resolves environment variables (prefixed with $ or ${}), and validates the schema. Supports local files, URLs, and cloud storage paths (S3/GCS).
Secrets Resolver — Before planning, decrypts secrets using eyaml, Vault, or environment variables. The resolved values are never written to disk in plaintext.
State Comparator — Runs helm list across all namespaces to build a live state map. Compares each app in the DSF against live state:
- Release exists + version matches + values hash matches → No-op
- Release exists + version differs → Upgrade
- Release exists +
enabled: false→ Delete - Release does not exist +
enabled: true→ Install - Release in a protected namespace → Skip
Plan Builder — Assembles operations into an ordered list respecting priority fields. Lower priority value = higher execution order. Operations at the same priority are candidates for parallelism.
Execution Engine — Runs the plan. Calls helm install, helm upgrade, helm delete, and optionally kubectl apply for hooks. Supports --parallel for concurrent operations at equal priority levels.
3. 📄 Desired State File (DSF) — Deep Breakdown
The DSF is the heart of Helmsman. It has six top-level sections:
context: production-cluster # Unique identifier for this DSF
metadata: # Human-readable labels (key/value, supports env vars)
certificates: # Optional connection certs (caCrt, caKey)
settings: # Cluster connection + global behavior
namespaces: # Namespace declarations + policies
helmRepos: # Chart repository definitions
preconfiguredHelmRepos: # Repos already added to Helm (e.g. via out-of-band basic auth)
appsTemplates: # YAML anchors for DRY app configurations
apps: # The releases you want to manage

Helmsman DSF Structure
context
A unique string identifier for the DSF. Critical for multi-cluster/multi-DSF setups — Helmsman uses context to scope which releases it “owns”. If two DSFs have different contexts, they manage non-overlapping sets of releases. Never share a context between two DSFs that run independently.
Operational Tip: If you rename a context, Helmsman will think the old releases are gone and try to delete them. Use the
--migrate-contextCLI flag to safely rename a context without replacing active releases.
settings
settings:
kubeContext: "prod-eks" # kubectl context to use
globalMaxHistory: 10 # Helm max release history across all apps
reverseDelete: false # If true, delete in reverse priority order
slackWebhook: "$SLACK_WEBHOOK" # Post plan/apply results to Slack
msTeamsWebhook: "$TEAMS_URL" # Same for Microsoft Teams
eyamlEnabled: true # Enable Hiera eyaml secret decryption
eyamlPrivateKeyPath: "keys/private.pem"
eyamlPublicKeyPath: "keys/public.pub"
vaultEnabled: false # Enable HashiCorp Vault secret injection
skipPendingApps: true # Skip apps in pending/upgrading state
storageBackend: "secret" # Use Secrets instead of ConfigMaps for Helm state
globalHooks: # Hooks applied to every release
successCondition: "Complete"
deleteOnSuccess: true
postInstall: "init-job.yaml"
namespaces
Namespaces are first-class citizens. Helmsman creates them if they don’t exist and enforces policies:
namespaces:
production:
protected: true # Prevents ANY changes to releases in this NS
labels:
env: "prod"
team: "platform"
annotations:
iam.amazonaws.com/role: "dynamodb-reader"
limits:
- type: Container
default:
cpu: "300m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
- type: Pod
max:
memory: "512Mi"
staging:
protected: false
quotas:
limits.cpu: "20"
limits.memory: "40Gi"
pods: 50
Protected namespaces are a powerful guardrail. When
protected: true, Helmsman will never install, upgrade, or delete any release in that namespace — even if the DSF instructs it to. This is a last-resort safeguard against accidental production changes.
Before we continue
If you found this story insightful, …
👏 Clap 50 times (yes, you can, simply hold the button), it will help me a lot. Medium’s algorithm favors this, increasing visibility to others who then discover the article.
🔔 Follow me on Medium and subscribe to get my latest articles straight to your inbox.
helmRepos
helmRepos:
ingress-nginx: "https://kubernetes.github.io/ingress-nginx"
cert-manager: "https://charts.jetstack.io"
argo: "https://argoproj.github.io/argo-helm"
# Private repos:
myS3repo: "s3://my-private-bucket/charts"
myGCSrepo: "gs://my-gcs-bucket/charts"
# Authenticated:
private: "https://$REPO_USER:$REPO_PASS@charts.mycompany.com"
preconfiguredHelmRepos:
# Repos configured out-of-band (useful if you don't want credentials in the DSF)
- myHighlySecureRepo
appsTemplates (YAML Only)
A powerful DRY feature for when you manage dozens of similar apps. You can define YAML anchors and reuse them:
appsTemplates:
default: &template
namespace: "platform"
wait: true
protected: false
test: true
apps:
my-app:
<<: *template # Inherits namespace, wait, protected, test
chart: "myrepo/my-app"
version: "1.0.0"
apps — The Core
Each key under apps becomes a Helm release name:
apps:
cert-manager:
namespace: "platform" # Must match a key in namespaces
enabled: true # false = delete this release
chart: "cert-manager/cert-manager"
version: "v1.14.4"
priority: -10 # Runs first (lower = higher priority)
wait: true # Block until all pods ready
timeout: 300 # Seconds to wait
protected: false # Protect this individual release
noHooks: false
maxHistory: 5 # Override globalMaxHistory for this release
postRenderer: "kustomize" # Helm 4 plugin / Helm 3 executable for post-rendering
helmFlags: # Added to `helm install/upgrade` (NOT `helm diff`)
- "--disable-openapi-validation"
helmDiffFlags: # Added ONLY to `helm diff`
- "--disable-openapi-validation"
valuesFile: "values/cert-manager.yaml"
# or multiple:
valuesFiles:
- "values/cert-manager-base.yaml"
- "values/cert-manager-prod.yaml"
secretsFile: "secrets/cert-manager-secrets.yaml" # helm-secrets encrypted
set: # Inline value overrides (env vars supported)
installCRDs: "true"
global.logLevel: "$LOG_LEVEL"
setString: # Force string type (uses --set-string)
image.tag: "v1.14.4"
hooks:
preInstall: "crds/cert-manager-crds.yaml" # kubectl apply before install
postInstall: "jobs/verify-cert-manager.yaml"
preUpgrade: "jobs/backup-certs.yaml"
successCondition: "Complete"
successTimeout: "120s"
deleteOnSuccess: true
ingress-nginx:
namespace: "platform"
enabled: true
chart: "ingress-nginx/ingress-nginx"
version: "4.9.1"
priority: -5 # Runs after cert-manager (higher value = lower priority)
valuesFile: "values/ingress-nginx.yaml"
Priority System: A Deep Look
priority: -10 → priority: -5 → priority: 0 (default)
cert-manager ingress-nginx your-app
[FIRST] [SECOND] [LAST]
Key rules:
- Use negative values to control order. Smaller numbers = higher priority = runs earlier.
- If omitted, apps default to priority 0 (which runs last).
- Apps at the same priority are run in DSF order by default, or in parallel with
--parallelflag - On
--apply --dry-run, you see the execution order — use it to validate your priority logic
4. 🔄 Execution Flow (Step-by-Step)
Here is exactly what happens when you run helmsman --apply -f dsf.yaml:

Step 1 — Parse & Validate: DSF is loaded. All $ENV_VARS are substituted. Schema validation runs. Chart versions are validated against configured repos.
Step 2 — Resolve Secrets: eyaml or Vault-encrypted values are decrypted in memory. Never written to disk.
Step 3 — State Query: helm list --all-namespaces is called. Helmsman builds a map of {release-name → {namespace, chart, version, status}}.
Step 4 — Comparison: Each DSF app is compared against the live state map. A values hash is computed from the resolved values and compared — this is how Helmsman detects value changes even when versions don’t change.
Step 5 — Plan Building: Operations are bucketed by priority. Within each priority bucket, operations are ordered by DSF appearance order (or randomized for parallel execution).
Step 6 — Pre-hooks: Any preInstall, preUpgrade, or preDelete hooks are applied via kubectl apply. Helmsman waits for them to complete (respecting successCondition and successTimeout).
Step 7 — Helm Operations: helm install, helm upgrade, or helm delete are called with the resolved values. The --wait flag is passed if wait: true is set.
Step 8 — Post-hooks: postInstall, postUpgrade, postDelete hooks run. If deleteOnSuccess: true, the hook job is cleaned up.
Step 9 — Reporting: Success/failure status is reported to stdout and optionally to Slack/Teams.
5. 🔍 Plan & Diff Analysis
One of Helmsman’s most powerful features: running without --apply gives you a complete plan.
# Generate a plan without applying
helmsman -f dsf.yaml
Example plan output:
2026-03-19 15:30:01 INFO [DRY-RUN] Helmsman plan:
DECISION: cert-manager is not present, install it in namespace [platform] -- priority: -10
DECISION: ingress-nginx is not present, install it in namespace [platform] -- priority: -5
DECISION: my-app v1.2.3 is already installed in namespace [apps], no diff -- NO-OP
DECISION: old-service is installed but not desired, delete it -- priority: 0
Using helm-diff for Value Changes
When a release’s values have changed but the version hasn’t, you can use the --debug flag (which leverages the helm-diff plugin under the hood) to see exact value substitutions in the plan output:
- replicas: 2
+ replicas: 5
- resources.limits.cpu: "500m"
+ resources.limits.cpu: "1000m"
Targeting Specific Apps
# Only plan/apply for one app
helmsman --apply --target my-app -f dsf.yaml
# Target a group of apps (using group field in DSF)
helmsman --apply --group platform-services -f dsf.yaml
6. 🌍 Multi-Environment Strategy
Pattern 1: Separate DSFs per Environment
The simplest and most common approach. One DSF file per environment:
helmsman/
├── dsf-dev.yaml
├── dsf-staging.yaml
└── dsf-production.yaml
Each DSF has a unique context and points to a different kubeContext. Namespace definitions, policies, and resource quotas differ per environment.
Pattern 2: Shared Base + Environment Overrides
For large fleets where most apps are identical across environments:
# base/apps.yaml (shared app definitions)
apps:
my-app:
chart: "myrepo/my-app"
version: "2.1.0"
enabled: true
# dsf-production.yaml
context: production
settings:
kubeContext: prod-eks
namespaces:
apps:
protected: true
apps:
my-app:
namespace: apps
valuesFile: "values/my-app-prod.yaml"
priority: -5
wait: true
Combine with templating tools (Jsonnet, Helm’s own --values merging, or simple script composition) to DRY up the shared sections.
Pattern 3: Multiple DSFs, One Pipeline
# CI: apply all environments sequentially
helmsman --apply -f dsf-staging.yaml
# Run integration tests
helmsman --apply -f dsf-production.yaml
Use context to ensure releases from one DSF don't interfere with another.
7. 🔐 Security & Secrets Handling
Helmsman has three built-in strategies for secrets — choose based on your stack.
Strategy 1: Environment Variables
The simplest approach. Inject secrets via env vars in your CI/CD system:
apps:
my-app:
set:
database.password: "$DB_PASSWORD" # Resolved at runtime from env
api.key: "$API_KEY"
Works immediately, but requires your CI system to securely manage env vars.
Strategy 2: helm-secrets (eyaml / SOPS)
Encrypt your values files using helm-secrets. Helmsman natively supports eyaml:
settings:
eyamlEnabled: true
eyamlPrivateKeyPath: "/keys/hiera-private.pem"
eyamlPublicKeyPath: "/keys/hiera-public.pub"
apps:
my-app:
secretsFile: "secrets/my-app.eyaml" # Encrypted, decrypted in-memory at apply time
For SOPS-encrypted files, use the helm-secrets plugin directly:
apps:
my-app:
secretsFile: "secrets/my-app.sops.yaml"
Encrypted secrets live safely in your Git repo. Decryption keys live in a KMS (AWS KMS, GCP KMS, Azure Key Vault).
Strategy 3: HashiCorp Vault
Helmsman supports Vault via the helm-secrets plugin (which itself wraps SOPS/Vault dependencies).
settings:
vaultEnabled: true
apps:
my-app:
secretsFile: "secrets/my-app.vault.yaml"
8. 🔁 CI/CD Integration
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
paths: ['helmsman/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Plan Helmsman
run: |
# Use docker to run helmsman
docker run --rm \
-v ${KUBECONFIG_DATA_PATH}:/root/.kube/config:ro \
-v $(pwd):/dsf \
-w /dsf \
ghcr.io/mkubaczyk/helmsman:latest \
helmsman -f helmsman/dsf-staging.yaml --no-banner
env:
KUBECONFIG_DATA_PATH: "/tmp/kubeconfig" # Simplified for example
apply:
needs: plan
environment: staging # Requires manual approval in GitHub
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply Helmsman
run: |
docker run --rm \
-v ${KUBECONFIG_DATA_PATH}:/root/.kube/config:ro \
-v $(pwd):/dsf \
-w /dsf \
ghcr.io/mkubaczyk/helmsman:latest \
helmsman --apply -f helmsman/dsf-staging.yaml
env:
KUBECONFIG_DATA_PATH: "/tmp/kubeconfig"
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
GitLab CI
# .gitlab-ci.yml
stages: [plan, apply]
.helmsman_base: &helmsman_base
image: ghcr.io/mkubaczyk/helmsman:latest
before_script:
- export KUBECONFIG=$(mktemp)
- echo "$KUBECONFIG_DATA" | base64 -d > $KUBECONFIG
plan:staging:
<<: *helmsman_base
stage: plan
script:
- helmsman -f helmsman/dsf-staging.yaml --no-banner
except: [main]
apply:staging:
<<: *helmsman_base
stage: apply
script:
- helmsman --apply -f helmsman/dsf-staging.yaml
only: [main]
when: manual # Require explicit trigger
apply:production:
<<: *helmsman_base
stage: apply
script:
- helmsman --apply -f helmsman/dsf-production.yaml
only: [main]
when: manual
environment:
name: production
Running Inside Kubernetes (Operator Pattern)
Helmsman can run as a CronJob inside the cluster for a lightweight GitOps loop:
apiVersion: batch/v1
kind: CronJob
metadata:
name: helmsman-reconciler
namespace: helmsman
spec:
schedule: "*/10 * * * *" # Every 10 minutes
jobTemplate:
spec:
template:
spec:
serviceAccountName: helmsman
containers:
- name: helmsman
image: ghcr.io/mkubaczyk/helmsman:latest
args:
- helmsman
- --apply
- -f
- /dsf/dsf-production.yaml
envFrom:
- secretRef:
name: helmsman-secrets
volumeMounts:
- name: dsf
mountPath: /dsf
volumes:
- name: dsf
configMap:
name: helmsman-dsf
restartPolicy: OnFailure
9. ⚔️ Helmsman vs Alternatives

Choose Helmsman when:
- You want declarative Helm management without installing ArgoCD/Flux CRDs
- Your team is already comfortable with Helm and just needs orchestration
- You need a lightweight, scriptable, file-based approach for smaller clusters
- You’re wrapping Helmsman in a custom CI/CD pipeline
Choose ArgoCD when:
- You need continuous reconciliation and a UI for visibility
- Your team is managing 50+ applications across multi-cluster environments
- You want full GitOps semantics with RBAC, SSO, and audit logs
Choose Helmfile when:
- You’re already deep in Helm and just need a layered values management tool
- You don’t need state tracking, just execution ordering
10. 🏭 Real-World Use Case: Platform Engineering Team
Scenario: A platform team manages 30+ Helm releases across dev, staging, and production clusters. Releases have hard dependencies (cert-manager must be ready before ingress, ingress before apps).
DSF structure:
context: prod-platform
settings:
kubeContext: prod-eks
globalMaxHistory: 10
slackWebhook: "$SLACK_WEBHOOK"
namespaces:
platform:
protected: true
labels:
managed-by: helmsman
tier: infrastructure
apps:
protected: true
helmRepos:
cert-manager: "https://charts.jetstack.io"
ingress-nginx: "https://kubernetes.github.io/ingress-nginx"
argo: "https://argoproj.github.io/argo-helm"
myrepo: "https://$REPO_USER:$REPO_PASS@charts.internal.company.com"
apps:
cert-manager:
namespace: platform
enabled: true
chart: cert-manager/cert-manager
version: "v1.14.4"
priority: -20 # First infrastructure layer
wait: true
set:
installCRDs: "true"
hooks:
preInstall: "manifests/cert-manager-crds.yaml"
ingress-nginx:
namespace: platform
enabled: true
chart: ingress-nginx/ingress-nginx
version: "4.9.1"
priority: -15 # Second layer
wait: true
valuesFile: "values/ingress-nginx-prod.yaml"
argocd:
namespace: platform
enabled: true
chart: argo/argo-cd
version: "6.7.3"
priority: -10 # Third layer
wait: true
valuesFile: "values/argocd-prod.yaml"
secretsFile: "secrets/argocd-secrets.eyaml"
team-a-app:
namespace: apps
enabled: true
chart: myrepo/team-a-app
version: "1.5.2"
priority: 0 # Last — depends on infra being ready
valuesFile: "values/team-a-prod.yaml"
secretsFile: "secrets/team-a-secrets.eyaml"
Result: CI runs helmsman -f dsf-production.yaml on every merge to main. The plan is posted to Slack. A second manual step runs --apply. The entire production release state is codified, reviewable as a PR, and reproducible.
11. 📈 Scaling Challenges
As your DSF grows, watch for these scaling limitations:
Challenge 1: Single DSF Becomes a Monolith
Once you have 50+ apps in one file, changes become risky. Small version bumps require reviewing the entire DSF.
Solution: Split by team or domain. Use separate DSFs with separate contexts:
helmsman/
├── platform/dsf-platform.yaml
├── team-a/dsf-team-a.yaml
└── team-b/dsf-team-b.yaml
Challenge 2: Long Apply Times
Each helm upgrade call blocks until completion (with wait: true). With 30+ releases, serial execution can take 20+ minutes.
Solution: Use --parallel for apps with the same priority:
helmsman --apply --parallel -f dsf.yaml
# Apps at priority -10 all run concurrently
# Then all apps at priority -5 run concurrently, etc.
Challenge 3: Context Conflicts
Two pipelines running different DSFs with the same context will interfere with each other's state tracking.
Solution: Always use globally unique contexts. Name them after the pipeline, environment, and cluster:
context: "pipeline-prod-eu-west-1"
Challenge 4: Diff Noise on Values Changes
Frequent changes to values files cause many releases to show up as “changed” in every plan, even when you only intended to update one app.
Solution: Use --target to scope applies to specific apps during development. Use group field in the DSF for logical grouping.
12. ⚠️ Common Pitfalls
Pitfall 1: Forgetting context Uniqueness
Two DSFs running against the same cluster with the same context will compete. One run may delete releases the other just installed.
Pitfall 2: Setting enabled: false Instead of Removing the App
enabled: false deletes the Helm release! If you want to temporarily pause deployment, comment out the app or use --target to exclude it. Only set enabled: false if you intentionally want the release removed.
Pitfall 3: Protected Namespaces Silently Skip Operations
If protected: true is set on a namespace, Helmsman prints a warning but proceeds with other apps. Easy to miss if you're not watching logs. Always check the plan output for "PROTECTED" messages.
Pitfall 4: wait: true Without a Timeout
Default timeout is 300 seconds. A broken release can block the entire pipeline. Set appropriate timeout values and monitor.
Pitfall 5: Changing chart Name Triggers Delete + Reinstall
In Helmsman, if you rename the chart (e.g., from argo/argo-cd to argo/argocd), it treats it as a different chart and deletes the current release before reinstalling. Always verify chart names before changing them in production.
Pitfall 6: Missing helmRepos Declaration
If a chart repo isn’t declared in helmRepos, the plan fails with a validation error. Missing repos are caught at plan time, not apply time.
13. ✅ Best Practices
- Always run without
--applyfirst. Review the plan before applying. In CI, make plan the first step and apply a manual gate. - Use
protected: trueon production namespaces. It's your circuit breaker. Combined with CI pipeline controls, it prevents accidental wipeouts. - Pin chart versions explicitly. Never use
version: "*"orversion: latest. Reproducibility requires exact versions. - Use negative priorities for all infrastructure. Give cert-manager, ingress controllers, and service mesh components priorities of -20 to -10. Leave 0 for application workloads.
- Commit your DSF to Git. This is the entire point. DSF changes should be reviewed as PRs, not applied ad-hoc.
- Enable
test: truefor apps. This automatically runshelm testafter an install or upgrade, giving you a free, synchronous validation gate directly in the deployment pipeline. - Use
grouplabels for targeted deploys. During development, usehelmsman --target my-appinstead of running the full DSF on every iteration. - Set
globalMaxHistoryto a sane value. Default Helm history can balloon. SetglobalMaxHistory: 10in settings to cap it globally. - Use
secretsFileoversetfor sensitive values. Inlinesetvalues appear in Helm history.secretsFilekeeps secrets out of the Helm release metadata.
14. 🔭 Observability & Debugging
Verbose Plan Output
# Show all decisions including no-ops
helmsman -f dsf.yaml --debug
# Dry run with debug
helmsman --dry-run --debug -f dsf.yaml
Checking Helmsman’s View of Live State
# What does Helmsman see in the cluster?
helm list --all-namespaces --output json | jq '.[] | {name, namespace, chart, app_version, status}'
Debugging a Specific Release
# Run Helmsman for just one release in debug mode
helmsman --apply --target my-app --debug -f dsf.yaml
# Check what values Helmsman would send
helm get values my-app -n my-namespace
Log Correlation with Hooks
Hook jobs can fail silently. Always set:
hooks:
successCondition: "Complete" # Wait for the job to complete
successTimeout: "120s" # How long to wait
deleteOnSuccess: true # Clean up after success
If a hook fails, the release operation is aborted. Check:
kubectl get jobs -n <namespace>
kubectl logs job/<hook-job-name> -n <namespace>
Slack/Teams Notifications
Add to settings to get plan + apply results posted automatically:
settings:
slackWebhook: "$SLACK_WEBHOOK"
msTeamsWebhook: "$TEAMS_WEBHOOK"
15. 🚫 When NOT to Use Helmsman
Helmsman is excellent, but it’s not always the right tool.
Don’t use Helmsman when:
- You need continuous reconciliation. Helmsman is run-based, not continuously watching. If a team member manually changes a release, Helmsman won’t correct it until the next run. Use ArgoCD or Flux for true GitOps.
- You’re managing 100+ clusters at enterprise scale. Helmsman’s flat DSF model doesn’t scale well to fleet-level multi-cluster management. ArgoCD ApplicationSets or Flux are better fits.
- Your team needs a UI. Helmsman has no web interface. If visibility and multi-team collaboration are priorities, ArgoCD’s UI is a major advantage.
- You need fine-grained K8s resource control. Helmsman manages at the Helm release level. If you need per-resource override reconciliation, you want a full GitOps operator.
- You’re not already using Helm. If your manifests are raw YAML or Kustomize, Helmsman adds no value. It’s exclusively a Helm orchestration layer.
Summary: The Helmsman Decision Framework

Helmsman isn’t trying to be ArgoCD. It’s trying to bring the Terraform workflow to Helm — and at that, it excels. For teams that want declarative, version-controlled, ordered Helm release management without the operational overhead of a full GitOps platform, Helmsman hits the sweet spot.
Further Reading
메타데이터
- post_id
- 86f58c8f7e6a
- slug
- helmsman-deep-dive-86f58c8f7e6a
- url
- https://medium.com/@rameshavutu/helmsman-deep-dive-86f58c8f7e6a
- canonical_url
- https://medium.com/@rameshavutu/helmsman-deep-dive-86f58c8f7e6a
- author_url
- https://medium.com/@rameshavutu
- status
- ok
- fetched_at
- 2026-06-09 15:37:30