skip to content

What does `helm upgrade --install` do, and why do CI pipelines prefer it to `helm install`?

level: juniorimportance: must knowfreq 80%

answer

  1. One deploy step, first run and later runs
  2. Each command refuses one situation
  3. Removes the status-probe shell guard
  4. Same outcome, new revision every run
  5. `-i`, `.Release.IsInstall`

basics

~20 s

helm upgrade --install upgrades a release when one with that name already exists in the namespace and installs it when none does. Pipelines use it because the same command works on the first deploy and every deploy after it.

solid answer

~50 s

`helm install` refuses to run when a release with that name already exists in the target namespace, and `helm upgrade` refuses to run when no such release exists — so either command alone is wrong exactly once in the life of a deployment. `helm upgrade --install NAME CHART` (short flag `-i`) collapses both: Helm looks the name up in the namespace, installs when there is no record and upgrades when there is. That makes the deploy step re-runnable, which is what people mean when they call it idempotent, and it removes the `helm status ... && upgrade || install` shell guard that pipelines otherwise grow. It is idempotent in outcome, not in bookkeeping: every successful run records a new revision even when the rendered manifests are byte-identical. Inside templates the two paths are still distinguishable through `.Release.IsInstall` and `.Release.IsUpgrade`.

code

bash · 9 lines
bash
# Fails on the second run: the name is already in use
helm install transcoder ./transcoder-operator -n media

# Fails on the first run: nothing to upgrade
helm upgrade transcoder ./transcoder-operator -n media

# Correct on both: installs when absent, upgrades when present
helm upgrade --install transcoder ./transcoder-operator \
  -n media -f values-prod.yaml --version 2.14.3

go deeper

for a junior

Be ready to state plainly what each of the two commands refuses to do, and to write the one-line deploy step that works on both the first run and the hundredth. Knowing the -i short form is a nice touch.

for a middle

Explain the dispatch: Helm looks the release name up in the target namespace and chooses install or upgrade from what it finds. Be able to say why the shell-guard alternative is worse, and that a repeat run still records a fresh revision.

for a senior

Show the operational consequences: scheduled runs padding history with empty revisions, a disaster-recovery rebuild that fails because the pipeline only ever upgrades, and values inputs that must be identical on every path that can deploy or the command stops being repeatable.

for a principal

Own the standard: one deploy interface across every service and environment, no per-team branching logic in pipeline files, explicit chart version pinning, and a clear rule about which jobs may write to a release at all. Be prepared to defend that against teams who want bespoke deploy scripts.

## Two entry points, each wrong exactly once Helm's write path has two commands. `helm install NAME CHART` creates a release: it renders the chart, applies the result, and records a release for `NAME` in the target namespace at revision 1. It deliberately refuses to run when a release with that name already exists there — the name is in use, and Helm will not quietly take it over. `helm upgrade NAME CHART` is the mirror image: it renders the chart again, applies it on top of what the previous revision recorded, and stores a new revision. It refuses to run when there is no release by that name to upgrade. That pairing is fine at a terminal, where a human knows which situation they are in. It is awkward in automation, because a pipeline step runs the same way every time. A deploy job built on `helm install` succeeds once and then fails on every subsequent merge. A deploy job built on `helm upgrade` fails the first time it meets a fresh namespace or a fresh cluster — the classic broken disaster-recovery drill, where the pipeline that has deployed fine for a year cannot rebuild the environment from nothing. The homemade fix is a shell guard: probe with `helm status NAME`, then branch to `upgrade` or `install`. It works most of the time, and it fails in the interesting cases. It races with a concurrent run of the same job, it treats a release that exists but is in a failed state as an ordinary upgrade target, and it puts logic in a pipeline file where nobody reviews it. ## What `--install` actually changes `helm upgrade --install NAME CHART` — `-i` for short — does the lookup that the shell guard was doing, inside Helm, against the release records in the target namespace. If there is no release under that name, Helm performs an install: revision 1, `.Release.IsInstall` true inside templates. If there is one, Helm performs the ordinary upgrade: render, apply, record the next revision. One command, no branch, and the same command is correct on a clean cluster and on the hundredth deploy. This is why nearly every CI deploy step and every chart README shows `helm upgrade --install`. It is also why interviewers ask: a candidate who has only ever run `helm install` by hand will not have met the second-deploy failure, and a candidate who wrote the shell guard will usually admit it once prompted. ## Idempotent in outcome, not in bookkeeping "Idempotent" here means you can run the step again and end up in the same desired state — not that a repeat run is a no-op. Every successful `helm upgrade` records a new revision, even when the manifests it renders are byte-identical to the previous revision's. `helm history NAME` will show a row per run, the revision counter keeps climbing, and old revisions fall off once the retained-history limit is reached. If a job runs on a schedule rather than on merge, that is a steady drip of empty revisions; nothing breaks, but the history stops being a useful record of what changed. A second, sharper caveat: the command is only repeatable if the *inputs* are repeatable. `helm upgrade` does not carry the previous run's values forward on its own — it starts from the chart's defaults plus whatever this invocation passes. A pipeline that passes `-f values-prod.yaml --set image.tag=$SHA` on one path and forgets the values file on another will produce two very different releases from the same command. ## What `--install` does not give you It does not create the namespace — a missing namespace is still an error unless you also pass `--create-namespace`. It does not adopt objects that already exist in the cluster but belong to nobody or to another release. It does not repair a release that a previous run left mid-operation. And it does not imply any waiting for workloads to become ready; that is a separate flag with its own semantics. ## Telling the paths apart from inside the chart When a chart genuinely needs to behave differently on the first run — seeding a database, creating a one-time bootstrap object — the built-in `.Release` object carries `.Release.IsInstall` and `.Release.IsUpgrade` booleans, plus `.Release.Revision`, which is 1 on the install. Note that `.Release.IsInstall` is true for the install performed by `helm upgrade --install` too: the flag changes which operation Helm dispatches, not what the templates are told about it. A practical habit for pipelines: pin the chart version explicitly rather than resolving whatever is newest, keep the values inputs identical across every path that can deploy, and render with `--dry-run=client` in review jobs so the diff is visible before the command that writes runs at all.

  • If nothing in the chart or the values changed, does re-running `helm upgrade --install` leave the release untouched?
    No. Every successful run records a new revision even when the rendered manifest is identical, so the revision counter climbs and older revisions eventually fall off the retained-history limit. The objects in the cluster end up the same, but the release history does not. If you need to know whether a run would actually change anything, render and compare before running the command that writes.
  • How can a chart template tell whether the current run is the initial install or a later upgrade?
    Through the built-in `.Release` object: `.Release.IsInstall` is true on the run that creates revision 1 — including the install that `helm upgrade --install` performs — and `.Release.IsUpgrade` is true on every later run. `.Release.Revision` carries the number itself. Charts use these to render a one-time bootstrap Job or to guard a migration so it does not re-run on every deploy.
  • Is there any reason left to use plain `helm install` in automation?
    Yes, when you want the run to fail loudly if the name is already taken — for instance a provisioning job that must never write to a release it did not create, or a per-pull-request environment that should be a fresh install or nothing. Plain `helm install` refusing an existing name is a cheap guard there. For an ordinary continuous-deploy step, `helm upgrade --install` is the default.

saying these in an interview costs you the question

  • Says `helm install` updates a release that already exists
  • Thinks `helm upgrade` works on a namespace with no release yet
  • Claims `--install` also creates a missing namespace
  • Believes a repeat run with no changes records no revision
  • Assumes `helm upgrade` reuses the previous run's values by default
  • Confuses `--install` with `--force-replace`

context