<?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>Helmfile on ln --help</title>
    <link>https://blog.mei-home.net/tags/helmfile/</link>
    <description>Recent content in Helmfile on ln --help</description>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Thu, 05 Jun 2025 21:20:53 +0200</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/helmfile/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Organizing Helm charts and other Manifests with Helmfile</title>
      <link>https://blog.mei-home.net/posts/helmfile/</link>
      <pubDate>Thu, 05 Jun 2025 21:20:53 +0200</pubDate>
      <guid>https://blog.mei-home.net/posts/helmfile/</guid>
      <description>How to organize and handle it all?</description>
      <content:encoded><![CDATA[<p>Wherein I describe how I organize Helm charts and other k8s manifests.</p>
<p>I&rsquo;ve had this post laying around in my draft folder for a long long time.
Mostly because I started writing it before I realized how useful it is to write
posts very close to when something happens.</p>
<p>The &ldquo;something happens&rdquo; in this case is the answer to the question &ldquo;How to
organize my Helm charts and other k8s manifests?&rdquo;. I liked Helm fine enough when
I looked at it. It&rsquo;s pretty nice to get all necessary manifests to run an app,
instead of having to write all of them myself.
But the question then was: How to store which exact Helm charts I have
installed, and in which version? And how/where to store the <code>values.yaml</code> files?
And then, what about random manifests, like additional PriorityClasses?</p>
<p>The solution that was pointed out to me on the Fediverse: <a href="https://github.com/helmfile/helmfile">Helmfile</a>.
It&rsquo;s a piece of software that allows reading a number of Helm charts to be
installed and deploying them onto a cluster. It does not re-implement Helm, but
simply calls a previously installed Helm binary.</p>
<p>All of the configuration for Helmfile is stored in a local Yaml file. A
good example for what that config looks like is my <a href="https://cloudnative-pg.io/">CloudNativePG</a>
setup. Helmfile by default reads the config from a file named <code>helmfile.yaml</code>
in the current working dir. My <code>helmfile.yaml</code>, stripped down only to the
CNPG setup, looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">repositories</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cloud-native-pg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://cloudnative-pg.github.io/charts</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">releases</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cnpg-operator</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">chart</span>: <span style="color:#ae81ff">cloud-native-pg/cloudnative-pg</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">version</span>: <span style="color:#ae81ff">v0.21.2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">cnpg-operator</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./cnpg-operator/hl-values.yaml.gotmpl</span>
</span></span></code></pre></div><p>And the <code>hl-vaues.yaml.gotmpl</code> is then just the <code>values.yaml</code> file for the
CNPG Helm chart. With one additional wrinkle: Helmfile can do templating, on the
<code>values.yaml</code> file. Which is pretty cool. Just one example of how I&rsquo;m using this
is my <a href="https://external-secrets.io/latest/">external-secrets</a> addon <code>values.yaml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">caBundle</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  {{- exec &#34;curl&#34; (list &#34;https://vault.example.com:8200/v1/my-ca/ca/pem&#34;) | nindent 2 }}</span>
</span></span></code></pre></div><p>Then in turn, I&rsquo;m writing that to a Secret:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">my-ca-cert</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">stringData</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">caCert</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    {{- .Values.caBundle | nindent 6 }}</span>
</span></span></code></pre></div><p>And the curl command is executed on the machine where Helmfile is executed. Which
is particularly nice when you&rsquo;re fetching some Secrets via this mechanism, because
it allows you to use local credentials that only exist on that single machine.</p>
<p>Once you&rsquo;ve entered a release into the Helmfile, it can be deployed with a
command like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>helmfile apply --selector name<span style="color:#f92672">=</span>cnpg-operator
</span></span></code></pre></div><p>This will automatically update all repositories and then run <code>helm upgrade</code>.
Very helpfully, it will also output the diff between the new release and what&rsquo;s
currently deployed on the cluster.</p>
<p>Besides working with Helm charts directly, you can also just throw a couple of
manifests into a directory and deploy it the same way. I&rsquo;m doing this for my
own priority classes for example. I just have them in a directory <code>hl-common/</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>ls hl-common/
</span></span><span style="display:flex;"><span>prio-hl-critical.yaml  prio-hl-external.yaml
</span></span></code></pre></div><p>Helmfile will then use <a href="https://github.com/helmfile/chartify">Chartify</a> to
turn those loose files into an ad-hoc chart and deploy it.</p>
<p>The <code>release[].values[]</code> list is also a pretty useful feature. It allows setting
Helm chart values right in the Helmfile instead of a separate <code>values.yaml</code>.
I don&rsquo;t use this too much, as I like having all configs neatly in one file. But
I like using this approach in one instance, namely for <code>appVersion</code>-like values
on Helm charts I wrote myself. Here&rsquo;s an example from my Audiobookshelf entry:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">audiobookshelf</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">chart</span>: <span style="color:#ae81ff">./audiobookshelf</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">audiobookshelf</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">appVersion</span>: <span style="color:#e6db74">&#34;2.23.0&#34;</span>
</span></span></code></pre></div><p>The fact that I have the appVersion in the Helmfile directly makes it a lot more
convenient when I do my regular service update rounds. Unless something deeper
changed, I just need to have my Helmfile open during Service Upgrade Friday and
either update the chart version or the <code>appVersion</code> right there, without having
to switch between all of the <code>values.yaml</code> or <code>Chart.yaml</code> files.</p>
<p>For my standard approach, I&rsquo;m currently working with two release entries when
using a 3rd party chart. Let&rsquo;s look at my <a href="https://forgejo.org/">Forgejo</a>
deployment as an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">repositories</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">forgejo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">url</span>: <span style="color:#ae81ff">code.forgejo.org/forgejo-helm</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">oci</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">releases</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">forgejo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">chart</span>: <span style="color:#ae81ff">forgejo/forgejo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">version</span>: <span style="color:#ae81ff">12.5.1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">forgejo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./forgejo/hl-values.yaml.gotmpl</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">forgejo-addons</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">forgejo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">chart</span>: <span style="color:#ae81ff">./forgejo-addons</span>
</span></span></code></pre></div><p>In this approach, the <code>forgejo/hl-values.yaml.gotmpl</code> file is the <code>values.yaml</code>
file for the Forgejo chart. But, in most instances, 3rd party charts don&rsquo;t
contain everything I need. One example which comes up almost every single time
are additional ExternalSecret manifests for credentials, or ObjectBucketClaims
for S3 buckets in my Ceph cluster. And those Yaml files need to go somewhere.</p>
<p>And that&rsquo;s what the <code>$chartname-addon</code> chart is for. It&rsquo;s a normal Helm chart,
including <code>Chart.yaml</code> and <code>templates/</code> directory. It also gets its own <code>values.yaml</code>
file. It gets deployed into the same Namespace as the primary chart.</p>
<p>I also trialed a different approach with some of my earliest charts. For those,
I created a &ldquo;parent&rdquo; chart, which contained the <code>Chart.yaml</code> and any additional
manifests on top of the 3rd party chart. Then said 3rd party chart got added
as a dependency. But I moved away from that approach, as I found the separation
between 3rd party chart and my own manifests in the <code>$chartname-addons</code> approach
more appealing. There was also the
fact that I couldn&rsquo;t just update the version of the 3rd party chart and then
deploy - Helm would always error out due to the <code>Chart.lock</code> file being
outdated. I moved away from this model completely.</p>
<h2 id="why-not-gitops">Why not GitOps?</h2>
<p>So the obvious question might be: Why not employ GitOps like Argo or Flux?
Mostly: Time. &#x1f601; I&rsquo;m not adverse to adding additional complexity to my
Homelab just for the fun of it. But a GitOps tool should have its own management
cluster, as it wouldn&rsquo;t make much sense to me to have e.g. ArgoCD running in
the same cluster that it&rsquo;s managing. So I skipped this option when I initially
looked for how I wanted to manage it all.</p>
<p>There&rsquo;s also the additional hassle of &ldquo;Okay, and then where will I store the
repo and execute the automation?&rdquo;. I have a Forgejo instance and Woodpecker as
CI, but both of those are deployed in my main cluster. So they would be controlled
by ArgoCD - which they would also be hosting.
But on the other hand, there&rsquo;s also the challenge to come up with something
reasonably small that can serve ArgoCD without being too much of a hassle.</p>
<p>Finally, there&rsquo;s also my current workflow: I generally work on a thing until it
works properly, and then it gets a commit in the Homelab repo. It would feel a
bit weird to make a commit for every thing I change, for no other reason than
that I need said commit to trigger a new deployment. I&rsquo;m used to this approach
from work, but there the CI triggers hundreds upon hundreds of jobs and tens of
thousands of tests. It is literally impossible to run the software on our
developer machines. But here? Making a commit for every change, pushing it just
to make a test deploy - it just feels a bit much?</p>
<p>All of the above being said - I&rsquo;d really like to hear what those of you who do
run GitOps tools to manage your cluster get out of it. What advantages does it
have for you? And what&rsquo;s your workflow? Do you perhaps always work with Helm
locally, and then let Argo do it&rsquo;s thing once everything already works? Ping me
<a href="https://social.mei-home.net/@mmeier">on the Fediverse</a>. I&rsquo;m genuinely curious.
And quite frankly, I want to be convinced - one more project for the Homelab
pile. &#x1f601;</p>
<h2 id="finale">Finale</h2>
<p>And that&rsquo;s it already for this one. I&rsquo;ve had it sitting in draft state for way
too long.</p>
<p>The next post will likely be on the setup of the tinkerbell lab, as I&rsquo;m done
with that now and have already deployed tinkerbell - but it&rsquo;s not working properly
yet.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
