<?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>Gateway API on 이영욱</title>
    <link>https://lyuk98.com/tags/gateway-api/</link>
    <description>Recent content in Gateway API on 이영욱</description>
    <image>
      <title>이영욱</title>
      <url>https://images.lyuk98.com/5052433f-7965-498c-8799-4e7ac343dbf5.avif</url>
      <link>https://images.lyuk98.com/5052433f-7965-498c-8799-4e7ac343dbf5.avif</link>
    </image>
    <generator>Hugo</generator>
    <language>en</language>
    <copyright>This work is marked CC0 1.0 Universal</copyright>
    <lastBuildDate>Tue, 28 Apr 2026 09:19:54 -0400</lastBuildDate>
    <atom:link href="https://lyuk98.com/tags/gateway-api/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Building a homelab (#2) - Setting up Argo CD</title>
      <link>https://lyuk98.com/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/</link>
      <pubDate>Tue, 28 Apr 2026 09:19:54 -0400</pubDate>
      <guid>https://lyuk98.com/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/</guid>
      <description>&lt;p&gt;This is part of series &lt;em&gt;Building a homelab&lt;/em&gt;, where I document my journey to build my own homelab with Kubernetes.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://lyuk98.com/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/&#34;&gt;Creating a Kubernetes cluster with Talos Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Setting up Argo CD&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1 id=&#34;introduction&#34;&gt;Introduction&lt;/h1&gt;
&lt;p&gt;I have previously stated &lt;a href=&#34;https://lyuk98.com/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#conclusion&#34; title=&#34;Building a homelab (#1) - Creating a Kubernetes cluster with Talos Linux&#34;&gt;at the end of my post about Talos Linux&lt;/a&gt; that the next step was to migrate existing services to my Kubernetes cluster. I currently have two of them in operation, which are &lt;a href=&#34;https://lyuk98.com/blog/2025/09/hosting-ente-with-terraform-vault-and-nixos/&#34; title=&#34;Hosting Ente with Terraform, Vault, and NixOS&#34;&gt;Ente Photos&lt;/a&gt; and &lt;a href=&#34;https://lyuk98.com/blog/2025/03/self-hosting-peertube-with-tailscale/&#34; title=&#34;Self-hosting PeerTube with Tailscale&#34;&gt;PeerTube&lt;/a&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>This is part of series <em>Building a homelab</em>, where I document my journey to build my own homelab with Kubernetes.</p>
<ol>
<li><a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/">Creating a Kubernetes cluster with Talos Linux</a></li>
<li>Setting up Argo CD</li>
</ol>
<hr>
<h1 id="introduction">Introduction</h1>
<p>I have previously stated <a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#conclusion" title="Building a homelab (#1) - Creating a Kubernetes cluster with Talos Linux">at the end of my post about Talos Linux</a> that the next step was to migrate existing services to my Kubernetes cluster. I currently have two of them in operation, which are <a href="/blog/2025/09/hosting-ente-with-terraform-vault-and-nixos/" title="Hosting Ente with Terraform, Vault, and NixOS">Ente Photos</a> and <a href="/blog/2025/03/self-hosting-peertube-with-tailscale/" title="Self-hosting PeerTube with Tailscale">PeerTube</a>.</p>
<p>With how (somewhat) effortless installing <a href="https://helm.sh/" title="Helm">Helm</a> charts with <a href="https://developer.hashicorp.com/terraform" title="Terraform | HashiCorp Developer">Terraform</a>/<a href="https://opentofu.org/" title="OpenTofu">OpenTofu</a> was, I initially considered using the infrastructure-as-code tool to deploy just about everything. However, I then realised that:</p>
<ul>
<li>not everything is packaged as Helm charts</li>
<li>applying some manifests with Terraform/OpenTofu can be problematic, especially if the resource in question is a <a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/" title="Extend the Kubernetes API with CustomResourceDefinitions | Kubernetes"><code>CustomResourceDefinition</code></a> (CRD) object</li>
</ul>
<p>The reason for the latter is that the <a href="https://search.opentofu.org/provider/hashicorp/kubernetes/v3.0.1/docs/resources/manifest" title="Kubernetes: kubernetes_manifest - hashicorp/kubernetes - OpenTofu Registry"><code>kubernetes_manifest</code> resource</a> from the <a href="https://search.opentofu.org/provider/hashicorp/kubernetes/v3.0.1" title="Provider: Kubernetes - hashicorp/kubernetes - OpenTofu Registry">Kubernetes provider</a> requires the cluster to be already set up prior to planning.</p>
<blockquote cite="https://search.opentofu.org/provider/hashicorp/kubernetes/v3.0.1/docs/resources/manifest"><p>This resource requires API access during planning time. This means the cluster has to be accessible at plan time and thus cannot be created in the same apply operation. We recommend only using this resource for custom resources or resources not yet fully supported by the provider.</p>
</blockquote>
<p>With <a href="https://search.opentofu.org/provider/siderolabs/talos/v0.10.1" title="Provider: Talos - siderolabs/talos - OpenTofu Registry">Talos Linux</a> being set up during the application, it was not going to be the case. Since I was likely better off choosing a better fit for the Kubernetes ecosystem anyway, I decided to set up a well-known &ldquo;continuous delivery tool for Kubernetes&rdquo;: <a href="https://argoproj.github.io/cd/" title="Argo CD | Argo">Argo CD</a>.</p>
<h1 id="maintaining-the-system">Maintaining the system</h1>
<p>Before starting with the deployment, though, I had a few things to do to get the cluster ready.</p>
<h2 id="hardware-the-coin-cell-battery">Hardware: the coin-cell battery</h2>
<p>The laptop that is running the cluster has <a href="/blog/2025/11/building-a-project-2-preparing-a-server/#disconnecting-the-battery" title="Building a project (#2) - Preparing a server">its main battery disconnected</a>, which was done to prevent wear.</p>
<p>Upon losing power, though, it was no longer able to retain modifications to firmware settings. It was a problem, as I had to <a href="https://www.dell.com/support/kbdoc/en-us/000123462/recommended-bios-settings-for-your-linux-system" title="Recommended BIOS Settings for your Linux System | Dell US">set &ldquo;SATA Operation&rdquo; to AHCI</a> (from RAID), which would be reverted when a power interruption happens.</p>
<p>To prevent the system from losing its settings, I decided to replace its coin-cell battery. <a href="https://dl.dell.com/Manuals/all-products/esuprt_laptop/esuprt_xps_laptop/xps-13-9350-laptop_Service%20Manual_en-us.pdf">The service manual</a> I have previously used for <a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#the-display-problem" title="Building a homelab (#1) - Creating a Kubernetes cluster with Talos Linux">disconnecting the display</a> was opened again as a reference.</p>
<p>First, I shut down the system using <code>talosctl</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;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ nix shell nixpkgs#talosctl
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[lyuk98@framework:~]$ talosctl shutdown
</span></span></code></pre></div>
<details>
  <summary>I then removed the device&rsquo;s cover, like I have previously done.</summary>
  <blockquote cite="/blog/2025/11/building-a-project-2-preparing-a-server/#disconnecting-the-battery"><p>First, I removed eight screws from the bottom of the laptop.</p>
<p><img alt="The laptop flipped upside down" loading="lazy" src="https://images.lyuk98.com/1a0e87a5-f35c-4bde-8f14-abf0073d7ad0.avif"></p>
<p>The flap in the middle was then opened, and a screw hidden underneath was removed.</p>
<p><img alt="A flap in the middle of the laptop&rsquo;s bottom cover is partially opened, with a screwdriver keeping it from closing. The screwdriver is placed on top of a screw underneath the flap." loading="lazy" src="https://images.lyuk98.com/5d43cbe9-0b19-4634-91c0-3950c54625bf.avif"></p>
</blockquote>
</details>

<p>Once it was open, I could see the RTC battery sitting beside the primary battery pack.</p>
<picture>
  <source srcset="https://images.lyuk98.com/c599441f-a760-4685-a604-25423695b5b6.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/c599441f-a760-4685-a604-25423695b5b6.webp" width="4080" height="3072" style="width: 100%; height: auto;" alt="The RTC battery, next to the main battery, plugged in to the mainboard">
</picture>
<p>It was an ML1220 rechargeable coin-cell battery. I peeled off what was wrapping up the cell and saw thin contacts seemingly soldered into the battery.</p>
<picture>
  <source srcset="https://images.lyuk98.com/b4b7b4d0-45ec-406e-92f5-bbab660b98f3.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/b4b7b4d0-45ec-406e-92f5-bbab660b98f3.webp" width="4080" height="3072" style="width: 100%; height: auto;" alt="The disconnected RTC battery, with its black plastic wrap peeled off, next to a Framework Screwdriver">
</picture>
<p>A replacement battery was to be purchased. The problem, however, was that it would take at least a few weeks for proper battery replacements (with connectors to connect to the motherboard with) to be delivered. As a result, while waiting for one, I experimented with just the cell, which arrived just a day after making an order.</p>
<p>The old cell was then separated from the connector.</p>
<picture>
  <source srcset="https://images.lyuk98.com/8923e578-b2be-4d3a-ade8-c825bb6a6692.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/8923e578-b2be-4d3a-ade8-c825bb6a6692.webp" width="4080" height="3072" style="width: 100%; height: auto;" alt="The RTC battery with its positive and negative contacts separated from the cell">
</picture>
<p>I initially considered soldering the new one next, but later felt that it is not a good idea to expose a cell to a lot of heat. Because of that, it was just wrapped with some electrical tape while making contacts.</p>
<picture>
  <source srcset="https://images.lyuk98.com/f1fa74ad-ccc8-4a38-84bb-eb85b5c12426.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/f1fa74ad-ccc8-4a38-84bb-eb85b5c12426.webp" width="4080" height="3072" style="width: 100%; height: auto;" alt="The RTC battery, with the cell wrapped with electrical tape, plugged in to the mainboard">
</picture>
<p>When the system was powered on, though, it disappointingly failed to start. Its diagnostic indicator, which blinked three times in amber and once in white, <a href="https://www.dell.com/support/kbdoc/en-us/000141206/a-reference-guide-to-the-xps-notebook-diagnostic-indicators#2014_2025" title="A Reference Guide to the XPS Laptop Diagnostic Indicators | Dell US">indicated</a> a &ldquo;CMOS battery failure&rdquo;.</p>
<p>I later realised that connecting the main battery allows the system to be usable again. With a possibility that the previous coin-cell battery only needed to be charged (and not outright be replaced), I started thinking that all the steps I have taken so far may have been for nothing.</p>
<p>I know about that now, at least. With the device running even without the RTC battery, I decided to continue in this state.</p>
<h2 id="software-upgrading-talos-linux-and-kubernetes">Software: upgrading Talos Linux and Kubernetes</h2>
<p>Since when I deployed my cluster, newer versions of both <a href="https://github.com/siderolabs/talos/releases" title="Releases · siderolabs/talos">Talos Linux</a> and <a href="https://kubernetes.io/releases/" title="Releases | Kubernetes">Kubernetes</a> were made available. Before making more modifications to the cluster, I decided to update the environment.</p>
<p>At the time I was working on this part, the newest version of Talos Linux was <a href="https://github.com/siderolabs/talos/releases/tag/v1.12.6" title="Release v1.12.6 · siderolabs/talos">v1.12.6</a>. As upgrading the cluster using Terraform/OpenTofu is <a href="https://github.com/siderolabs/terraform-provider-talos/issues/140" title="gracefull upgrades through terraform · Issue #140 · siderolabs/terraform-provider-talos">still not possible</a>, some manual steps were to be involved.</p>
<p>I first went to my repository for OpenTofu configurations. From there, I went through pretty much the same steps <a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#running-opentofu" title="Building a homelab (#1) - Creating a Kubernetes cluster with Talos Linux">as the last time</a>.</p>
<blockquote cite="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#running-opentofu"><p>Before initialisation, I prepared environment variables for <code>tofu</code> to use.</p>
<pre tabindex="0"><code>[lyuk98@framework:~/opentofu-kubernetes]$ nix-shell --pure
[nix-shell:~/opentofu-kubernetes]$ source ~/env.sh
</code></pre><p>The <code>env.sh</code> I wrote contained declarations of the following environment variables:</p>
<ul>
<li><code>AWS_SECRET_ACCESS_KEY</code>: the application key from Backblaze B2 to access the bucket with</li>
<li><code>AWS_ACCESS_KEY_ID</code>: the ID of the abovementioned application key</li>
<li><code>AWS_ENDPOINT_URL_S3</code>: Backblaze B2&rsquo;s S3 API endpoint</li>
<li><code>CLOUDFLARE_API_TOKEN</code>: the API token for Cloudflare operations</li>
<li><code>TAILSCALE_OAUTH_CLIENT_SECRET</code>: the OAuth credential used for deployment</li>
<li><code>TAILSCALE_OAUTH_CLIENT_ID</code>: the ID of the abovementioned OAuth credential</li>
</ul>
<p>On top of the above, the file also had input variables set as environment variables:</p>
<ul>
<li><code>TF_VAR_state_passphrase</code>: the passphrase used for encrypting and decrypting state and plan data</li>
<li><code>TF_VAR_cloudflare_zone_id</code>: Cloudflare zone ID</li>
<li><code>TF_VAR_node_xps13</code>: address of the node; the only possible option, with the node in maintenance mode, was the internal IP address of my home network (such as <code>192.168.0.2</code>).</li>
</ul>
<p>I then prepared another file <code>backend.tfvars</code> with just one line of backend configuration: the bucket name.</p>
<pre tabindex="0"><code>[nix-shell:~/opentofu-kubernetes]$ cat backend.tfvars
bucket = &#34;opentofu-state-kubernetes&#34;
</code></pre></blockquote>
<p>One thing I did differently this time was that I did not set <code>TF_VAR_node_xps13</code>. It <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/471ab3b86f776f084fc1481ca4402a6f8cfcf40a/variables.tf#L29-L33" title="opentofu-kubernetes/variables.tf at 471ab3b86f776f084fc1481ca4402a6f8cfcf40a · lyuk98/opentofu-kubernetes">defaults to the hostname</a>, which Tailscale&rsquo;s <a href="https://tailscale.com/docs/features/magicdns" title="MagicDNS · Tailscale Docs">MagicDNS</a> automatically resolves to the device&rsquo;s tailnet IP address.</p>
<p>I ran <code>tofu init</code>, while adding <code>-upgrade</code> to get latest versions of providers.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu init -backend-config=backend.tfvars -upgrade
</span></span></code></pre></div><p>Then, at <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/c3b637ab6c0224907274d09cea731fbd871ebd98/variables.tf" title="opentofu-kubernetes/variables.tf at c3b637ab6c0224907274d09cea731fbd871ebd98 · lyuk98/opentofu-kubernetes"><code>variables.tf</code></a>, new versions were specified.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> variable &#34;talos_version&#34; {
</span></span><span style="display:flex;"><span>   type        = string
</span></span><span style="display:flex;"><span>   description = &#34;Version of Talos Linux&#34;
</span></span><span style="display:flex;"><span><span style="color:#f92672">-  default     = &#34;v1.12.4&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+  default     = &#34;v1.12.6&#34;
</span></span></span><span style="display:flex;"><span>   nullable    = false
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> variable &#34;kubernetes_version&#34; {
</span></span><span style="display:flex;"><span>   type        = string
</span></span><span style="display:flex;"><span>   description = &#34;Version of Kubernetes to use with Talos Linux&#34;
</span></span><span style="display:flex;"><span><span style="color:#f92672">-  default     = &#34;v1.35.1&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+  default     = &#34;v1.35.3&#34;
</span></span></span><span style="display:flex;"><span>   nullable    = false
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>The changes were applied next, by running <code>tofu plan</code> and <code>tofu apply</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;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu plan -out=tfplan
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu apply tfplan
</span></span></code></pre></div><p>When it was done, I was surprised to see that Kubernetes was already updated. Talos Linux, on the other hand, was expectedly still in its previous version.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ nix shell nixpkgs#kubectl nixpkgs#talosctl
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[lyuk98@framework:~]$ kubectl version
</span></span><span style="display:flex;"><span>Client Version: v1.35.3
</span></span><span style="display:flex;"><span>Kustomize Version: v5.7.1
</span></span><span style="display:flex;"><span>Server Version: v1.35.3
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[lyuk98@framework:~]$ talosctl version
</span></span><span style="display:flex;"><span>Client:
</span></span><span style="display:flex;"><span>	Tag:         v1.12.6
</span></span><span style="display:flex;"><span>	SHA:         undefined
</span></span><span style="display:flex;"><span>	Built:       
</span></span><span style="display:flex;"><span>	Go version:  go1.26.1
</span></span><span style="display:flex;"><span>	OS/Arch:     linux/amd64
</span></span><span style="display:flex;"><span>Server:
</span></span><span style="display:flex;"><span>	NODE:        xps13
</span></span><span style="display:flex;"><span>	Tag:         v1.12.4
</span></span><span style="display:flex;"><span>	SHA:         fc8e600b
</span></span><span style="display:flex;"><span>	Built:       
</span></span><span style="display:flex;"><span>	Go version:  go1.25.7
</span></span><span style="display:flex;"><span>	OS/Arch:     linux/amd64
</span></span><span style="display:flex;"><span>	Enabled:     RBAC
</span></span></code></pre></div><p>Rebooting the node did not do anything, so I queried the OpenTofu state to see new <a href="https://factory.talos.dev/" title="Image Factory">Image Factory</a> URLs.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu state show data.talos_image_factory_urls.xps13
</span></span><span style="display:flex;"><span># data.talos_image_factory_urls.xps13:
</span></span><span style="display:flex;"><span>data &#34;talos_image_factory_urls&#34; &#34;xps13&#34; {
</span></span><span style="display:flex;"><span>    architecture  = &#34;amd64&#34;
</span></span><span style="display:flex;"><span>    platform      = &#34;metal&#34;
</span></span><span style="display:flex;"><span>    schematic_id  = &#34;19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9&#34;
</span></span><span style="display:flex;"><span>    talos_version = &#34;v1.12.6&#34;
</span></span><span style="display:flex;"><span>    urls          = {
</span></span><span style="display:flex;"><span>        disk_image            = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64.raw.zst&#34;
</span></span><span style="display:flex;"><span>        disk_image_secureboot = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64-secureboot.raw.zst&#34;
</span></span><span style="display:flex;"><span>        initramfs             = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/initramfs-amd64.xz&#34;
</span></span><span style="display:flex;"><span>        installer             = &#34;factory.talos.dev/metal-installer/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9:v1.12.6&#34;
</span></span><span style="display:flex;"><span>        installer_secureboot  = &#34;factory.talos.dev/metal-installer-secureboot/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9:v1.12.6&#34;
</span></span><span style="display:flex;"><span>        iso                   = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64.iso&#34;
</span></span><span style="display:flex;"><span>        iso_secureboot        = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64-secureboot.iso&#34;
</span></span><span style="display:flex;"><span>        kernel                = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/kernel-amd64&#34;
</span></span><span style="display:flex;"><span>        kernel_command_line   = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/cmdline-metal-amd64&#34;
</span></span><span style="display:flex;"><span>        pxe                   = &#34;https://pxe.factory.talos.dev/pxe/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64&#34;
</span></span><span style="display:flex;"><span>        uki                   = &#34;https://factory.talos.dev/image/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9/v1.12.6/metal-amd64-secureboot-uki.efi&#34;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>installer_secureboot</code> was the one for me to use. With it, the upgrade was performed using <code>talosctl upgrade</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;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ talosctl upgrade <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --image factory.talos.dev/metal-installer-secureboot/19bad511ec610d3ce5fe680fd34334e45621f0e90c7b2f621998647e7f5f5ef9:v1.12.6 \
</span></span><span style="display:flex;"><span>  --nodes xps13 \
</span></span><span style="display:flex;"><span>  --stage
</span></span></code></pre></div><p>After a long wait, the node was rebooted to finish the upgrade process.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ talosctl reboot
</span></span></code></pre></div><p>With the cluster now up to date, I moved on to actual deployments.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ talosctl version
</span></span><span style="display:flex;"><span>Client:
</span></span><span style="display:flex;"><span>	Tag:         v1.12.6
</span></span><span style="display:flex;"><span>	SHA:         undefined
</span></span><span style="display:flex;"><span>	Built:       
</span></span><span style="display:flex;"><span>	Go version:  go1.26.1
</span></span><span style="display:flex;"><span>	OS/Arch:     linux/amd64
</span></span><span style="display:flex;"><span>Server:
</span></span><span style="display:flex;"><span>	NODE:        xps13
</span></span><span style="display:flex;"><span>	Tag:         v1.12.6
</span></span><span style="display:flex;"><span>	SHA:         a1b8bd61
</span></span><span style="display:flex;"><span>	Built:       
</span></span><span style="display:flex;"><span>	Go version:  go1.25.8
</span></span><span style="display:flex;"><span>	OS/Arch:     linux/amd64
</span></span><span style="display:flex;"><span>	Enabled:     RBAC
</span></span></code></pre></div><h1 id="the-plan">The plan</h1>
<h2 id="tailscale-network-with-support-for-custom-domains-and-https">Tailscale network with support for custom domains and HTTPS</h2>
<p>I wanted to make Argo CD accessible by web browsers, while blocking access from the rest of the internet by exposing it only over the tailnet. Tailscale allows users <a href="https://tailscale.com/docs/features/kubernetes-operator/how-to/cluster-ingress" title="Expose a Kubernetes cluster workload to your tailnet (cluster ingress) · Tailscale Docs">to do just that</a>, where the easiest solution was <a href="https://tailscale.com/docs/features/kubernetes-operator/how-to/cluster-ingress#exposing-a-cluster-workload-by-annotating-an-existing-service" title="Expose a Kubernetes cluster workload to your tailnet (cluster ingress) · Tailscale Docs">to annotate an existing <code>Service</code></a>. It could have almost worked for me, but I <del>unfortunately</del> wanted one more thing: using a custom domain to access the service.</p>
<p>I initially tried to solve this in three steps:</p>
<ol>
<li>Deploy Helm chart for Argo CD with aforementioned annotation trick</li>
<li>Wait for the service to appear in the tailnet</li>
<li>Let OpenTofu create DNS records with the service&rsquo;s tailnet IP addresses</li>
</ol>
<p>However, this meant that I will have to access the service via plain HTTP, like <a href="/blog/2025/09/hosting-ente-with-terraform-vault-and-nixos/#vault" title="Hosting Ente with Terraform, Vault, and NixOS">what I am currently doing with Vault</a>. Although I am aware that network traffic between Tailscale nodes are encrypted, I thought it would be a plus to make web browsers stop complaining about insecure connections. As a result, I decided to manually expose the service, while additionally deploying <a href="https://cert-manager.io/" title="cert-manager">cert-manager</a> to issue HTTPS certificates.</p>
<h2 id="in-favour-of-gateway-api">In favour of Gateway API</h2>
<p>A common way to expose a <a href="https://kubernetes.io/docs/concepts/services-networking/service/" title="Service | Kubernetes"><code>Service</code></a> to an external network seemed to be using <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/" title="Ingress | Kubernetes"><code>Ingress</code></a>. However, I noticed that Kubernetes no longer recommends its use.</p>
<blockquote cite="https://kubernetes.io/docs/concepts/services-networking/ingress/"><p>The Kubernetes project recommends using <a href="https://gateway-api.sigs.k8s.io/">Gateway</a> instead of <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">Ingress</a>. The Ingress API has been frozen.</p>
<p>This means that:</p>
<ul>
<li>The Ingress API is generally available, and is subject to the <a href="https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-parts-of-the-api">stability guarantees</a> for generally available APIs. The Kubernetes project has no plans to remove Ingress from Kubernetes.</li>
<li>The Ingress API is no longer being developed, and will have no further changes or updates made to it.</li>
</ul>
</blockquote>
<p>With <a href="https://kubernetes.github.io/ingress-nginx/" title="Welcome - Ingress-Nginx Controller">Ingress Nginx</a> already retired, and <a href="https://github.com/kubernetes/ingress-nginx" title="kubernetes/ingress-nginx: Ingress NGINX Controller for Kubernetes">its repository</a> archived, I did not want my cluster to have a known dependency to a retiring technology right from the beginning. Despite the relative lack of documentation, I decided to adopt <a href="https://gateway-api.sigs.k8s.io/" title="Introduction - Kubernetes Gateway API">Gateway API</a>, choosing <a href="https://cilium.io/" title="Cilium - Cloud Native, eBPF-based Networking, Observability, and Security">Cilium</a> as its implementation (because it is already present in the cluster).</p>
<h2 id="managing-dns-records-from-kubernetes">Managing DNS records from Kubernetes</h2>
<p>While searching for resources on using Gateway API, I found <a href="https://tailscale.com/docs/solutions/kubernetes-operator-byod-gateway-api" title="Use custom domains with Kubernetes Gateway API and Tailscale · Tailscale Docs">a documentation from Tailscale</a> that does almost exactly what I wanted. A difference, though, was that I intended to use Cilium instead of <a href="https://www.envoyproxy.io/" title="Envoy proxy - home">Envoy</a> as the Gateway API implementation (although I believe the former does use the latter in some capacity).</p>
<p>The aforementioned documentation also mentioned <a href="https://kubernetes-sigs.github.io/external-dns/latest/" title="external-dns">ExternalDNS</a>, which would automatically create DNS records. I felt it was much more reliable than letting OpenTofu do so, eventually also deciding to deploy this service as a result.</p>
<h2 id="using-argo-cd-to-deploy-argo-cd-in-some-way">Using Argo CD to deploy Argo CD (in some way)</h2>
<p>At first, I very much wanted to deploy everything as Helm charts, due to their seemingly less maintenance burden. Some resources (like <a href="https://cert-manager.io/docs/concepts/issuer/" title="Issuer - cert-manager Documentation"><code>ClusterIssuer</code></a>), however, were not automatically created by them. Even worse, using Helm to deploy those additional resources, such as by setting the chart&rsquo;s <a href="https://artifacthub.io/packages/helm/cert-manager/cert-manager#:~:text=extraObjects"><code>extraObjects</code></a> value for cert-manager, was not very reliable; in some cases, user-specified manifests were attempted to be installed before CRDs for them were even available.</p>
<p>Eventually, I planned to create Argo CD&rsquo;s <a href="https://argo-cd.readthedocs.io/en/release-3.3/core_concepts/" title="Core Concepts - Argo CD - Declarative GitOps CD for Kubernetes"><code>Application</code></a> resources that are synchronised with a repository that contains additional manifests. To deploy them before the service is properly set up, I chose <a href="https://github.com/argoproj/argo-helm/tree/argo-cd-9.5.0/charts/argocd-apps" title="argo-helm/charts/argocd-apps at main · argoproj/argo-helm">argocd-apps</a> from a collection of community-maintained Argo Helm charts.</p>
<p>Charts for services themselves (including Cilium, Tailscale Kubernetes Operator, cert-manager, ExternalDNS, and Argo CD) were to be installed in the same way as before, using the <a href="https://search.opentofu.org/provider/hashicorp/helm/v3.1.1/docs/resources/release" title="helm: helm_release - hashicorp/helm - OpenTofu Registry"><code>helm_release</code></a> resource. For other resources, a set of <code>Application</code> objects was to handle their deployments.</p>
<h2 id="talos-linux--cilium--tailscale--gateway-api--cert-manager--externaldns">Talos Linux + Cilium + Tailscale + Gateway API + cert-manager + ExternalDNS</h2>
<p>At this point, I have not seen anyone attempt to deploy Argo CD with this combination of services. I was somewhat excited to be one of the few (if not the only one) to document doing so, but at the same time, it meant that resources I find online have to be modified to fit my needs. It is not like I am usually complacent with solutions I think could be improved, though.</p>
<p><del>It was not supposed to be this complicated&hellip;</del></p>
<h1 id="writing-configurations">Writing configurations</h1>
<h2 id="a-little-bit-of-refactoring">A little bit of refactoring</h2>
<p>I started by moving all <code>provider</code> declarations, like the following block for Helm, to <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/main.tf" title="opentofu-kubernetes/main.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>main.tf</code></a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm package manager
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;helm&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">kubernetes</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">host</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">local</span>.<span style="color:#a6e22e">cluster_endpoint</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_certificate</span>     <span style="color:#f92672">=</span> base64decode(<span style="color:#a6e22e">talos_cluster_kubeconfig</span>.<span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">kubernetes_client_configuration</span>.<span style="color:#a6e22e">client_certificate</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">client_key</span>             <span style="color:#f92672">=</span> base64decode(<span style="color:#a6e22e">talos_cluster_kubeconfig</span>.<span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">kubernetes_client_configuration</span>.<span style="color:#a6e22e">client_key</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cluster_ca_certificate</span> <span style="color:#f92672">=</span> base64decode(<span style="color:#a6e22e">talos_cluster_kubeconfig</span>.<span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">kubernetes_client_configuration</span>.<span style="color:#a6e22e">ca_certificate</span>)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Versions for Helm charts were then specified <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">as variables</a>, which would help make changes to them without editing the 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;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cilium_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the Helm chart for Cilium&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1.19.2&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;tailscale_operator_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the Helm chart for Tailscale Operator&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1.96.5&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>helm.tf</code> was deleted and charts declared there were moved. <code>helm_release.cilium</code> was moved to <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cilium.tf" title="opentofu-kubernetes/cilium.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>cilium.tf</code></a>, and <code>helm_release.tailscale_operator</code> was moved to <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/tailscale.tf" title="opentofu-kubernetes/tailscale.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>tailscale.tf</code></a>.</p>
<p>Node-specific Tailscale resources were then moved to <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/talos-xps13.tf" title="opentofu-kubernetes/talos-xps13.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>talos-xps13.tf</code></a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># OAuth client for node (XPS 13)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;tailscale_oauth_client&#34;</span> <span style="color:#e6db74">&#34;xps13&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">scopes</span>      <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;auth_keys&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">local</span>.<span style="color:#a6e22e">hostnames</span>.<span style="color:#a6e22e">control_plane</span>.<span style="color:#a6e22e">xps13</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">tags</span>        <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;tag:k8s-control-plane&#34;</span>, <span style="color:#e6db74">&#34;tag:k8s-worker&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Tailnet device information (XPS 13)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;tailscale_device&#34;</span> <span style="color:#e6db74">&#34;xps13&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">talos_machine_configuration_apply</span>.<span style="color:#a6e22e">xps13</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">hostname</span>   <span style="color:#f92672">=</span> <span style="color:#a6e22e">local</span>.<span style="color:#a6e22e">hostnames</span>.<span style="color:#a6e22e">control_plane</span>.<span style="color:#a6e22e">xps13</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">wait_for</span>   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10m&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Lastly, <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/" title="Namespaces | Kubernetes"><code>Namespace</code></a> for Tailscale was set to be managed by OpenTofu, instead of being created upon the Helm chart&rsquo;s installation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Kubernetes Namespace (Tailscale)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_namespace_v1&#34;</span> <span style="color:#e6db74">&#34;tailscale&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadata</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tailscale&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">labels</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # Enforce &#34;privileged&#34; Pod Security Standards policy
</span></span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;pod-security.kubernetes.io/enforce&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;privileged&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <a href="https://kubernetes.io/docs/concepts/security/pod-security-standards/" title="Pod Security Standards | Kubernetes">Pod Security Standards</a> policy for resources in this <code>Namespace</code> was set to <em>Privileged</em>, as it is apparently required to avoid <a href="https://github.com/tailscale/tailscale/issues/11143" title="Kubernetes load balancer services stay pending · Issue #11143 · tailscale/tailscale">problems</a> in setting up Tailscale&rsquo;s <a href="https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/" title="Create an External Load Balancer | Kubernetes">load balancer</a> that will be used for Gateway API configuration later.</p>
<h2 id="gateway-api">Gateway API</h2>
<p>As <a href="/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/#in-favour-of-gateway-api" title="Building a homelab (#2) - Setting up Argo CD">mentioned earlier</a>, Cilium was my choice of Gateway API implementation. <a href="https://docs.cilium.io/en/v1.19/network/servicemesh/gateway-api/gateway-api/" title="Gateway API Support — Cilium 1.19.3 documentation">Their documentation</a> listed a few prerequisites for enabling the feature, which I followed next.</p>
<h3 id="the-kube-proxy-replacement">The kube-proxy replacement</h3>
<blockquote>
<ul>
<li>Cilium must be configured with the kube-proxy replacement, using <code>kubeProxyReplacement=true</code>. For more information, see <a href="https://docs.cilium.io/en/v1.19/network/kubernetes/kubeproxy-free/#kubeproxy-free">kube-proxy replacement</a>.</li>
</ul>
</blockquote>
<p>Adding <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cilium.tf#L91-L95" title="opentofu-kubernetes/cilium.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">one additional object</a> to <a href="https://search.opentofu.org/provider/hashicorp/helm/v3.1.1/docs/resources/release#schema" title="helm: helm_release - hashicorp/helm - OpenTofu Registry"><code>set</code></a> was apparently enough.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Cilium)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;cilium&#34;</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # ...
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">set</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # Configure the kube-proxy replacement
</span></span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubeProxyReplacement&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    },<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span>  ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>However, I then remembered that Talos Linux had <a href="https://docs.siderolabs.com/talos/v1.12/reference/configuration/v1alpha1/config#proxy" title="MachineConfig - Sidero Documentation">an option to disable kube-proxy</a>, which I <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/talos-controlplane.tf#L31-L38" title="opentofu-kubernetes/talos-controlplane.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">added next</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#a6e22e">locals</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Patches for control plane nodes
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">talos_patches_controlplane</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # Disable kube-proxy
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">disable_proxy</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">cluster</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">proxy</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">disabled</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="gateway-api-crds">Gateway API CRDs</h3>
<blockquote>
<ul>
<li>The below CRDs from Gateway API v1.4.1 <code>must</code> be pre-installed. Please refer to this <a href="https://gateway-api.sigs.k8s.io/guides/?h=crds#getting-started-with-gateway-api">docs</a> for installation steps. Alternatively, the below snippet could be used.
<ul>
<li><a href="https://gateway-api.sigs.k8s.io/api-types/gatewayclass/">GatewayClass</a></li>
<li><a href="https://gateway-api.sigs.k8s.io/api-types/gateway/">Gateway</a></li>
<li><a href="https://gateway-api.sigs.k8s.io/api-types/httproute/">HTTPRoute</a></li>
<li><a href="https://gateway-api.sigs.k8s.io/api-types/grpcroute/">GRPCRoute</a></li>
<li><a href="https://gateway-api.sigs.k8s.io/api-types/referencegrant/">ReferenceGrant</a></li>
</ul>
</li>
</ul>
</blockquote>
<p>From what I understood, this meant that the CRDs must exist before Cilium; I had to find a way to somehow install them with OpenTofu.</p>
<p>Although I could not find an official Helm chart for the resources, their repository had <a href="https://github.com/kubernetes-sigs/gateway-api/blob/v1.4.1/config/crd/kustomization.yaml" title="gateway-api/config/crd/kustomization.yaml at v1.4.1 · kubernetes-sigs/gateway-api">a <code>Kustomization</code> resource</a>, which could be used to install them with <a href="https://search.opentofu.org/provider/kbst/kustomization/v0.9.7" title="index - kbst/kustomization - OpenTofu Registry">the Kustomize provider</a>.</p>
<p>I first <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L84-L89" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">added a variable</a> to declare the version of Gateway API CRDs to use. The latest version at the time of writing is <a href="https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.5.1" title="Release v1.5.1 · kubernetes-sigs/gateway-api">v1.5.1</a>, but given that Cilium specifically requires <a href="https://github.com/kubernetes-sigs/gateway-api/releases/tag/v1.4.1" title="Release v1.4.1 · kubernetes-sigs/gateway-api">v1.4.1</a> and that v1.5 does not properly work with the implementation, I could not choose the newer version.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;gateway_api_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of Gateway API CRDs&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;v1.4.1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>(It was a pain to roll back from v1.5 to v1.4, as a <a href="https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/" title="Validating Admission Policy | Kubernetes">validating admission policy</a> was introduced in the newer version to prevent downgrades.)</p>
<p>The provider was then set up by <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/main.tf#L55-L58" title="opentofu-kubernetes/main.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">adding it to the <code>required_providers</code> block</a> and <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/main.tf#L107-L110" title="opentofu-kubernetes/main.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">declaring a <code>provider</code> block</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#a6e22e">terraform</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # ...
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">required_providers</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kustomization</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">source</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kbst/kustomization&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">version</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 0.9.7&#34;</span>
</span></span><span style="display:flex;"><span>    }<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Kustomization provider
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;kustomization&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">kubeconfig_raw</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">talos_cluster_kubeconfig</span>.<span style="color:#a6e22e">kubernetes</span>.<span style="color:#a6e22e">kubeconfig_raw</span>
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># ...
</span></span></span></code></pre></div><p>Then, using the <a href="https://search.opentofu.org/provider/kbst/kustomization/v0.9.7/docs/datasources/build" title="build - kbst/kustomization - OpenTofu Registry"><code>kustomization_build</code></a> data source, manifests for the <a href="https://search.opentofu.org/provider/kbst/kustomization/v0.9.7/docs/resources/resource" title="resource - kbst/kustomization - OpenTofu Registry"><code>kustomization_resource</code></a> resource to apply to the cluster <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cilium.tf#L1-L4" title="opentofu-kubernetes/cilium.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">were set to be generated</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Kustomization - Gateway API CRDs
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;kustomization_build&#34;</span> <span style="color:#e6db74">&#34;gateway_api&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github.com/kubernetes-sigs/gateway-api/config/crd?ref=</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">urlencode</span>(var.<span style="color:#a6e22e">gateway_api_version</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>It would download the <code>Kustomization</code> resource over Git and build the manifest. Because this operation needs Git itself as well as public Certificate Authorities (CA) to communicate over HTTPS, corresponding packages <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/default.nix#L9-L10" title="opentofu-kubernetes/default.nix at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">were added</a> to <code>default.nix</code> for the <code>nix-shell</code> environment.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  pkgs <span style="color:#f92672">?</span> <span style="color:#f92672">import</span> <span style="color:#e6db74">&lt;nixpkgs&gt;</span> { }<span style="color:#f92672">,</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">...</span>
</span></span><span style="display:flex;"><span>}:
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># The default development environment</span>
</span></span><span style="display:flex;"><span>  default <span style="color:#f92672">=</span> pkgs<span style="color:#f92672">.</span>mkShellNoCC {
</span></span><span style="display:flex;"><span>    nativeBuildInputs <span style="color:#f92672">=</span> <span style="color:#66d9ef">with</span> pkgs; [
</span></span><span style="display:flex;"><span>      cacert <span style="color:#75715e"># for fetching from Git over HTTPS</span>
</span></span><span style="display:flex;"><span>      git
</span></span><span style="display:flex;"><span>      opentofu
</span></span><span style="display:flex;"><span>      python3
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The manifests were then set to be applied to the cluster. Because some resources apparently need to be applied before the others, I followed <a href="https://search.opentofu.org/provider/kbst/kustomization/v0.9.7/docs/resources/resource#provider-example" title="resource - kbst/kustomization - OpenTofu Registry">the provider&rsquo;s example</a> to apply them sequentially based on their priorities.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Gateway API CRDs - priority 0
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kustomization_resource&#34;</span> <span style="color:#e6db74">&#34;gateway_api_p0&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">for_each</span> <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">ids_prio</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span> <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">manifests</span>[each.<span style="color:#a6e22e">value</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Gateway API CRDs - priority 1
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kustomization_resource&#34;</span> <span style="color:#e6db74">&#34;gateway_api_p1&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">kustomization_resource</span>.<span style="color:#a6e22e">gateway_api_p0</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">for_each</span>   <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">ids_prio</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span>   <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">manifests</span>[each.<span style="color:#a6e22e">value</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Gateway API CRDs - priority 2
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kustomization_resource&#34;</span> <span style="color:#e6db74">&#34;gateway_api_p2&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">kustomization_resource</span>.<span style="color:#a6e22e">gateway_api_p1</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">for_each</span>   <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">ids_prio</span>[<span style="color:#ae81ff">2</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">manifest</span>   <span style="color:#f92672">=</span> data.<span style="color:#a6e22e">kustomization_build</span>.<span style="color:#a6e22e">gateway_api</span>.<span style="color:#a6e22e">manifests</span>[each.<span style="color:#a6e22e">value</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="modifying-cilium">Modifying Cilium</h3>
<p>After the prerequisites were dealt with, the chart resource for Cilium <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cilium.tf#L26" title="opentofu-kubernetes/cilium.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was edited</a> to <code>depends_on</code> the Gateway API resources. Some additional values <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cilium.tf#L96-L110" title="opentofu-kubernetes/cilium.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">were specified</a> as well, which include Envoy-related configurations that I would later need for using Tailscale&rsquo;s load balancer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Cilium)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;cilium&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">kustomization_resource</span>.<span style="color:#a6e22e">gateway_api_p2</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cilium&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cilium&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://helm.cilium.io/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kube-system&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">cilium_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">set</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # ...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # Enable Cilium&#39;s Gateway API implementation
</span></span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;gatewayAPI.enabled&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    },<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # Enable dedicated Envoy proxy DaemonSet
</span></span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;envoy.enabled&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    },<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">    # Enable CiliumEnvoyConfig CRD
</span></span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">name</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;envoyConfig.enabled&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # ...
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="cert-manager-and-externaldns">cert-manager and ExternalDNS</h2>
<h3 id="creating-namespace">Creating <code>Namespace</code></h3>
<p>First, new <code>Namespace</code> objects were set to be created by OpenTofu.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Kubernetes Namespace (cert-manager)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_namespace_v1&#34;</span> <span style="color:#e6db74">&#34;cert_manager&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadata</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Kubernetes Namespace (ExternalDNS)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_namespace_v1&#34;</span> <span style="color:#e6db74">&#34;external_dns&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadata</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external-dns&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="creating-secret">Creating <code>Secret</code></h3>
<p>The <a href="https://kubernetes.io/docs/concepts/configuration/secret/" title="Secrets | Kubernetes"><code>Secret</code></a> resources containing the Cloudflare API token, for cert-manager to <a href="https://cert-manager.io/docs/configuration/acme/dns01/cloudflare/" title="Cloudflare - cert-manager Documentation">perform DNS-01 challenges</a> (because HTTP-01 challenges are not possible for services over the tailnet) and for ExternalDNS to modify DNS records, were next. I could not figure out how either cert-manager or ExternalDNS can refer to a <code>Secret</code> from a different <code>Namespace</code>, so I used a <a href="https://opentofu.org/docs/language/meta-arguments/for_each/" title="The for_each Meta-Argument | OpenTofu"><code>for_each</code> argument</a> for the resource <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L8-L29" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">to be applied</a> in two separate places.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Secret containing Cloudflare API Token
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_secret_v1&#34;</span> <span style="color:#e6db74">&#34;cloudflare_api_token&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">cert_manager</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">external_dns</span>
</span></span><span style="display:flex;"><span>  ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # Deploy to each Namespace
</span></span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">for_each</span> <span style="color:#f92672">=</span> toset([<span style="color:#e6db74">&#34;cert-manager&#34;</span>, <span style="color:#e6db74">&#34;external-dns&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadata</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloudflare-api-token&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> each.<span style="color:#a6e22e">value</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">immutable</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Opaque&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  data <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">api</span><span style="color:#f92672">-</span><span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">cloudflare_api_token</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I decided to store <a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#preparing-cloudflare">the same Cloudflare API token</a> that OpenTofu itself uses, because <a href="https://search.opentofu.org/provider/cloudflare/cloudflare/v5.18.0/docs/resources/api_token">creating one</a> from the infrastructure-as-code tool seemed a bit complicated. <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L21-L26" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">A new variable</a> to accept the existing token was first created.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cloudflare_api_token&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;API token for Cloudflare operations&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sensitive</span>   <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The Cloudflare provider <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/main.tf#L82-L85" title="opentofu-kubernetes/main.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was then configured</a> to use the variable for its API token instead.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Cloudflare resource management
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;cloudflare&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">api_token</span> <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">cloudflare_api_token</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="installing-helm-charts">Installing Helm charts</h3>
<p>Like before, versions of services&rsquo; charts were declared as variables.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;cert_manager_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the Helm chart for cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1.20.2&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;external_dns_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the Helm chart for ExternalDNS&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;1.20.0&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The chart for cert-manager was first <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cert-manager.tf#L19-L47" title="opentofu-kubernetes/cert-manager.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">set to be installed</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (cert-manager)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;cert_manager&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">cert_manager</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">cilium</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://charts.jetstack.io/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">cert_manager_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">crds</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">enabled</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">config</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">enableGatewayAPI</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L31-L88" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">chart for ExternalDNS</a> was next. Some supplied <code>values</code> indicated:</p>
<ul>
<li><a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L51-L54" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">Cloudflare is to be used</a> as the DNS provider</li>
<li>the API token is going to be provided by <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L57-L66" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">the previously defined <code>Secret</code></a></li>
<li>ExternalDNS <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L69-L72" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">will only affect</a> my domain&rsquo;s records</li>
<li>only the resources that set the label <code>external-dns: enabled</code> <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L74-L75" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">will be set up</a></li>
<li>on top of the defaults (<code>Service</code> and <code>Ingress</code>), ExternalDNS <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/external-dns.tf#L77-L83" title="opentofu-kubernetes/external-dns.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">will also watch</a> <code>HTTPRoute</code> and <code>GRPCRoute</code> resources from Gateway API</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (ExternalDNS)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;external_dns&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_secret_v1</span>.<span style="color:#a6e22e">cloudflare_api_token</span>[<span style="color:#e6db74">&#34;external-dns&#34;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">cilium</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external-dns&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external-dns&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://kubernetes-sigs.github.io/external-dns/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external-dns&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">external_dns_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">provider</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # Use Cloudflare as the DNS provider
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloudflare&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">env</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # Cloudflare API token
</span></span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;CF_API_TOKEN&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">valueFrom</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">secretKeyRef</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cloudflare-api-token&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">key</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;api-token&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # Limit target zone to personal domain
</span></span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">domainFilters</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        data.<span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">default</span>.<span style="color:#a6e22e">name</span>
</span></span><span style="display:flex;"><span>      ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # Only watch resources with the following label
</span></span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">labelFilter</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;external-dns==enabled&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # Query Gateway API resources for endpoints
</span></span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">sources</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;service&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;ingress&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;gateway-httproute&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;gateway-grpcroute&#34;</span>
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">timeout</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">600</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="argo-cd">Argo CD</h2>
<h3 id="setting-up-the-chart">Setting up the chart</h3>
<p>A <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L49-L54" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">variable</a>, a <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/argocd.tf#L23-L28" title="opentofu-kubernetes/argocd.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>Namespace</code></a>, and a <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/argocd.tf#L30-L62" title="opentofu-kubernetes/argocd.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">chart</a> were defined first.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;argocd_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the Helm chart for Argo CD&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;9.5.0&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Kubernetes Namespace (Argo CD)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;kubernetes_namespace_v1&#34;</span> <span style="color:#e6db74">&#34;argocd&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">metadata</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Argo CD)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;argocd&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">argocd</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">cilium</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argo-cd&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argo-cd&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://argoproj.github.io/argo-helm&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">argocd_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">      # Set domain for all components
</span></span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">global</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">domain</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argo-cd.tailnet.</span><span style="color:#e6db74">${</span>data.<span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">default</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">configs</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">params</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # Run API server with TLS disabled for SSL termination
</span></span></span><span style="display:flex;"><span>          <span style="color:#e6db74">&#34;server.insecure&#34;</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Since my main objective was to encrypt the traffic between the user and the cluster (and not the one within), <a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" title="TLS termination proxy - Wikipedia">TLS termination</a> was to be set up. As such, Argo CD&rsquo;s API server had its own TLS settings disabled.</p>
<figure><a href="https://commons.wikimedia.org/wiki/File:SSL_termination_proxy.svg">
    <img loading="lazy" src="https://upload.wikimedia.org/wikipedia/commons/3/34/SSL_termination_proxy.svg"
         alt="The SSL termination proxy decrypts incoming HTTPS traffic and forwards it to a webservice."/> </a><figcaption>
            <p><a href="https://commons.wikimedia.org/wiki/File:SSL_termination_proxy.svg">File:SSL termination proxy.svg</a> by <a href="https://commons.wikimedia.org/w/index.php?title=User:Galgalesh">User:Galgalesh</a> is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0</a></p>
        </figcaption>
</figure>

<h3 id="new-repository-and-kustomization">New repository and <code>Kustomization</code></h3>
<p>I created <a href="https://github.com/lyuk98/argocd-kubernetes" title="lyuk98/argocd-kubernetes: Argo CD Application (Kubernetes)">a new repository at GitHub</a> to separately store manifests. From there, I wrote three <code>Kustomization</code> resources with the following directory structure:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ nix shell nixpkgs#tree
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[lyuk98@framework:~]$ tree ~/argocd-kubernetes/
</span></span><span style="display:flex;"><span>/home/lyuk98/argocd-kubernetes/
</span></span><span style="display:flex;"><span>├── argocd
</span></span><span style="display:flex;"><span>│   ├── argocd-grpc-route.yaml
</span></span><span style="display:flex;"><span>│   ├── argocd-http-route.yaml
</span></span><span style="display:flex;"><span>│   └── kustomization.yaml
</span></span><span style="display:flex;"><span>├── cert-manager
</span></span><span style="display:flex;"><span>│   ├── kustomization.yaml
</span></span><span style="display:flex;"><span>│   ├── letsencrypt-staging.yaml
</span></span><span style="display:flex;"><span>│   └── letsencrypt.yaml
</span></span><span style="display:flex;"><span>├── README.md
</span></span><span style="display:flex;"><span>└── tailscale
</span></span><span style="display:flex;"><span>    ├── gateway-class-tailscale.yaml
</span></span><span style="display:flex;"><span>    ├── gateway-config-tailscale.yaml
</span></span><span style="display:flex;"><span>    ├── gateway-tailscale.yaml
</span></span><span style="display:flex;"><span>    └── kustomization.yaml
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>4 directories, 11 files
</span></span></code></pre></div><p>The reason why I did not implement the <a href="https://argo-cd.readthedocs.io/en/release-3.3/operator-manual/cluster-bootstrapping/#app-of-apps-pattern" title="Cluster Bootstrapping - Argo CD - Declarative GitOps CD for Kubernetes">app of apps pattern</a> is because I wanted to keep <code>Application</code> resources within the OpenTofu configuration; it felt like the easiest way <a href="https://argo-cd.readthedocs.io/en/release-3.3/user-guide/kustomize/#patches" title="Kustomize - Argo CD - Declarative GitOps CD for Kubernetes">to patch <code>Kustomization</code> resources</a> with sensitive infrastructure details.</p>
<h4 id="kustomization-cert-manager"><code>Kustomization</code>: cert-manager</h4>
<p>Pretty much the only significant thing <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/cert-manager/kustomization.yaml" title="argocd-kubernetes/cert-manager/kustomization.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>kustomization.yaml</code></a> was there for was letting Argo CD include other manifests.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.config.k8s.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
</span></span><span style="display:flex;"><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">kustomize-cert-manager</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">cert-manager</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">letsencrypt-staging.yaml</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">letsencrypt.yaml</span>
</span></span></code></pre></div><p>Cluster-wide issuer objects using DNS-01 challenges were then defined, where <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/cert-manager/letsencrypt-staging.yaml" title="argocd-kubernetes/cert-manager/letsencrypt-staging.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>letsencrypt-staging.yaml</code></a> uses <a href="https://letsencrypt.org/docs/staging-environment/" title="Staging Environment - Let&#39;s Encrypt">the staging environment from Let&rsquo;s Encrypt</a>.</p>
<table>
  <thead>
      <tr>
          <th><a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/cert-manager/letsencrypt-staging.yaml" title="argocd-kubernetes/cert-manager/letsencrypt-staging.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>letsencrypt-staging.yaml</code></a></th>
          <th><a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/cert-manager/letsencrypt.yaml" title="argocd-kubernetes/cert-manager/letsencrypt.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>letsencrypt.yaml</code></a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">cert-manager.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterIssuer</span>
</span></span><span style="display:flex;"><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">letsencrypt-staging</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">cert-manager</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">acme</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">email</span>: <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">profile</span>: <span style="color:#ae81ff">shortlived</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#ae81ff">https://acme-staging-v02.api.letsencrypt.org/directory</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privateKeySecretRef</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">issuer-account-key-staging</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">solvers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">dns01</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cloudflare</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">apiTokenSecretRef</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cloudflare-api-token</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">key</span>: <span style="color:#ae81ff">api-token</span></span></span></code></pre></div></td>
          <td><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">cert-manager.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterIssuer</span>
</span></span><span style="display:flex;"><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">letsencrypt</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">cert-manager</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">acme</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">email</span>: <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">profile</span>: <span style="color:#ae81ff">shortlived</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#ae81ff">https://acme-v02.api.letsencrypt.org/directory</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privateKeySecretRef</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">issuer-account-key</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">solvers</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">dns01</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">cloudflare</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">apiTokenSecretRef</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cloudflare-api-token</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">key</span>: <span style="color:#ae81ff">api-token</span></span></span></code></pre></div></td>
      </tr>
  </tbody>
</table>
<p>The email (<code>spec.acme.email</code>) was set to blank, which OpenTofu <a href="/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/#application-cert-manager" title="Building a homelab (#2) - Setting up Argo CD">will later patch</a>. Aside from that, <a href="https://letsencrypt.org/docs/profiles/#shortlived" title="Profiles - Let&#39;s Encrypt">the shortlived profile</a> was to be used for issuing certificates; just 160 hours of validity period was not an issue to me, since everything was going to be automated anyway.</p>
<h4 id="kustomization-tailscale"><code>Kustomization</code>: Tailscale</h4>
<p>Just like earlier, <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/tailscale/kustomization.yaml" title="argocd-kubernetes/tailscale/kustomization.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>kustomization.yaml</code></a> included other manifests as <code>resources</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;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.config.k8s.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
</span></span><span style="display:flex;"><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">kustomize-tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">gateway-class-tailscale.yaml</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">gateway-config-tailscale.yaml</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">gateway-tailscale.yaml</span>
</span></span></code></pre></div><p>To perform the equivalent of <a href="https://tailscale.com/docs/solutions/kubernetes-operator-byod-gateway-api#step-2-configure-envoyproxy-with-tailscale-integration" title="Use custom domains with Kubernetes Gateway API and Tailscale · Tailscale Docs">configuring <code>EnvoyProxy</code></a> (to use <code>tailscale</code> as its load balancer) with Cilium instead, a <a href="https://docs.cilium.io/en/v1.19/network/servicemesh/gateway-api/parameterized-gatewayclass/#reference" title="GatewayClass Parameters Support — Cilium 1.19.3 documentation"><code>CiliumGatewayClassConfig</code></a> resource <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/tailscale/gateway-config-tailscale.yaml" title="argocd-kubernetes/tailscale/gateway-config-tailscale.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes">was created</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># Gateway configuration, using Tailscale as a LoadBalancer provider</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">cilium.io/v2alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumGatewayClassConfig</span>
</span></span><span style="display:flex;"><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">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">LoadBalancer</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">loadBalancerClass</span>: <span style="color:#ae81ff">tailscale</span>
</span></span></code></pre></div><p>The <a href="https://gateway-api.sigs.k8s.io/api-types/gatewayclass/" title="GatewayClass - Kubernetes Gateway API"><code>GatewayClass</code></a> resource, that refers to the Gateway configuration that was just created, <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/tailscale/gateway-class-tailscale.yaml" title="argocd-kubernetes/tailscale/gateway-class-tailscale.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes">was defined next</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># GatewayClass configuration</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GatewayClass</span>
</span></span><span style="display:flex;"><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">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">controllerName</span>: <span style="color:#ae81ff">io.cilium/gateway-controller</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parametersRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">group</span>: <span style="color:#ae81ff">cilium.io</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumGatewayClassConfig</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span></code></pre></div><p>Lastly, a <a href="https://gateway-api.sigs.k8s.io/api-types/gateway/" title="Gateway - Kubernetes Gateway API"><code>Gateway</code></a> was defined. As far as I could tell, this resource was meant to handle traffic for all tailnet-only services.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># Tailscale Gateway specification</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Gateway</span>
</span></span><span style="display:flex;"><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">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">external-dns</span>: <span style="color:#ae81ff">enabled</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">annotations</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cert-manager.io/cluster-issuer</span>: <span style="color:#ae81ff">letsencrypt</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">gatewayClassName</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">listeners</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">https</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">protocol</span>: <span style="color:#ae81ff">HTTPS</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">port</span>: <span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">hostname</span>: <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">allowedRoutes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">namespaces</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">from</span>: <span style="color:#ae81ff">All</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">Terminate</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">certificateRefs</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tailnet-certificate</span>
</span></span></code></pre></div><p>The label <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/tailscale/gateway-tailscale.yaml#L9" title="argocd-kubernetes/tailscale/gateway-tailscale.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes">was set</a> to what ExternalDNS expects, and the annotation for cert-manager <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/tailscale/gateway-tailscale.yaml#L11" title="argocd-kubernetes/tailscale/gateway-tailscale.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes">was set</a> to use the production environment from Let&rsquo;s Encrypt.</p>
<p>The <code>hostname</code> this <code>Gateway</code> will listen to was not explicitly specified, but it was later to <a href="/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/#application-tailscale" title="Building a homelab (#2) - Setting up Argo CD">be patched by OpenTofu</a> to contain a wildcard (like <code>*.tailnet.[custom-domain]</code>). This was because I was worried about hitting <a href="https://letsencrypt.org/docs/rate-limits/" title="Rate Limits - Let&#39;s Encrypt">rate limits from Let&rsquo;s Encrypt</a> after issuing too many certificates for all the potential services that will be served over the tailnet.</p>
<h4 id="kustomization-argo-cd"><code>Kustomization</code>: Argo CD</h4>
<p>For Argo CD, two resources that <a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/argocd/kustomization.yaml"><code>kustomization.yaml</code></a> includes are <a href="https://gateway-api.sigs.k8s.io/api-types/httproute/" title="HTTPRoute - Kubernetes Gateway API"><code>HTTPRoute</code></a> and <a href="https://gateway-api.sigs.k8s.io/api-types/grpcroute/" title="GRPCRoute - Kubernetes Gateway API"><code>GRPCRoute</code></a> that Argo CD wrote about <a href="https://argo-cd.readthedocs.io/en/release-3.3/operator-manual/ingress/#cilium-gateway-api-example" title="Ingress Configuration - Argo CD - Declarative GitOps CD for Kubernetes">in their documentation</a>.</p>
<table>
  <thead>
      <tr>
          <th><a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/argocd/argocd-http-route.yaml" title="argocd-kubernetes/argocd/argocd-http-route.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>argocd-http-route.yaml</code></a></th>
          <th><a href="https://github.com/lyuk98/argocd-kubernetes/blob/8116239483ee68c4310e270478cb0d8d1f6682fb/argocd/argocd-grpc-route.yaml" title="argocd-kubernetes/argocd/argocd-grpc-route.yaml at 8116239483ee68c4310e270478cb0d8d1f6682fb · lyuk98/argocd-kubernetes"><code>argocd-grpc-route.yaml</code></a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># Gateway API - HTTPRoute</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">HTTPRoute</span>
</span></span><span style="display:flex;"><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">argocd-http-route</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">argocd</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parentRefs</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hostnames</span>: []
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">backendRefs</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">argo-cd-argocd-server</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matches</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">path</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">type</span>: <span style="color:#ae81ff">PathPrefix</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">value</span>: <span style="color:#ae81ff">/</span></span></span></code></pre></div></td>
          <td><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># Gateway API - GRPCRoute</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">gateway.networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">GRPCRoute</span>
</span></span><span style="display:flex;"><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">argocd-grpc-route</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">argocd</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parentRefs</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">tailscale</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hostnames</span>: []
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">backendRefs</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">argo-cd-argocd-server</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matches</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">headers</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Content-Type</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RegularExpression</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;^application/grpc.*$&#34;</span></span></span></code></pre></div></td>
      </tr>
  </tbody>
</table>
<p>The routes were set to use the Tailscale <code>Gateway</code>. The <code>hostnames</code> properties were later to be filled by OpenTofu.</p>
<h3 id="appproject-and-application"><code>AppProject</code> and <code>Application</code></h3>
<p>As mentioned earlier, to create an initial set of Argo CD resources, the <a href="https://github.com/argoproj/argo-helm/tree/argo-cd-9.5.0/charts/argocd-apps" title="argo-helm/charts/argocd-apps at main · argoproj/argo-helm">argocd-apps</a> Helm chart was to be used. First, the version of the said chart <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L56-L61" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was defined</a> as a variable.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;argocd_apps_version&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Version of the argocd-apps Helm chart&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">default</span>     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2.0.4&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A new <a href="https://argo-cd.readthedocs.io/en/release-3.3/user-guide/projects/" title="Projects - Argo CD - Declarative GitOps CD for Kubernetes">project</a>, that will be home to future <code>Application</code> resources, was first defined within <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/argocd.tf#L64-L132" title="opentofu-kubernetes/argocd.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">the chart installation declaration</a>. It was restricted to only allow creation of resources under expected conditions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (AppProject - Argo CD)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;argocd_project&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">cert_manager</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_namespace_v1</span>.<span style="color:#a6e22e">tailscale</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">argocd</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-apps&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-project-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://argoproj.github.io/argo-helm&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">argocd_apps_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">projects</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # AppProject containing applications necessary for proper cluster operation
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">argocd</span><span style="color:#f92672">-</span><span style="color:#a6e22e">project</span><span style="color:#f92672">-</span><span style="color:#a6e22e">kubernetes</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">namespace</span>   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Project for cluster&#39;s operation&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # Only allow repository for this project
</span></span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">sourceRepos</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;https://github.com/lyuk98/argocd-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>          ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">destinations</span> <span style="color:#f92672">=</span> [<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            # Allow deployments to namespace &#34;argocd&#34;
</span></span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            },<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            # Allow deployments to namespace &#34;cert-manager&#34;
</span></span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            },<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            # Allow deployments to namespace &#34;tailscale&#34;
</span></span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tailscale&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          ]<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # List of CustomResourceDefinitions to allow for bootstrapping
</span></span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">clusterResourceWhitelist</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">kind</span> <span style="color:#66d9ef">in</span> [
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;CiliumGatewayClassConfig&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;Gateway&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;GatewayClass&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;GRPCRoute&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;HTTPRoute&#34;</span>
</span></span><span style="display:flex;"><span>              ] <span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">group</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">kind</span>  <span style="color:#f92672">=</span> <span style="color:#a6e22e">kind</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          ]
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>Application</code> resources were next.</p>
<h4 id="application-cert-manager"><code>Application</code>: cert-manager</h4>
<p>The resource <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/cert-manager.tf#L49-L110" title="opentofu-kubernetes/cert-manager.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was first defined</a> at <code>cert-manager.tf</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;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Argo CD Application - cert-manager)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;argocd_cert_manager&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">argocd_project</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">kubernetes_secret_v1</span>.<span style="color:#a6e22e">cloudflare_api_token</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-apps&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-application-cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://argoproj.github.io/argo-helm&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">argocd_apps_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">applications</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # Argo CD configuration
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">argocd</span><span style="color:#f92672">-</span><span style="color:#a6e22e">application</span><span style="color:#f92672">-</span><span style="color:#a6e22e">cert</span><span style="color:#f92672">-</span><span style="color:#a6e22e">manager</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">namespace</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">finalizers</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;resources-finalizer.argocd.argoproj.io&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">project</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-project-kubernetes&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # ClusterIssuer configuration
</span></span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">path</span>           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">repoURL</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/lyuk98/argocd-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">targetRevision</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;main&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">kustomize</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">patches</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">target</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">kind</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt-staging&#34;</span>
</span></span><span style="display:flex;"><span>                  }
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">patch</span> <span style="color:#f92672">=</span> yamlencode([
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">op</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;replace&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">path</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/spec/acme/email&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">acme_email</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                  ])
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">target</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">kind</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ClusterIssuer&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;letsencrypt&#34;</span>
</span></span><span style="display:flex;"><span>                  }
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">patch</span> <span style="color:#f92672">=</span> yamlencode([
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">op</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;replace&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">path</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/spec/acme/email&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">acme_email</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                  ])
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>              ]
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">destination</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;cert-manager&#34;</span>
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">syncPolicy</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">automated</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">selfHeal</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The variable <code>acme_email</code> <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L28-L33" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was separately defined</a> afterwards.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;acme_email&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">type</span>        <span style="color:#f92672">=</span> <span style="color:#a6e22e">string</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">description</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Email for ACME ClusterIssuer configuration&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sensitive</span>   <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nullable</span>    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="application-tailscale"><code>Application</code>: Tailscale</h4>
<p>Following cert-manager, Argo CD <code>Application</code> for Tailscale <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/tailscale.tf#L50-L111" title="opentofu-kubernetes/tailscale.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was defined</a> at <code>tailscale.tf</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;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Argo CD application - Tailscale Kubernetes Operator)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;argocd_tailscale&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">argocd_cert_manager</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">tailscale_operator</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">external_dns</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-apps&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-application-tailscale&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://argoproj.github.io/argo-helm&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">argocd_apps_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">applications</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # Argo CD configuration
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">argocd</span><span style="color:#f92672">-</span><span style="color:#a6e22e">application</span><span style="color:#f92672">-</span><span style="color:#a6e22e">tailscale</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">namespace</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">finalizers</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;resources-finalizer.argocd.argoproj.io&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">project</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-project-kubernetes&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          # Tailscale Gateway configuration
</span></span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">path</span>           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tailscale&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">repoURL</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/lyuk98/argocd-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">targetRevision</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;main&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">kustomize</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">patches</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">target</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">kind</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Gateway&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tailscale&#34;</span>
</span></span><span style="display:flex;"><span>                  }
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">patch</span> <span style="color:#f92672">=</span> yamlencode([
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">op</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;replace&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">path</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/spec/listeners/0/hostname&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*.tailnet.</span><span style="color:#e6db74">${</span>data.<span style="color:#a6e22e">cloudflare_zone</span>.<span style="color:#a6e22e">default</span>.<span style="color:#a6e22e">name</span><span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                  ])
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>              ]
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">destination</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tailscale&#34;</span>
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">syncPolicy</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">automated</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">selfHeal</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="application-argo-cd"><code>Application</code>: Argo CD</h4>
<p>Lastly, the <code>Application</code> for creating routes to Argo CD <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/argocd.tf#L134-L191" title="opentofu-kubernetes/argocd.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes">was defined</a> at <code>argocd.tf</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;-webkit-text-size-adjust:none;"><code class="language-tf" data-lang="tf"><span style="display:flex;"><span><span style="color:#75715e"># Helm chart (Argo CD Application - Argo CD)
</span></span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;helm_release&#34;</span> <span style="color:#e6db74">&#34;argocd_application&#34;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">depends_on</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">helm_release</span>.<span style="color:#a6e22e">argocd_tailscale</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chart</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-apps&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">name</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-application&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">repository</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://argoproj.github.io/argo-helm&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">atomic</span>          <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">cleanup_on_fail</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">namespace</span>       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">upgrade_install</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">version</span>         <span style="color:#f92672">=</span> var.<span style="color:#a6e22e">argocd_apps_version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">values</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">yamlencode</span>({
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">applications</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">        # Argo CD configuration
</span></span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">argocd</span><span style="color:#f92672">-</span><span style="color:#a6e22e">application</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">namespace</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">finalizers</span> <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;resources-finalizer.argocd.argoproj.io&#34;</span>]
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">project</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-project-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">source</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">path</span>           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">repoURL</span>        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://github.com/lyuk98/argocd-kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">targetRevision</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;main&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">kustomize</span> <span style="color:#f92672">=</span> {<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">              # Add custom domain to Kustomization
</span></span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">patches</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">target</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">kind</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HTTPRoute&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-http-route&#34;</span>
</span></span><span style="display:flex;"><span>                  }
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">patch</span> <span style="color:#f92672">=</span> yamlencode([
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">op</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;add&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">path</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/spec/hostnames/0&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">local</span>.<span style="color:#a6e22e">argocd_domain</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                  ])
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">target</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">kind</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;GRPCRoute&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd-grpc-route&#34;</span>
</span></span><span style="display:flex;"><span>                  }
</span></span><span style="display:flex;"><span>                  <span style="color:#a6e22e">patch</span> <span style="color:#f92672">=</span> yamlencode([
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">op</span>    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;add&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">path</span>  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/spec/hostnames/0&#34;</span>
</span></span><span style="display:flex;"><span>                      <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">local</span>.<span style="color:#a6e22e">argocd_domain</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                  ])
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>              ]
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">destination</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">name</span>      <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;in-cluster&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">namespace</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;argocd&#34;</span>
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#a6e22e">syncPolicy</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">automated</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>              <span style="color:#a6e22e">selfHeal</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h1 id="applying-the-new-configuration">Applying the new configuration</h1>
<p>With the configuration ready, I started preparing for the resource application. New providers were introduced since the last change, so I ran <code>tofu init</code> again.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ source ~/env.sh
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu init -backend-config=backend.tfvars
</span></span></code></pre></div><p>The <code>env.sh</code> was made earlier, <a href="/blog/2026/04/building-a-homelab-2-setting-up-argo-cd/#software-upgrading-talos-linux-and-kubernetes" title="Building a homelab (#2) - Setting up Argo CD">while upgrading the cluster</a>, but now had a few differences:</p>
<ul>
<li><code>CLOUDFLARE_API_TOKEN</code> was replaced with <a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L21-L26" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>TF_VAR_cloudflare_api_token</code></a>, containing the same API token.</li>
<li><a href="https://github.com/lyuk98/opentofu-kubernetes/blob/fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686/variables.tf#L28-L33" title="opentofu-kubernetes/variables.tf at fce616b2abe59bc2b29f3ef3fc9559b1ea7fa686 · lyuk98/opentofu-kubernetes"><code>TF_VAR_acme_email</code></a> was added.</li>
</ul>
<p>When it was done, <code>tofu plan</code> and <code>tofu apply</code> were all there were left to run.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu plan -out=tfplan
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span>[nix-shell:~/opentofu-kubernetes]$ tofu apply tfplan
</span></span></code></pre></div><h1 id="accessing-argo-cd">Accessing Argo CD</h1>
<p>A while after the changes were applied, I noticed new DNS records and, consequently, I could now access Argo CD&rsquo;s web interface using my custom domain.</p>
<p>I was locked behind a login page, however. To get authenticated, I accessed the administrator account&rsquo;s initial password.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash-session" data-lang="bash-session"><span style="display:flex;"><span>[lyuk98@framework:~]$ kubectl get --output<span style="color:#f92672">=</span>jsonpath<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{.data.password}&#34;</span> secret argocd-initial-admin-secret --namespace argocd | base64 --decode
</span></span></code></pre></div><p>With the password, I then logged in to the service.</p>
<picture>
  <source srcset="https://images.lyuk98.com/3b0ec7dd-3967-406f-b3a1-0026b6946e61.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/3b0ec7dd-3967-406f-b3a1-0026b6946e61.webp" width="2848" height="1824" style="width: 100%; height: auto;" alt="Login page from self-hosted Argo CD, with the &quot;Username&quot; set to &quot;admin&quot; and the &quot;Password&quot; filled up">
</picture>
<p>Applications in the cluster, which were created by OpenTofu, were now visible.</p>
<picture>
  <source srcset="https://images.lyuk98.com/b2e77c59-ccaf-4520-8dde-606a827ff28e.avif" type="image/avif">
  <img loading="lazy" src="https://images.lyuk98.com/b2e77c59-ccaf-4520-8dde-606a827ff28e.webp" width="2848" height="1824" style="width: 100%; height: auto;" alt="Dashboard from self-hosted Argo CD, showing 3 deployed Argo CD Applications">
</picture>
<h1 id="conclusion">Conclusion</h1>
<p>Just like <a href="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#conclusion" title="Building a homelab (#1) - Creating a Kubernetes cluster with Talos Linux">the last time</a>, I wrote another visual representation for the OpenTofu-managed resources.</p>
<pre class="mermaid">
  flowchart RL
  %% Resource/data source declaration

  %% argocd.tf
  kubernetes_namespace_v1.argocd
  helm_release.argocd
  helm_release.argocd_project
  helm_release.argocd_application

  %% cert-manager.tf
  kubernetes_namespace_v1.cert_manager
  helm_release.cert_manager
  helm_release.argocd_cert_manager

  %% cilium.tf
  data.kustomization_build.gateway_api([data.kustomization_build.gateway_api])
  kustomization_resource.gateway_api_p0
  kustomization_resource.gateway_api_p1
  kustomization_resource.gateway_api_p2
  helm_release.cilium

  %% cloudflare.tf
  data.cloudflare_zone.default([data.cloudflare_zone.default])
  time_sleep.dns_ready
  terraform_data.dns_ready
  cloudflare_dns_record.xps13_a
  cloudflare_dns_record.xps13_aaaa

  %% external-dns.tf
  kubernetes_namespace_v1.external_dns
  kubernetes_secret_v1.cloudflare_api_token
  helm_release.external_dns

  %% tailscale.tf
  kubernetes_namespace_v1.tailscale
  helm_release.tailscale_operator
  helm_release.argocd_tailscale
  tailscale_oauth_client.kubernetes_operator

  %% talos-controlplane.tf
  data.talos_machine_configuration.controlplane([data.talos_machine_configuration.controlplane])

  %% talos-xps13.tf
  talos_machine_configuration_apply.xps13
  talos_machine_bootstrap.xps13
  random_password.talos_encryption_passphrase_xps13
  talos_image_factory_schematic.xps13
  data.talos_image_factory_urls.xps13([data.talos_image_factory_urls.xps13])
  tailscale_oauth_client.xps13
  data.tailscale_device.xps13([data.tailscale_device.xps13])

  %% talos.tf
  talos_machine_secrets.kubernetes
  data.talos_client_configuration.kubernetes([data.talos_client_configuration.kubernetes])
  talos_cluster_kubeconfig.kubernetes
  local_sensitive_file.talosconfig
  local_sensitive_file.kubeconfig

  %% Relation declaration

  %% argocd.tf
  helm_release.argocd-- depends_on --&gt;kubernetes_namespace_v1.argocd
  helm_release.argocd-- depends_on --&gt;helm_release.cilium
  helm_release.argocd-- name --&gt;data.cloudflare_zone.default
  helm_release.argocd_project-- depends_on --&gt;kubernetes_namespace_v1.cert_manager
  helm_release.argocd_project-- depends_on --&gt;kubernetes_namespace_v1.tailscale
  helm_release.argocd_project-- depends_on --&gt;helm_release.argocd
  helm_release.argocd_application-- depends_on --&gt;helm_release.argocd_tailscale
  helm_release.argocd_application-- name --&gt;data.cloudflare_zone.default

  %% cert-manager.tf
  helm_release.cert_manager-- depends_on --&gt;kubernetes_namespace_v1.cert_manager
  helm_release.cert_manager-- depends_on --&gt;helm_release.cilium
  helm_release.argocd_cert_manager-- depends_on --&gt;helm_release.argocd_project
  helm_release.argocd_cert_manager-- depends_on --&gt;kubernetes_secret_v1.cloudflare_api_token

  %% cilium.tf
  kustomization_resource.gateway_api_p0-- ids_prio --&gt;data.kustomization_build.gateway_api
  kustomization_resource.gateway_api_p1-- depends_on --&gt;kustomization_resource.gateway_api_p0
  kustomization_resource.gateway_api_p1-- ids_prio --&gt;data.kustomization_build.gateway_api
  kustomization_resource.gateway_api_p2-- depends_on --&gt;kustomization_resource.gateway_api_p1
  kustomization_resource.gateway_api_p2-- ids_prio --&gt;data.kustomization_build.gateway_api
  helm_release.cilium-- depends_on --&gt;kustomization_resource.gateway_api_p2

  %% cloudflare.tf
  time_sleep.dns_ready-- content --&gt;cloudflare_dns_record.xps13_a
  time_sleep.dns_ready-- content --&gt;cloudflare_dns_record.xps13_aaaa
  terraform_data.dns_ready-- triggers_replace --&gt;time_sleep.dns_ready
  terraform_data.dns_ready-- name --&gt;data.cloudflare_zone.default
  terraform_data.dns_ready-- triggers --&gt;time_sleep.dns_ready
  cloudflare_dns_record.xps13_a-- addresses --&gt;data.tailscale_device.xps13
  cloudflare_dns_record.xps13_aaaa-- addresses --&gt;data.tailscale_device.xps13

  %% external-dns.tf
  kubernetes_secret_v1.cloudflare_api_token-- depends_on --&gt;kubernetes_namespace_v1.cert_manager
  kubernetes_secret_v1.cloudflare_api_token-- depends_on --&gt;kubernetes_namespace_v1.external_dns
  helm_release.external_dns-- depends_on --&gt;kubernetes_secret_v1.cloudflare_api_token
  helm_release.external_dns-- depends_on --&gt;helm_release.cilium
  helm_release.external_dns-- name --&gt;data.cloudflare_zone.default

  %% tailscale.tf
  helm_release.tailscale_operator-- depends_on --&gt;kubernetes_namespace_v1.tailscale
  helm_release.tailscale_operator-- depends_on --&gt;helm_release.cilium
  helm_release.tailscale_operator-- id --&gt;tailscale_oauth_client.kubernetes_operator
  helm_release.tailscale_operator-- key --&gt;tailscale_oauth_client.kubernetes_operator
  helm_release.argocd_tailscale-- depends_on --&gt;helm_release.argocd_cert_manager
  helm_release.argocd_tailscale-- depends_on --&gt;helm_release.tailscale_operator
  helm_release.argocd_tailscale-- depends_on --&gt;helm_release.external_dns
  helm_release.argocd_tailscale-- name --&gt;data.cloudflare_zone.default

  %% talos-controlplane.tf
  data.talos_machine_configuration.controlplane-- name --&gt;data.cloudflare_zone.default
  data.talos_machine_configuration.controlplane-- machine_secrets --&gt;talos_machine_secrets.kubernetes

  %% talos-xps13.tf
  talos_machine_configuration_apply.xps13-- client_configuration --&gt;talos_machine_secrets.kubernetes
  talos_machine_configuration_apply.xps13-- machine_configuration --&gt;data.talos_machine_configuration.controlplane
  talos_machine_configuration_apply.xps13-- urls --&gt;data.talos_image_factory_urls.xps13
  talos_machine_configuration_apply.xps13-- key --&gt;tailscale_oauth_client.xps13
  talos_machine_configuration_apply.xps13-- tags --&gt;tailscale_oauth_client.xps13
  talos_machine_configuration_apply.xps13-- result --&gt;random_password.talos_encryption_passphrase_xps13
  talos_machine_bootstrap.xps13-- depends_on --&gt;data.tailscale_device.xps13
  talos_machine_bootstrap.xps13-- replace_triggered_by --&gt;talos_machine_configuration_apply.xps13
  talos_machine_bootstrap.xps13-- client_configuration --&gt;talos_machine_secrets.kubernetes
  data.talos_image_factory_urls.xps13-- id --&gt;talos_image_factory_schematic.xps13
  tailscale_device.xps13-- depends_on --&gt;talos_machine_configuration_apply.xps13

  %% talos.tf
  data.talos_client_configuration.kubernetes-- client_configuration --&gt;talos_machine_secrets.kubernetes
  data.talos_client_configuration.kubernetes-- content --&gt;cloudflare_dns_record.xps13_a
  talos_cluster_kubeconfig.kubernetes-- depends_on --&gt;terraform_data.dns_ready
  talos_cluster_kubeconfig.kubernetes-- replace_triggered_by --&gt;talos_machine_secrets.kubernetes
  talos_cluster_kubeconfig.kubernetes-- client_configuration --&gt;talos_machine_secrets.kubernetes
  talos_cluster_kubeconfig.kubernetes-- content --&gt;cloudflare_dns_record.xps13_a
  local_sensitive_file.talosconfig-- talos_config --&gt;data.talos_client_configuration.kubernetes
  local_sensitive_file.kubeconfig-- kubeconfig_raw --&gt;talos_cluster_kubeconfig.kubernetes
</pre>

<p>The graph became complex to the point where it essentially became incomprehensible. To understand the application flow better, I read the code and wrote what would be done in each step of <code>tofu apply</code>.</p>

<details open>
  <summary><strong>Step 1</strong></summary>
  <ul>
<li>The following providers are set up:
<ul>
<li><code>provider &quot;cloudflare&quot;</code></li>
<li><code>provider &quot;local&quot;</code></li>
<li><code>provider &quot;random&quot;</code></li>
<li><code>provider &quot;tailscale&quot;</code></li>
<li><code>provider &quot;talos&quot;</code></li>
<li><code>provider &quot;time&quot;</code></li>
</ul>
</li>
</ul>
</details>


<details open>
  <summary><strong>Step 2</strong></summary>
  <ul>
<li><code>data.cloudflare_zone.default</code> retrieves information about the specified Cloudflare zone</li>
<li><code>random_password.talos_encryption_passphrase_xps13</code> generates storage encryption passphrase for XPS 13</li>
<li>OAuth clients from Tailscale are generated for Kubernetes operator as well as Talos Linux nodes:
<ul>
<li><code>tailscale_oauth_client.kubernetes_operator</code></li>
<li><code>tailscale_oauth_client.xps13</code></li>
</ul>
</li>
<li><code>talos_image_factory_schematic.xps13</code> generates Image Factory schematic for XPS 13</li>
<li><code>talos_machine_secrets.kubernetes</code> generates machine secrets for the cluster</li>
</ul>
</details>


<details open>
  <summary><strong>Step 3</strong></summary>
  <ul>
<li><code>data.talos_machine_configuration.controlplane</code> prepares machine configuration for control plane nodes</li>
<li><code>data.talos_image_factory_urls.xps13</code> makes URLs for the Image Factory schematic (for XPS 13) available</li>
</ul>
</details>


<details open>
  <summary><strong>Step 4</strong></summary>
  <ul>
<li><code>talos_machine_configuration_apply.xps13</code> applies configuration to XPS 13, installing Talos Linux</li>
</ul>
</details>


<details open>
  <summary><strong>Step 5</strong></summary>
  <ul>
<li><code>data.tailscale_device.xps13</code> waits until the node connects to the tailnet, then retrieves its information</li>
</ul>
</details>


<details open>
  <summary><strong>Step 6</strong></summary>
  <ul>
<li>The following DNS records for the Talos Linux nodes are added:
<ul>
<li><code>cloudflare_dns_record.xps13_a</code></li>
<li><code>cloudflare_dns_record.xps13_aaaa</code></li>
</ul>
</li>
<li><code>talos_machine_bootstrap.xps13</code> bootstraps the <a href="https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/" title="Operating etcd clusters for Kubernetes | Kubernetes">etcd</a> cluster</li>
</ul>
</details>


<details open>
  <summary><strong>Step 7</strong></summary>
  <ul>
<li><code>time_sleep.dns_ready</code> waits while DNS records propagate</li>
<li><code>data.talos_client_configuration.kubernetes</code> generates <code>talosconfig</code> that can be used to access Talos Linux nodes via <code>talosctl</code></li>
</ul>
</details>


<details open>
  <summary><strong>Step 8</strong></summary>
  <ul>
<li><code>terraform_data.dns_ready</code> checks, and optionally waits, for DNS propagation</li>
<li><code>local_sensitive_file.talosconfig</code> writes generated <code>talosconfig</code> to the local file system</li>
</ul>
</details>


<details open>
  <summary><strong>Step 9</strong></summary>
  <ul>
<li><code>talos_cluster_kubeconfig.kubernetes</code> generates <code>kubeconfig</code> for accessing the Kubernetes cluster via <code>kubectl</code></li>
</ul>
</details>


<details open>
  <summary><strong>Step 10</strong></summary>
  <ul>
<li>The following providers are set up using generated <code>kubeconfig</code>:
<ul>
<li><code>provider &quot;helm&quot;</code></li>
<li><code>provider &quot;kubernetes&quot;</code></li>
<li><code>provider &quot;kustomization&quot;</code></li>
</ul>
</li>
<li><code>local_sensitive_file.kubeconfig</code> writes generated <code>kubeconfig</code> to the local file system</li>
</ul>
</details>


<details open>
  <summary><strong>Step 11</strong></summary>
  <ul>
<li>Four Kubernetes <code>Namespace</code> resources are created:
<ul>
<li><code>kubernetes_namespace_v1.argocd</code></li>
<li><code>kubernetes_namespace_v1.cert_manager</code></li>
<li><code>kubernetes_namespace_v1.external_dns</code></li>
<li><code>kubernetes_namespace_v1.tailscale</code></li>
</ul>
</li>
<li><code>data.kustomization_build.gateway_api</code> builds manifest from a <code>Kustomization</code> for Gateway API resources</li>
</ul>
</details>


<details open>
  <summary><strong>Step 12</strong></summary>
  <ul>
<li><code>kustomization_resource.gateway_api_p0</code> applies Gateway API resources with <code>kind</code> <code>Namespace</code> and <code>CustomResourceDefinition</code></li>
<li><code>kubernetes_secret_v1.cloudflare_api_token</code> creates Kubernetes <code>Secret</code> containing Cloudflare API token</li>
</ul>
</details>


<details open>
  <summary><strong>Step 13</strong></summary>
  <ul>
<li><code>kustomization_resource.gateway_api_p1</code> applies Gateway API resources with <code>kind</code> other than <code>Namespace</code>, <code>CustomResourceDefinition</code>, <code>MutatingWebhookConfiguration</code>, and <code>ValidatingWebhookConfiguration</code></li>
</ul>
</details>


<details open>
  <summary><strong>Step 14</strong></summary>
  <ul>
<li><code>kustomization_resource.gateway_api_p2</code> applies remaining Gateway API resources</li>
</ul>
</details>


<details open>
  <summary><strong>Step 15</strong></summary>
  <ul>
<li><code>helm_release.cilium</code> installs Helm chart for Cilium</li>
</ul>
</details>


<details open>
  <summary><strong>Step 16</strong></summary>
  <ul>
<li>The following Helm charts are installed:
<ul>
<li><code>helm_release.argocd</code></li>
<li><code>helm_release.cert_manager</code></li>
<li><code>helm_release.external_dns</code></li>
<li><code>helm_release.tailscale_operator</code></li>
</ul>
</li>
</ul>
</details>


<details open>
  <summary><strong>Step 17</strong></summary>
  <ul>
<li><code>helm_release.argocd_project</code> sets up an Argo CD <code>AppProject</code></li>
</ul>
</details>


<details open>
  <summary><strong>Step 18</strong></summary>
  <ul>
<li><code>helm_release.argocd_cert_manager</code> creates an Argo CD <code>Application</code> for <code>ClusterIssuer</code> resources</li>
</ul>
</details>


<details open>
  <summary><strong>Step 19</strong></summary>
  <ul>
<li><code>helm_release.argocd_tailscale</code> creates an Argo CD <code>Application</code> for Tailscale-related Gateway API configurations</li>
</ul>
</details>


<details open>
  <summary><strong>Step 20</strong></summary>
  <ul>
<li><code>helm_release.argocd_application</code> creates an Argo CD <code>Application</code> for setting up routes to Argo CD itself</li>
</ul>
</details>

<h2 id="the-next-steps">The next steps</h2>
<p>I initially planned to finish this post by successfully deploying a user-facing service. However, what was believed to be a small setup at first turned out to be a much more involved process. The fact that I tried to tick all the boxes in my wishlist at once, while having no one with the exact same setup as mine, did not help, either.</p>
<p>Argo CD is now operational, but aside from deploying other services with it, I could already see things I should think about next.</p>
<h3 id="integrating-openid-connect-with-tailscale">Integrating OpenID Connect with Tailscale</h3>
<p>Logging in to Argo CD is currently done with initial admin credentials. This was not desirable to me, but instead of maintaining another set of text credentials, I considered letting Tailscale be an <a href="https://openid.net/developers/how-connect-works/" title="How OpenID Connect Works - OpenID Foundation">OpenID Connect</a> provider.</p>
<p>I then came across a service, named <a href="https://tailscale.com/docs/features/tsidp" title="tsidp · Tailscale Docs"><code>tsidp</code></a>, that I can self-host to solve the problem. Its experimental status was slightly concerning, but I thought I can give it a try nonetheless.</p>
<h3 id="storing-states">Storing states</h3>
<p><a href="/blog/2025/09/hosting-ente-with-terraform-vault-and-nixos/" title="Hosting Ente with Terraform, Vault, and NixOS">Hosting web applications on NixOS</a> had a huge advantage: with their configuration defined in Git repositories, the compute instances themselves could remain practically stateless, as long as the application data stayed somewhere else (like on an object storage service).</p>
<p>Despite Kubernetes being a much more complex system compared to a single web application (like Vault), I still wanted to see if such a thing can be done. If it is either impossible or impractical (which I guess would be due to performance reasons), I wanted to at least achieve seamless backups and restorations of the states in question.</p>
<p>I believe that, once I know more about this, I will be working on it.</p>
<h3 id="crossing-paths-with-crossplane">Crossing paths with Crossplane</h3>
<p>A bunch of cloud resources for <a href="/blog/2025/09/hosting-ente-with-terraform-vault-and-nixos/" title="Hosting Ente with Terraform, Vault, and NixOS">my self-hosted photo service</a> were created by Terraform, but I thought it will be a little problematic once the eventual migration of the service to the Kubernetes cluster happens. I wished to keep all the configuration for a service in a single source of truth, but it was definitely not going to be the case when the infrastructure-as-code tool still has to be run somewhere else.</p>
<p>That is when I found out about <a href="https://www.crossplane.io/" title="Crossplane Is the Cloud-Native Framework for Platform Engineering">Crossplane</a>. I will have to learn more about this as well, but once I do, I believe this will be one of the major parts that will keep my services running.</p>
<h2 id="finishing-up">Finishing up</h2>
<blockquote cite="/blog/2026/02/building-a-homelab-1-creating-a-kubernetes-cluster-with-talos-linux/#conclusion"><p>The next step for me, with the Kubernetes cluster now in operation, would be to finally migrate my existing self-hosted services to the container orchestration system.</p>
</blockquote>
<p>Running user-facing services will have to come later, as this post was about another lay-the-groundwork process. While I had to put more effort into getting things correct, I was still glad that it could be done without any significant compromises (for now).</p>
<p>I wish I can write about deploying something I will actually use (in my daily life) in my next post. My present self will never know, though, how complicated the next part of this project will be.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
