New to Kestra?
Use blueprints to kickstart your first workflows.
Deploy a Helm chart with Kestra through a server-side dry run and a production approval gate, with automatic rollback and Slack alerts on failure.
Ship Helm releases through the same guarded path every time. This blueprint renders the chart offline, validates the exact upgrade against the live API server, pauses production deploys for a human approval, then runs helm upgrade --install and waits for every resource to become ready. If the deploy, the read-back or the health check fails, the release is rolled back to its previous revision and Slack is alerted. CI triggers it with a webhook, so a pipeline can push a new image tag without anyone touching kubectl or a kubeconfig.
render_chart (io.kestra.plugin.helm.Template) runs helm template for the podinfo chart 6.15.0 with the requested image tag. It never contacts the cluster. The rendered manifest is stored with the execution ({{ outputs.render_chart.manifest }}), so every run keeps a reviewable, diffable record of what was about to ship.validate_on_cluster (io.kestra.plugin.helm.Upgrade with dryRun: SERVER) runs the same upgrade as a server-side dry run against the API server. Bad credentials, an unreachable cluster or a chart that will not install stop the flow here, before approval is requested and before anything is applied. A dry run emits no Assets.production_gate (io.kestra.plugin.core.flow.If) checks inputs.environment. For production, request_approval posts to Slack and approve_production (io.kestra.plugin.core.flow.Pause) holds the run for up to one hour. Resume the execution to deploy. With behavior: FAIL, an approval nobody acts on fails the run instead of deploying. Staging skips the gate.release (io.kestra.plugin.core.flow.Sequential) does the actual change:deploy (io.kestra.plugin.helm.Upgrade) runs helm upgrade --install with createNamespace: true, wait: WATCHER and timeout: PT10M, so the task only succeeds once the new pods are ready. It registers the release and its resources as Assets tagged with the environment.verify_release (io.kestra.plugin.helm.Status) reads the release back from the cluster.fail_if_unhealthy (io.kestra.plugin.core.execution.Fail) fails the step unless {{ outputs.verify_release.status }} is deployed.errors branch on release only runs when one of those three tasks fails. A failed dry run or an expired approval never triggers it. rollback (io.kestra.plugin.helm.Rollback, wait: WATCHER) reverts to the previous revision and waits for it to be ready, then alert_rollback posts the outcome to Slack. The execution still ends FAILED, so the failure stays visible.notify_success posts the release name, deployed revision and environment to Slack.ci_deploy_webhook trigger (io.kestra.plugin.core.trigger.Webhook) lets CI start a deploy. The image tag is read with {{ trigger.body.image_tag ?? inputs.image_tag }}: the JSON body wins when it carries image_tag, and manual runs fall back to the input.environment metadata.helm upgrade --atomic shell steps in CI with something they can observe, resume and replay.Helm on its own has no approval step, no notifications, and no memory of who deployed what. A CI job that shells out to helm upgrade loses the rendered manifest when the runner is recycled and cannot wait an hour for a human without holding a runner. Kestra keeps each stage as a separate, retryable task with its own logs and outputs. The approval pause costs nothing while it waits, the rollback is scoped so it only runs for the release step, and every run records the manifest, revision and release Assets. The same flow serves CI (webhook) and humans (manual run with inputs).
alpine/helm:4.3.0 container by default. A Helm 3 image is not supported.io.kestra.plugin.helm) and the Slack plugin installed.K8S_MASTER_URL: Kubernetes API server URL, e.g. https://my-cluster.example.com:6443.K8S_CA_CERT: the cluster CA certificate, base64-encoded (the certificate-authority-data value from a kubeconfig). Do not paste a raw PEM.K8S_TOKEN: bearer token of the service account Helm deploys with.SLACK_WEBHOOK_URL: Slack incoming webhook used for the approval request, the success message and the rollback alert.HELM_DEPLOY_WEBHOOK_KEY: the secret key in the webhook URL that CI calls. Treat it like a password.environment (SELECT, staging or production, default staging): selects the namespace podinfo-<environment> and whether the approval gate applies. Webhook runs use the default (staging).image_tag (STRING, default 6.15.0): podinfo image tag for manual runs. A webhook body field image_tag overrides it.{{ outputs.render_chart.manifest }}: URI of the offline-rendered manifest in internal storage.{{ outputs.render_chart.resources }}: kinds, names and namespaces of the resources the chart renders.{{ outputs.deploy.releaseName }}, {{ outputs.deploy.namespace }}: the deployed release and its namespace.{{ outputs.deploy.revision }}: Helm revision created by the deploy.{{ outputs.deploy.status }}, {{ outputs.verify_release.status }}: Helm status, deployed on success.{{ outputs.deploy.chartVersion }}, {{ outputs.deploy.appVersion }}: chart and application versions now live.{{ outputs.deploy.manifest }}: URI of the manifest Helm actually applied.{{ outputs.rollback.revision }}, {{ outputs.rollback.status }}: where the release ended up after a rollback.SECRET_<NAME> environment variables. K8S_CA_CERT is already base64, so it ends up encoded twice in OSS.environment: staging to install the release.environment: production. The run pauses after the dry run. Resume it from the execution page to deploy./api/v1/{tenant}/executions/webhook/company.team/helm-gated-release-with-rollback/{HELM_DEPLOY_WEBHOOK_KEY} with a JSON body, for example curl -X POST -H 'Content-Type: application/json' -d '{"image_tag": "6.14.1"}' https://your-kestra/api/v1/main/executions/webhook/company.team/helm-gated-release-with-rollback/$KEY.helm history podinfo -n podinfo-staging to see the revisions Kestra created, including any rollback.podinfo for your own chart: change chart.repository, chart.name and chart.version on the three tasks that use it, or point chart.repository at an oci:// registry.io.kestra.plugin.core.flow.WorkingDirectory with io.kestra.plugin.git.Clone and pass the files through valuesFrom.cluster and region on the release tasks so Asset metadata matches your naming, e.g. cluster: prod-eu.K8S_MASTER_URL_PRODUCTION) to deploy staging and production to different clusters.io.kestra.plugin.core.http.Request against the service) inside release. If it fails, the rollback runs.rollbackOnFailure: true on deploy if you want Helm to revert by itself. Remove the rollback task in that case, because both together would move the revision twice.