← Back to list

Helmsman Deep Dive: Declarative Helm at Scale

Stop scripting Helm. Start declaring it like terraform.

Ramesh · 2026-06-01 05:22 · 1 claps · 15.7 min read
#kubernetes #devops #helm #gitops #platform-engineering
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

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 --install commands 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 wantCOMPARE with what existsPLAN the diffAPPLY 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: falseDelete
  • Release does not exist + enabled: trueInstall
  • 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

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-context CLI 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 --parallel flag
  • 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

  1. Always run without --apply first. Review the plan before applying. In CI, make plan the first step and apply a manual gate.
  2. Use protected: true on production namespaces. It's your circuit breaker. Combined with CI pipeline controls, it prevents accidental wipeouts.
  3. Pin chart versions explicitly. Never use version: "*" or version: latest. Reproducibility requires exact versions.
  4. 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.
  5. Commit your DSF to Git. This is the entire point. DSF changes should be reviewed as PRs, not applied ad-hoc.
  6. Enable test: true for apps. This automatically runs helm test after an install or upgrade, giving you a free, synchronous validation gate directly in the deployment pipeline.
  7. Use group labels for targeted deploys. During development, use helmsman --target my-app instead of running the full DSF on every iteration.
  8. Set globalMaxHistory to a sane value. Default Helm history can balloon. Set globalMaxHistory: 10 in settings to cap it globally.
  9. Use secretsFile over set for sensitive values. Inline set values appear in Helm history. secretsFile keeps 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