<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Helm-Diff on inetshell</title><link>https://inet.sh/en/tags/helm-diff/</link><description>Recent content in Helm-Diff on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/helm-diff/index.xml" rel="self" type="application/rss+xml"/><item><title>helmfile in production: three-way diff, 'already set in map', OCI 403, duplicate dependencies and adopting resources that were already deployed</title><link>https://inet.sh/en/posts/kubernetes/helmfile-in-production-tricks/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/kubernetes/helmfile-in-production-tricks/</guid><description>What you learn about helmfile after months of running it in CI: HELM_DIFF_THREE_WAY_MERGE to see the real cluster drift, HELMFILE_GOCCY_GOYAML for the &amp;#39;key already set in map&amp;#39; error, --skip-diff-on-install for huge installs, the OCI registry 403 that a newer helm fixes, &amp;#39;more than one dependency with name or alias&amp;#39; from version mismatches, filtering with -l name=, and how to make Helm adopt resources that were applied with kubectl. Plus the helmfile 1.x and Helm 4 changes.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELM_DIFF_THREE_WAY_MERGE</span><span class="o">=</span><span class="nb">true</span>        <span class="c1"># diff against what is in the cluster, not against the last release</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELM_DIFF_NORMALIZE_MANIFESTS</span><span class="o">=</span><span class="nb">true</span>    <span class="c1"># strip formatting noise from the diff</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELMFILE_GOCCY_GOYAML</span><span class="o">=</span><span class="nb">true</span>            <span class="c1"># helmfile 0.x: avoids &#34;key already set in map&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span>rabbitmq diff        <span class="c1"># a single release</span>
</span></span><span class="line"><span class="cl">helmfile -e prod apply --skip-diff-on-install <span class="c1"># giant first install without a diff</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in CI, pinned versions of all three pieces (<code>helm</code>, <code>helmfile</code>, <code>helm-diff</code>): half of helmfile&rsquo;s odd errors are combinations nobody tested together.</p>
<h2 id="the-errors-and-what-fixes-them">The errors and what fixes them</h2>
<table>
  <thead>
      <tr>
          <th>Message</th>
          <th>Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>helmfile diff</code> says nothing changed, but the cluster does not match the chart</td>
          <td>helm-diff compares the last release&rsquo;s manifest with the new one, not the live state; a manual <code>kubectl edit</code> never shows up</td>
          <td><code>HELM_DIFF_THREE_WAY_MERGE=true</code> (or <code>--three-way-merge</code> in helm-diff)</td>
      </tr>
      <tr>
          <td>The diff is full of quoting, key order and indentation changes</td>
          <td>Different Helm or chart versions serialize differently</td>
          <td><code>HELM_DIFF_NORMALIZE_MANIFESTS=true</code></td>
      </tr>
      <tr>
          <td><code>key &quot;image&quot; already set in map</code> while rendering values</td>
          <td>The old YAML library rejects duplicate keys and some anchors in <code>values</code></td>
          <td><code>HELMFILE_GOCCY_GOYAML=true</code> on helmfile 0.x; helmfile 1.x already uses that library</td>
      </tr>
      <tr>
          <td><code>Body is too long (maximum is 65536 characters)</code> or the job takes forever on the first install</td>
          <td>A fresh install&rsquo;s diff is the whole chart, and the PR comment or the log cannot hold it</td>
          <td><code>--skip-diff-on-install</code></td>
      </tr>
      <tr>
          <td><code>Error: failed to authorize: … 403 Forbidden</code> pulling a chart from <code>oci://ghcr.io/…</code></td>
          <td>Helm older than 3.8 with experimental OCI support, or no <code>helm registry login</code></td>
          <td>Upgrade helm; <code>helm registry login ghcr.io</code> if the registry is private</td>
      </tr>
      <tr>
          <td><code>Error: validation: more than one dependency with name or alias</code></td>
          <td>helm and helmfile out of step (helm 3.15 needed helmfile ≥ 0.164; helm 3.13 went with 0.158)</td>
          <td>Upgrade helmfile alongside helm; on 1.x it no longer happens</td>
      </tr>
      <tr>
          <td>The diff has no color in CI</td>
          <td>helm-diff detects there is no TTY</td>
          <td><code>HELM_DIFF_COLOR=true</code></td>
      </tr>
  </tbody>
</table>
<h2 id="adopting-resources-that-already-exist">Adopting resources that already exist</h2>
<p>The typical case: someone installed ArgoCD (or whatever) with <code>kubectl apply -f</code>, and now you want a helmfile release to manage it. Helm refuses to install over resources that are not its own (<code>rendered manifests contain a resource that already exists … invalid ownership metadata</code>). Since Helm 3.2 it is enough to <strong>mark them as Helm&rsquo;s</strong> before installing:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">NS</span><span class="o">=</span>argocd<span class="p">;</span> <span class="nv">REL</span><span class="o">=</span>argocd
</span></span><span class="line"><span class="cl"><span class="k">for</span> kind in deploy sts svc cm secret sa ingress role rolebinding networkpolicy<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl -n <span class="nv">$NS</span> get <span class="nv">$kind</span> -o name 2&gt;/dev/null <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r r<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="c1"># cluster-scoped resources: same labels, no -n</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> r in <span class="k">$(</span>kubectl get crd,clusterrole,clusterrolebinding -o name <span class="p">|</span> grep -i argo<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">  kubectl annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> diff     <span class="c1"># should be nearly empty</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> apply
</span></span></code></pre></td></tr></table>
</div>
</div><p>Only mark what the chart is going to render: a labeled resource the chart does not include is harmless, but one the chart renders under another name ends up duplicated.</p>
<h2 id="helmfile-1x-and-helm-4">helmfile 1.x and Helm 4</h2>
<p>Coming from 0.x, this is what changes when you upgrade:</p>
<ul>
<li>A <code>helmfile.yaml</code> with Go templates must be called <strong><code>helmfile.yaml.gotmpl</code></strong>; a plain <code>.yaml</code> is no longer rendered.</li>
<li><code>environments:</code> and <code>releases:</code> go in <strong>separate parts split by <code>---</code></strong> within the same file.</li>
<li><code>helmfile delete</code> is gone (it is <code>destroy</code>), so is <code>helmfile charts</code>, and <code>--args</code> is removed.</li>
<li><code>HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS</code> becomes <code>HELMFILE_DISABLE_INSECURE_FEATURES</code>.</li>
<li>helmfile 1.x works with <strong>Helm 3 and Helm 4</strong>. With Helm 4, install helm-diff with <code>helm plugin install https://github.com/databus23/helm-diff --verify=false</code> (Helm 4 verifies plugin provenance and helm-diff does not publish it).</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>apply</code> with no prior diff in CI</strong>: <code>helmfile apply</code> already diffs and only applies when there are changes; <code>sync</code> always applies. In pipelines, <code>diff</code> on the PR and <code>apply</code> on main.</li>
<li><strong><code>installed: false</code></strong> uninstalls the release on the next <code>apply</code>; handy to retire things through Git, dangerous if copied from another environment.</li>
<li><strong>Secrets in <code>values</code></strong>: helmfile leaves them in the release manifest (Helm&rsquo;s Secret in the namespace); use <code>helm-secrets</code> with SOPS or external references.</li>
<li><strong><code>--skip-diff-on-install</code> is not <code>--skip-diff</code></strong>: it only skips the diff for releases that do not exist yet. Upgrades still show their diff.</li>
<li><strong>Three binaries, three versions</strong>: <code>helm version</code>, <code>helmfile version</code> and <code>helm plugin list</code> as the first pipeline step, pinned by tag in the CI image.</li>
</ul>
<p>References: <a href="https://github.com/databus23/helm-diff">helm-diff</a>, <a href="https://helmfile.readthedocs.io/">helmfile</a> and the <a href="https://github.com/helmfile/helmfile/blob/main/docs/proposals/towards-1.0.md">towards 1.0 proposal</a>.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/k3s/import-images-without-registry/">k3s: import images without a registry</a> — when the release depends on an image that lives in no registry.</li>
</ul>
]]></content:encoded></item></channel></rss>