···11+---
22+title: "Rebuilding my homelab: Suffering as a service"
33+desc: With additional Kubernetes mode!
44+date: 2024-05-15
55+tags:
66+ - Homelab
77+ - RockyLinux
88+ - FedoraCoreOS
99+ - TalosLinux
1010+ - Kubernetes
1111+ - Ansible
1212+ - Longhorn
1313+ - Nginx
1414+ - CertManager
1515+ - ExternalDNS
1616+hero:
1717+ ai: "Photo by Xe Iaso, Canon EOS R6 mark II with a Rokinon Cine DSX 85mm T1.5 lens"
1818+ file: ../xedn/dynamic/766623e0-26d1-4068-9a63-a91d274f23d0
1919+ prompt: "A field of dandelion flowers in the sun, heavy depth of field. A thin strip of the field is in focus, the rest is a blur."
2020+---
2121+2222+I have a slight problem where I have too many computers in my office. These extra computers are my [homelab](https://www.reddit.com/r/homelab/), or a bunch of slack compute that I can use to run various workloads at home. I use my homelab to have a place to "just run things" like [Plex](https://plex.tv) and the whole host of other services that I either run or have written for my husband and I.
2323+2424+<Conv name="Cadey" mood="hug">
2525+ I want to have my own platform so that I can run things that I used to run in
2626+ the cloud. If I can "just run things locally", I can put my slack compute
2727+ space to work for good. This can help me justify the power bill of these nodes
2828+ to my landlord!
2929+</Conv>
3030+3131+Really, I just wanna be able to use this to mess around, try new things, and turn the fruit of those experiments into blogposts like this one. Until very recently, everything in my homelab ran NixOS. [A friend of mine](https://fasterthanli.me) has been goading me into trying Kubernetes again, and in a moment of weakness, I decided to see how bad the situation was to get Kubernetes running on my own hardware at home.
3232+3333+- `kos-mos`, a small server that I use for running some CI things and periphery services. It has 32 GB of ram and a Core i5-10600.
3434+- `ontos`, identical to `kos-mos` but with an RTX 2060 6 GB.
3535+- `logos`, identical to `kos-mos` but with a RTX 3060 12 GB.
3636+- `pneuma`, my main shellbox and development machine. It is a handbuilt tower PC with 64 GB of ram and a Ryzen 9 5900X. It has a GPU (AMD RX5700 non-XT w/8GB of vram) because the 5900X doesn't have integrated graphics. It has a bunch of random storage devices in it. It also handles the video transcoding for xesite video uploads.
3737+- `itsuki`, the NAS. It has all of our media and backups on it. It runs Plex and a few other services, mostly managed by docker compose. It has 16 GB of ram and a Core i5-10600.
3838+- `chrysalis`, an old Mac Pro from 2013 that I mostly use as my Prometheus server. It has 32 GB of ram and a Xeon E5-1650. It also runs the IRC bot `[Mara]` in `#xeserv` on Libera.chat (it announces new posts on my blog). It's on its last legs in multiple ways, but it works for now. I've been holding off on selling it because I won it in a competition involving running an IRC network in Docker containers. Sentimental value is a bitch, eh?
3939+4040+<Conv name="Mara" mood="hacker">
4141+ When the homelab was built, the Core i5-10600 was a "last generation"
4242+ processor. It also met a perfect balance between compute oomph, onboard iGPU
4343+ support, power usage, and not requiring a massive cooler to keep it running
4444+ happily. We could probably get some more use out of newer processors, but that
4545+ will probably have to wait for one or more of our towers/their parts to get
4646+ cycled out in regular upgrades. That probably won't happen for a year or two,
4747+ but it'll be nice to get a Ryzen 9 5950x or two into the cluster eventually.
4848+</Conv>
4949+5050+Of these machines, `kos-mos` is the easiest to deal with because it normally doesn't have any services dedicated to it. In the past, I had to move some workloads off of it for various reasons.
5151+5252+I have no plans to touch my shellbox or the NAS, those have complicated setups that I don't want to mess with. I'm okay with my shellbox being different because that's where I do a lot of development and development servers are almost always vastly different from production servers. I'm also scared to touch the NAS because that has all my media on it and I don't want to risk losing it. It has more space than the rest of the house combined.
5353+5454+A rebuild of the homelab is going to be a fair bit of work. I'm going to have to take this one piece at a time and make sure that I don't lose anything important.
5555+5656+<Conv name="Numa" mood="delet">
5757+ Foreshadowing is a literary technique in which...
5858+</Conv>
5959+6060+This post isn't going to be like my other posts. This is a synthesis of a few patron-exclusive notes that described my steps in playing with options and had my immediate reactions as I was doing things. If you want to read those brain-vomit notes, you can [support me on Patreon](https://patreon.com/cadey) and get access to them.
6161+6262+When I was considering what to do, I had a few options in mind:
6363+6464+- [Rocky Linux](https://rockylinux.org/) (or even [Oracle Linux](https://yum.oracle.com/)) with Ansible
6565+- Something in the [Universal Blue](https://universal-blue.org/) ecosystem
6666+- [Fedora CoreOS](https://fedoraproject.org/coreos/)
6767+- [K3os](https://k3os.io/)
6868+- [Talos Linux](https://talos.dev)
6969+- Giving up on the idea of having a homelab, throwing all of my computers into the sun (or selling them on Kijiji), and having a simpler life
7070+7171+<Conv name="Aoi" mood="wut">
7272+ Wait, hold up. You're considering _Kubernetes_ for your _homelab_? I thought
7373+ you were as staunchly anti-Kubernetes as it got.
7474+</Conv>
7575+<Conv name="Cadey" mood="coffee">
7676+ I am, but hear me out. Kubernetes gets a lot of things wrong, but it does get
7777+ one thing so clearly right that it's worth celebration: you don't need to SSH
7878+ into a machine to look at logs, deploy new versions of things, or see what's
7979+ running. Everything is done via the API. You also don't need to worry about
8080+ assigning workloads to machines, it just does it for you. Not to mention I
8181+ have to shill a [Kubernetes product for
8282+ work](https://fly.io/docs/kubernetes/fks-quickstart/) at some point so having
8383+ some experience with it would be good.
8484+</Conv>
8585+<Conv name="Aoi" mood="facepalm">
8686+ Things really must be bad if you're at this point...
8787+</Conv>
8888+<Conv name="Cadey" mood="enby">
8989+ Let's be real, the latest release is actually, real life, unironically named
9090+ uwubernetes. I can't _not_ try it. I'd be betraying my people.
9191+</Conv>
9292+<Conv name="Aoi" mood="facepalm">
9393+ You really weren't kidding about technology decisions being made arbitrarily
9494+ in the [Shashin talk](/talks/2024/shashin/), were you. How do you exist?
9595+</Conv>
9696+9797+I ran a poll on [Mastodon](https://pony.social/@cadey/112345742472623188) to see what people wanted me to do. The results were overwhelmingly in favor of Rocky Linux. As an online "content creator", who am I to not give the people what they want?
9898+9999+## Rocky Linux
100100+101101+[Rocky Linux](https://rockylinux.org/) is a fork of pre-Stream CentOS. It aims to be a 1:1 drop-in replacement for CentOS and RHEL. It's a community-driven project that is sponsored by the [Rocky Enterprise Software Foundation](https://resf.org/).
102102+103103+For various reasons involving my HDMI cable being too short to reach the other machines, I'm gonna start with `chrysalis`. Rocky Linux has a GUI installer and I can hook it up to the sideways monitor that I have on my desk. For extra fun, whenever the mac tries to display something on the monitor, the EFI framebuffer dances around until the OS framebuffer takes over.
104104+105105+<Video path="video/2024/oneoff-mac-boot" />
106106+107107+<Conv name="Cadey" mood="coffee">
108108+ I really hope one of the GPUs isn't dying. That would totally ruin the resale
109109+ value of that machine. I wasn't able to recreate this on my 1080p crash cart
110110+ monitor, so I think that it's just the combination of that mac, the HDMI cable
111111+ I used, and my monitor. It's really weird though.
112112+</Conv>
113113+114114+The weird part about `chrysalis` is that it's a Mac Pro from 2013. Macs of that vintage can boot normal EFI partitions and binaries, but they generally prefer to have your EFI partition be a HFS+ volume. This is normally not a problem because the installer will just make that weird EFI partition for you.
115115+116116+<Picture
117117+ path="blog/2024/homelab-v2/IMG_0256"
118118+ desc="an error message saying: resource to create this format macefi is unavailable"
119119+/>
120120+121121+However, the Rocky Linux installer doesn't make that magic partition for you. They ifdeffed out the macefi installation flow because Red Hat ifdeffed it out.
122122+123123+<Conv name="Cadey" mood="coffee">
124124+ I get that they want to be a 1:1 drop-in replacement (which means that any bug
125125+ RHEL has, they have), but it is massively inconvenient to me in particular.
126126+</Conv>
127127+128128+As a result, you have to do a very manual install that looks something like this [lifted from the Red Hat bug tracker](https://bugzilla.redhat.com/show_bug.cgi?id=1751311#c16):
129129+130130+> - Boot Centos/RHEL 8 ISO Normally (I used 8.1 of each)
131131+> - Do the normal setup of network, packages, etc.
132132+> - Enter disk partitioning
133133+> - Select your disk
134134+> - At the bottom, click the "Full disk summary and boot loader" text
135135+> - Click on the disk in the list
136136+> - Click "Do not install boot loader"
137137+> - Close
138138+> - Select "Custom" (I didn't try automatic, but it probably would not create the EFI partition)
139139+> - Done in the top left to get to the partitioning screen
140140+> - Delete existing partitions if needed
141141+> - Click +
142142+> - CentOS 8: create /boot/efi mountpoint, 600M, Standard EFI partition
143143+> - RHEL 8: create /foo mountpoint, 600M, Standard EFI partition, then edit the partition to be on /boot/efi
144144+> - Click + repeatedly to create the rest of the partitions as usual (/boot, / swap, /home, etc.)
145145+> - Done
146146+> - During the install, there may be an error about the mactel package, just continue
147147+> - On reboot, both times I've let it get to the grub prompt, but there's no grub.cfg; not sure if this is required
148148+> - Boot off ISO into rescue mode
149149+> - Choose 1 to mount the system on /mnt/sysimage
150150+> - At the shell, chroot /mnt/sysimage
151151+> - Check on the files in /boot to make sure they exist: ls -l /boot/ /boot/efi/EFI/redhat (or centos)
152152+> - Run the create the grub.cfg file: grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg
153153+> - I got a couple reload ioctl errors, but that didn't seem to hurt anything
154154+> - exit
155155+> - Next reboot should be fine, and as mentioned above it'll reboot after SELinux labelling
156156+157157+<Conv name="Cadey" mood="percussive-maintenance" standalone>
158158+ Yeah, no. I'm not going to do that. Another solution I found involved you
159159+ manually booting the kernel from the GRUB rescue shell. I'm not going to do
160160+ that either. I hate myself enough to run Kubernetes on metal in my homelab,
161161+ but not so much that I'm gonna unironically slonk the grub rescue shell in
162162+ anger.
163163+</Conv>
164164+165165+So, that's a wash. In the process of figuring this out I also found out that when I wiped the drive, I took down my IRC bot (and lost the password, thanks `A_Dragon` for helping me recover that account). I'm going to have to fix that eventually.
166166+167167+<Conv name="Aoi" mood="facepalm">
168168+ Yep, called it.
169169+</Conv>
170170+171171+I ended up moving the announcer IRC bot to be a part of [`within.website/x/cmd/mimi`](https://github.com/Xe/x/tree/master/cmd/mimi). `mimi` is a little bot that has claws into a lot of other things, including:
172172+173173+- Status page updates for the [fly.io community Discord](https://discord.gg/V4bE5uhtUg)'s #status channel
174174+- Announcing new blogposts on [#xeserv on libera.chat](https://web.libera.chat/#xeserv)
175175+- Google Calendar and its own Gmail account for a failed experiment to make a bot that could read emails forwarded to it and schedule appointments based on what a large language model parsed out of the email
176176+177177+<Conv name="Cadey" mood="enby">
178178+ I really need to finish that project. Someday! Maybe that upcoming AI
179179+ hackathon would give me a good excuse to make it happen.
180180+</Conv>
181181+182182+### Ansible
183183+184184+As a bonus round, let's see what it would look like to manage things with Ansible on Rocky Linux should I have been able to install Rocky Linux anyways. Ansible is a Red Hat product, so I expect that it would be the easiest thing to use to manage things.
185185+186186+Ansible is a "best hopes" configuration management system. It doesn't really authoritatively control what is going on, it merely suggest what should be going on. As such, you influence what the system does with "plays" like this:
187187+188188+```yaml
189189+- name: Full system update
190190+ dnf:
191191+ name: "*"
192192+ state: latest
193193+```
194194+195195+This is a play that tells the system to update all of its packages with dnf. However, when I ran the linter on this, I got told I need to instead format things like this:
196196+197197+```yaml
198198+- name: Full system update
199199+ ansible.builtin.dnf:
200200+ name: "*"
201201+ state: latest
202202+```
203203+204204+You need to use the fully qualified module name because [you might install other collections that have the name `dnf` in the future](https://docs.ansible.com/ansible/latest/collections/index.html). This kinda makes sense at a glance, I guess, but it's probably overkill for this usecase. However, it makes the lint errors go away and it is fixed mechanically, so I guess that's fine.
205205+206206+<Conv name="Mara" mood="hacker">
207207+ Programming idiom for you: the output of the formatter is always the correct
208208+ way to write your code/configuration. Less linter warnings, less problems.
209209+</Conv>
210210+211211+What's not fine is how you prevent Ansible from running the same command over and over. You need to make a folder full of empty semaphore files that get touched when the command runs:
212212+213213+```yaml
214214+- name: Xe's semaphore flags
215215+ ansible.builtin.shell: mkdir -p /etc/xe/semaphores
216216+ args:
217217+ creates: /etc/xe/semaphores
218218+219219+- name: Enable CRB repositories # CRB == "Code-Ready Builder"
220220+ ansible.builtin.shell: |
221221+ dnf config-manager --set-enabled crb
222222+223223+ touch /etc/xe/semaphores/crb
224224+ args:
225225+ creates: /etc/xe/semaphores/crb
226226+```
227227+228228+And then finally you can install a package:
229229+230230+```yaml
231231+- name: Install EPEL repo lists
232232+ ansible.builtin.dnf:
233233+ name: "epel-release"
234234+ state: present
235235+```
236236+237237+This is about the point where I said "No, I'm not going to deal with this". I haven't even created user accounts or installed dotfiles yet, I'm just trying to install a package repository so that I can install other packages.
238238+239239+<Conv name="Aoi" mood="wut">
240240+ Do you even really need any users but root or your dotfiles on production
241241+ servers? Ideally those should be remotely managed anyways. Logging into them
242242+ should be a situation of last resort, right?
243243+</Conv>
244244+<Conv name="Numa" mood="delet">
245245+ Assuming that you don't have triangular cows in the mix yeah.
246246+</Conv>
247247+248248+So I'm not going with Ansible (or likely any situation where Ansible would be required), even on the machines where installing Rocky Linux works without having to enter GRUB rescue shell purgatory.
249249+250250+<Conv name="Cadey" mood="coffee" standalone>
251251+ One of my patrons pointed out that I need to use [Ansible
252252+ conditionals](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_conditionals.html)
253253+ in order to prevent these same commands from running over and over. Of course,
254254+ in an ideal world these commands would be idempotent (meaning that they can be
255255+ run over and over without changing the system), but that's not always the
256256+ case. I'm going to dig deeper into this once I have virtualization working on
257257+ the cluster.
258258+259259+ Apparently you're supposed to use pre-made roles for as much as you can, such
260260+ as from [Ansible Galaxy](https://galaxy.ansible.com/) or [Linux System
261261+ Roles](https://linux-system-roles.github.io/). I don't know how I feel about
262262+ this (doing things with NixOS got me used to a combination of defining things
263263+ myself and then using third party things only when I really have to, but that
264264+ was probably because the documentation for anything out of the beaten path is
265265+ so poor there), but if this is the "right" way to do things then I'll do it.
266266+267267+ Thanks for the tip, Tudor!
268268+269269+</Conv>
270270+271271+## CoreOS
272272+273273+Way back when my career was just starting, CoreOS was released. CoreOS was radical and way ahead of its time. Instead of having a mutable server that you can SSH into and install packages at will on, CoreOS had the view that thou must put all your software into Docker containers and run them that way. This made it impossible to install new packages on the server, which they considered a feature.
274274+275275+I loved using CoreOS when I could because of one part that was absolutely revolutionary: [Fleet](https://github.com/coreos/fleet). Fleet was a distributed init system that let you run systemd services _somewhere_, but you could care where it ran when you needed to. Imagine a world where you could just spin your jobs somewhere, that was Fleet.
276276+277277+The really magical part about Fleet was the fact that it was deeply integrated into the discovery mechanism of CoreOS. Want 4 nodes in a cluster? Provision them with the same join token and Fleet would just figure it out. Newly provisioned nodes would also accept new work as soon as it was issued.
278278+279279+<Conv name="Numa" mood="happy">
280280+ Fleet was glorious. It was what made me decide to actually learn how to use
281281+ systemd in earnest. Before I had just been a "bloat bad so systemd bad" pleb,
282282+ but once I really dug into the inner workings I ended up really liking it.
283283+ Everything being composable units that let you build _up_ to what you want
284284+ instead of having to be an expert in all the ways shell script messes with you
285285+ is just such a better place to operate from. Not to mention being able to
286286+ restart multiple units with the same command, define ulimits, and easily
287287+ create "oneshot" jobs. If you're a "systemd hater", please actually give it a
288288+ chance before you decry it as "complicated bad lol". Shit's complicated
289289+ because life is complicated.
290290+</Conv>
291291+292292+And then it became irrelevant in the face of Kubernetes after CoreOS got bought out by Red Hat and then IBM bought out Red Hat.
293293+294294+Also, "classic" CoreOS is no more, but its spirit lives on in the form of [Fedora CoreOS](https://fedoraproject.org/coreos/), which is like CoreOS but built on top of [rpm-ostree](https://coreos.github.io/rpm-ostree/). The main difference between Fedora CoreOS and actual CoreOS is that Fedora CoreOS lets you install additional packages on the system.
295295+296296+<Conv name="Mara" mood="hacker">
297297+ Once Red Hat announced that CoreOS would be deprecated in favor of Fedora
298298+ CoreOS, Kinvolk forked "classic" CoreOS to [Flatcar
299299+ Linux](https://www.flatcar.org/), where you can still use it to this day. This
300300+ post didn't end up evaluating it because it doesn't let you change Ignition
301301+ configurations without reimaging the machine, which is unworkable for reasons
302302+ that will become obvious later in the article.
303303+304304+They are using [systemd-sysext](https://www.flatcar.org/blog/2024/04/os-innovation-with-systemd-sysext/) in order to extend the system with more packages, which is reminiscent of rpm-ostree layering.
305305+306306+</Conv>
307307+308308+### Fedora CoreOS
309309+310310+For various reasons involving divine intervention, I'm going to be building a few of my own RPM packages. I'm also going to be installing other third party programs on top of the OS, such as [yeet](https://github.com/Xe/x/tree/master/cmd/yeet).
311311+312312+Fedora CoreOS is a bit unique because you install it by declaring the end result of the system, baking that into an ISO, and then plunking that onto a flashdrive to assimilate the machine. If you are using it from a cloud environment, then you plunk your config into the "user data" section of the instance and it will happily boot up with that configuration.
313313+314314+This is a lot closer to the declarative future I want, with the added caveat that changing the configuration of a running system is a bit more involved than just SSHing into the machine and changing a file. You have to effectively blow away the machine and start over.
315315+316316+<Conv name="Aoi" mood="wut">
317317+ What? That sounds like a _terrible_ idea. How would you handle moving state
318318+ around?
319319+</Conv>
320320+<Conv name="Cadey" mood="aha">
321321+ Remember, this is for treating machines as replaceable _cattle_, not _pets_
322322+ that you imprint on. I'm sure that this will be a fun learning experience at
323323+ the very least.
324324+</Conv>
325325+<Conv name="Numa" mood="delet">
326326+ Again, foreshadowing is a literary technique in which...
327327+</Conv>
328328+329329+I want to build this on top of rpm-ostree because I want to have the best of both worlds: an immutable system that I can still install packages on. This is an absolute superpower and I want to have it in my life. Realistically I'm gonna end up installing only one or two packages on top of the base system, but those one or two packages are gonna make so many things so much easier. Especially for my WireGuard mesh so I can route the pod/service subnets in my Kubernetes cluster.
330330+331331+As a more practical example of how rpm-ostree, let's take a look at [Bazzite Linux](https://bazzite.gg). Bazzite is a spin of Fedora Silverblue (desktop Fedora built on top of rpm-ostree) that has the Steam Deck UI installed on top of it. This turns devices like the [ROG Ally](https://www.asus.com/ca-en/site/gaming/rog/handheld-consoles/rog-ally/) into actual game consoles instead of handheld gaming PCs.
332332+333333+<Conv name="Cadey" mood="coffee">
334334+ I went into this distinction more in my failed review video of the ROG Ally. I
335335+ plan to post this to [my Patreon](https://patreon.com/cadey) in case you want
336336+ to see what could have been. The video is probably fine all things considered,
337337+ I just don't think it's up to my standards and don't have the time/energy to
338338+ heal it right now.
339339+</Conv>
340340+341341+In Bazzite, rpm-ostree lets you layer on additional things like the Fanatec steering wheel drivers and wheel managers like [Oversteer](https://github.com/berarma/oversteer). This allows you to _add_ optional functionality without having to worry about breaking the base system. Any time updates are installed, rpm-ostree will layer Oversteer on top of it for you so that you don't have to worry about it.
342342+343343+This combined with my own [handrolled RPMs with `yeet`](https://github.com/Xe/x/tree/master/cmd/yeet) means that I could add software to my homelab nodes (like I have with Nix/NixOS) without having to worry about it being rebuilt from scratch or its distribution. This is a superpower that I want to keep in my life.
344344+345345+It's not gonna be as nice as the Nix setup, but something like this:
346346+347347+```js
348348+["amd64", "arm64"].forEach((goarch) =>
349349+ rpm.build({
350350+ name: "yeet",
351351+ description: "Yeet out actions with maximum haste!",
352352+ homepage: "https://within.website",
353353+ license: "CC0",
354354+ goarch,
355355+356356+ build: (out) => {
357357+ go.build("-o", `${out}/usr/bin/`);
358358+ },
359359+ })
360360+);
361361+```
362362+363363+is so much easier to read and manage than it is to do with RPM specfiles. It really does get closer to what it's like to use Nix.
364364+365365+<Conv name="Cadey" mood="coffee">
366366+ Not to mention if I did my Go packaging the full normal way with RPM
367367+ specfiles, I'd have to have my own personal dependencies risk fighting the
368368+ system-level dependencies. I don't want to do that, but you can if you want
369369+ to. I'd also like my builds to publish one package, not 50-100.
370370+</Conv>
371371+372372+I'd also need to figure out how to [fix Gitea's RPM package serving support so that it signs packages for me](https://github.com/go-gitea/gitea/pull/27069), but would be solvable. Most of the work is already done, I'd just need to take over the PR and help push it over the finish line.
373373+374374+### Installing Fedora CoreOS
375375+376376+The method I'm going to be using to install Fedora CoreOS is to use [`coreos-installer`](https://coreos.github.io/coreos-installer/) to build an ISO image with a configuration file generated by [`butane`](https://coreos.github.io/butane/).
377377+378378+To make things extra _fun_, I'm writing this on a Mac, which means I will need to have a Fedora environment handy to build the ISO because Fedora only ships Linux builds of `coreos-installer` and `butane`.
379379+380380+<Conv name="Mara" mood="hacker">
381381+ This installation was adapted from [this
382382+ tutorial](https://devnonsense.com/posts/k3s-on-fedora-coreos-bare-metal/),
383383+ with modifications made because I'm using a MacBook instead of a Fedora
384384+ machine.
385385+</Conv>
386386+387387+First, I needed to install [Podman Desktop](https://podman-desktop.io/), which is like the Docker Desktop app except it uses the [Red Hat Podman](https://podman.io/) stack instead of the Docker stack. For the purposes of this article, they are functionally equivalent.
388388+389389+I made a new repo/folder and then started up a Fedora container:
390390+391391+```
392392+podman run --rm -itv .:/data fedora:latest
393393+```
394394+395395+I then installed the necessary packages:
396396+397397+```
398398+dnf -y install coreos-installer butane ignition-validate
399399+```
400400+401401+And then I copied over the template from the Fedora CoreOS k3s tutorial into `chrysalis.bu`. I edited it to have the hostname `chrysalis`, loaded my SSH keys into it, and then ran the script to generate a prebaked install ISO. I loaded it on a flashdrive and then stuck it into the same Mac Pro from the last episode.
402402+403403+<Conv name="Cadey" mood="coffee">
404404+ Annoyingly, it seems that the right file extension for Butane configs is `.bu`
405405+ and that there isn't a VSCode plugin for it. If I stick with Fedora CoreOS,
406406+ I'll have to make something that makes `.bu` files get treated as YAML files
407407+ or something. I just told VSCode to treat them as YAML files for now.
408408+</Conv>
409409+410410+It installed perfectly. I suspect that the actual Red Hat installer can be changed to just treat this machine as a normal EFI platform without any issues, but that is a bug report for another day. Intel Macs are quickly going out of support anyways, so it's probably not going to be the highest priority for then even if I did file that bug.
411411+412412+I got k3s up and running and then I checked the version number. My config was having me install k3s version 1.27.10, which is much older than the current version [1.30.0 "Uwubernetes"](https://kubernetes.io/blog/2024/04/17/kubernetes-v1-30-release/). I fixed the butane config to point to the new version of k3s and then I tried to find a way to apply it to my running machine.
413413+414414+<Conv name="Aoi" mood="wut">
415415+ That should be easy, right? You should just need to push the config to the
416416+ server somehow and then it'll reconverge, right?
417417+</Conv>
418418+419419+Yeah, about that. It turns out that Fedora CoreOS is very much on the side of "cattle, not pets" when it comes to datacenter management. The Fedora CoreOS view is that any time you need to change out the Ignition config, you should reimage the machine. This makes sense for a lot of hyperconverged setups where this is as simple as pushing a button and waiting for it to come back.
420420+421421+<Conv name="Cadey" mood="wat">
422422+ I'm not sure what the ideal Fedora CoreOS strategy for handling disk-based
423423+ application state is. Maybe it's "don't fuck around with prod enough that this
424424+ is an issue", which is reasonable enough. I remember that with normal CoreOS
425425+ the advice was "please avoid relying on local storage as much as you can", but
426426+ they probably solved that by this point, either with a blessed state partition
427427+ or by continuing the advice to avoid local storage as much as you can. Further
428428+ research would be required.
429429+</Conv>
430430+431431+However, my homelab is many things, but it isn't a hyperconverged datacenter setup. It's where I fuck around so I can find out (and then launder that knowledge through you to the rest of the industry for Patreon money and ad impressions). If I want to adopt an OS in the homelab, I need the ability to change my mind without having to burn four USB drives and reflash my homelab.
432432+433433+This was a bummer. I'm gonna have to figure out something else to get Kubernetes up and running for me.
434434+435435+## Other things I evaluated and ended up passing on
436436+437437+I was told by a coworker that [k3OS](https://k3os.io/) is a great way to have a "boot to Kubernetes" environment that you don't have to think about. This is by the Rancher team, which I haven't heard about in ages since I played with [RancherOS](https://rancher.com/docs/os/v1.x/en/) way back in the before times.
438438+439439+RancherOS was super wild for its time. It didn't have a package manager, it had the Docker daemon. Two Docker daemons in fact, one for the "system" docker daemon that handled things like TTY sessions, DHCP addresses, device management, system logs, and the like. The other Docker daemon was for the userland, which was where you ran your containers.
440440+441441+<Conv name="Cadey" mood="coffee">
442442+ I kinda miss how wild RancherOS was. It was great for messing around with at
443443+ one of my former workplaces. We didn't use it for anything super critical, but
444444+ it was a great hypervisor for a Minecraft server.
445445+</Conv>
446446+447447+I tried to get K3os up and running, but then I found out that it's deprecated. That information isn't on the website, it's on the [getting started documentation](https://github.com/rancher/k3os/blob/master/README.md#quick-start). It's apparently replaced by [Elemental](https://elemental.docs.rancher.com/), which seems to be built on top of OpenSUSE (kinda like how Fedora CoreOS is built on Fedora).
448448+449449+<Conv name="Aoi" mood="wut">
450450+ Didn't Rancher get bought out by SUSE? That'd explain why everything is
451451+ deprecated except for something based on OpenSUSE.
452452+</Conv>
453453+<Conv name="Cadey" mood="coffee">
454454+ Oh. Right. That makes sense. I guess I'll have to look into Elemental at some
455455+ point. Maybe I'll do that in the future.
456456+</Conv>
457457+458458+I'm gonna pass on this for now. Maybe in the future.
459459+460460+## The Talos Principle
461461+462462+[Straton of Stageira](https://talosprinciple.fandom.com/wiki/Straton_of_Stageira) once argued that the mythical construct Talos (an automaton that experienced qualia and had sapience) proved that there was nothing special about mankind. If a product of human engineering could have the same kind of qualia that people do, then realistically there is nothing special about people when compared to machines.
463463+464464+To say that [Talos Linux](https://www.talos.dev/) is minimal is a massive understatement. It only has literally [12 binaries in it](https://www.siderolabs.com/blog/there-are-only-12-binaries-in-talos-linux/). I've been conceptualizing it as "what if [gokrazy](/blog/gokrazy/) was production-worthy?".
465465+466466+My main introduction to it was last year at [All Systems Go!](https://media.ccc.de/v/all-systems-go-2023-202-talos-linux-trustedboot-for-a-minimal-immutable-os) by a fellow speaker. I'd been wanting to try something like this out for a while, but I haven't had a good excuse to sample those waters until now. It's really intriguing because of how damn minimal it is.
467467+468468+So I downloaded the arm64 ISO and set up a VM on my MacBook to fuck around with it. Here's a few of the things that I learned in the process:
469469+470470+<Conv name="Cadey" mood="enby">
471471+ If you haven't tried out [UTM](https://mac.getutm.app) yet, you are really
472472+ missing out. It's the missing virtual machine hypervisor for macOS. It's one
473473+ of the best apps I know of for running virtual machines on Apple Silicon. I
474474+ mostly use it to run random Linux machines on my MacBook, but I've also heard
475475+ of people using it to play [Half-Life on an
476476+ iPad](https://youtu.be/LrLDKYFyLMM). Highly suggest.
477477+</Conv>
478478+479479+UTM has two modes it can run a VM in. One is "Apple Virtualization" mode that gives you theoretically higher performance at the cost of less options when it comes to networking (probably because `Hypervisor.framework` has less knobs available to control the VM environment). In order to connect the VM to a shared network (so you can poke it directly with `talosctl` commands without needing overlay VPNs or crazy networking magic like that), you need to create it without "Apple Virtualization" checked. This does mean you can't expose Rosetta to run amd64 binaries (and performance might be theoretically slower in a way you can't perceive given the minimal linux distros in play), but that's an acceptable tradeoff.
480480+481481+<Picture
482482+ path="xedn/dynamic/4bda0ab5-46db-4abd-b37b-8f14d2882e60"
483483+ desc="UTM showing off the 'Shared Network' pane, you want this enabled to get access to the 192.168.65.0/24 network to poke your VM directly."
484484+/>
485485+486486+Talos Linux is completely declarative for the base system and really just exists to make Kubernetes easier to run. One of my favorite parts has to be the way that you can combine different configuration snippets together into a composite machine config. Let's say you have a base "control plane config" in `controlplane.yaml` and some host-specific config in `hosts/hostname.yaml`. Your `talosctl apply-config` command would look like this:
487487+488488+```sh
489489+talosctl apply-config -n kos-mos -f controlplane.yaml -p @patches/subnets.yaml -p @hosts/kos-mos.yaml
490490+```
491491+492492+This allows your `hosts/kos-mos.yaml` file to look like this:
493493+494494+```yaml
495495+cluster:
496496+ apiServer:
497497+ certSANs:
498498+ - 100.110.6.17
499499+500500+machine:
501501+ network:
502502+ hostname: kos-mos
503503+ install:
504504+ disk: /dev/nvme0n1
505505+```
506506+507507+which allows me to do generic settings cluster-wide _and then_ specific settings for each host (just like I have with my Nix flakes repo). For example, I have a few homelab nodes with nvidia GPUs that I'd like to be able to run AI/large langle mangle tasks on. I can set up the base config to handle generic cases and then enable the GPU drivers only on the nodes that need them.
508508+509509+<Conv name="Cadey" mood="coffee">
510510+ By the way, resist the temptation to install the nvidia GPU drivers on
511511+ machines that do not need them. It will result in the nvidia GPU drivers
512512+ trying to load in a loop, then complaining that they can't find the GPU, and
513513+ then trying to load again. In order to unstuck yourself from that situation,
514514+ you have to reimage the machine by attaching a crash cart and selecting the
515515+ "wipe disk and boot into maintenance mode" option. This was fun to figure out
516516+ by hand, but it was made easier with the `talosctl dashboard` command.
517517+</Conv>
518518+519519+### The Talosctl Dashboard
520520+521521+I just have to take a moment to gush about the `talosctl dashboard` command. It's a TUI interface that lets you see what your nodes are doing. When you boot a metal Talos Linux node, it opens the dashboard by default so you can watch the logs as the system wakes up and becomes active.
522522+523523+When you run it on your laptop, it's as good as if not better than having SSH access to the node. All the information you could want is right there at a glance and you can connect to mulitple machines at once. Just look at this:
524524+525525+<Picture
526526+ path="xedn/dynamic/f6bb22c4-f26d-41aa-868d-56dc7af841b3"
527527+ desc="The talosctl dashboard, it's a TUI interface that lets you see what is going on with your nodes."
528528+/>
529529+530530+Those three nodes can be swapped between by pressing the left and right arrow keys. It's the best kind of simple, the kind that you don't have to think about in order to use it. No documentation needed, just run the command and go on instinct. I love it.
531531+532532+You can press F2 to get a view of the processes, resource use, and other errata. It's everything you could want out of htop, just without the ability to run Doom.
533533+534534+### Making myself a Kubernete
535535+536536+<Conv name="Cadey" mood="coffee">
537537+ A little meta note because it's really easy to get words conflated here:
538538+ whenever I use CapitalizedWords, I'm talking about the Kubernetes concepts,
539539+ not the common English words. It's really hard for me to avoid talking about
540540+ the word "service" given the subject matter. Whenever you see "Service",
541541+ "Deployment", "Secret", "Ingress", or the like; know that I'm talking about
542542+ the Kubernetes definition of those terms.
543543+</Conv>
544544+545545+Talos Linux is built to do two things:
546546+547547+1. Boot into Linux
548548+2. Run Kubernetes
549549+550550+That's it. It's beautifully brutalist. I love it so far.
551551+552552+I decided to start with `kos-mos` arbitrarily. I downloaded the ISO, tried to use balenaEtcher to flash it to a USB drive and then windows decided that now was the perfect time to start interrupting me with bullshit related to Explorer desperately trying to find and mount USB drives.
553553+554554+<Conv name="Cadey" mood="coffee">
555555+ Lately Windows has been going out of its way to actively interfere when I try
556556+ to do anything fancy or convenient. I only tolerate it for games, but I am
557557+ reconsidering my approach. If only Wayland supported accessibility hooks.
558558+</Conv>
559559+560560+I was unable to use balenaEtcher to write it, but then I found out that [Rufus](https://rufus.ie/en/) can write ISOs to USB drives in a way that doesn't rely on Windows to do the mounting or writing. That worked and I had `kos-mos` up and running in short order.
561561+562562+<Conv name="Cadey" mood="enby">
563563+ This is when I found out about the hostname patch yaml trick, so it booted
564564+ into a randomly generated `talos-whatever` hostname by default. This was okay,
565565+ but I wanted to have the machine names be more meaningful so I can figure out
566566+ what's running where at a glance. Changing hostnames was trivial though, you
567567+ can do it from the dashboard worst case. I'm aware that this is defeating the
568568+ point of the "cattle, not pets" flow that a lot of modern Linux distributions
569569+ want you to go down, but my homelab servers are my pets.
570570+</Conv>
571571+572572+After bootstrapping etcd and exposing the subnet routes, I made an nginx deployment with a service as a "hello world" to ensure that things were working properly. Here's the configuration I used:
573573+574574+```yaml
575575+---
576576+apiVersion: apps/v1
577577+kind: Deployment
578578+metadata:
579579+ name: nginx
580580+ labels:
581581+ app.kubernetes.io/name: nginx
582582+spec:
583583+ replicas: 3
584584+ selector:
585585+ matchLabels:
586586+ app.kubernetes.io/name: nginx
587587+ template:
588588+ metadata:
589589+ labels:
590590+ app.kubernetes.io/name: nginx
591591+ spec:
592592+ containers:
593593+ - name: nginx
594594+ image: nginx:1.14.2
595595+ ports:
596596+ - containerPort: 80
597597+---
598598+apiVersion: v1
599599+kind: Service
600600+metadata:
601601+ name: nginx
602602+spec:
603603+ selector:
604604+ app.kubernetes.io/name: nginx
605605+ ports:
606606+ - protocol: TCP
607607+ port: 80
608608+ targetPort: 80
609609+ type: ClusterIP
610610+```
611611+612612+<Conv name="Mara" mood="hacker">
613613+For those of you that don't grok k8s yaml, this configuration creates two things:
614614+615615+- A `Deployment` (think of it as a set of `Pods` that can be scaled up or down and upgraded on a rolling basis) that runs three copies of [nginx](https://nginx.org/en/) showing the default "welcome to nginx" page, with port 80 marked as "open" to other things.
616616+- A `ClusterIP Service` that exposes the nginx `Deployment`'s port 80 to a stable IP address within the cluster. This cluster IP will be used by other services to talk to the nginx `Deployment`.
617617+618618+Normally these `ClusterIP` services are only exposed in the cluster (as the name implies), but when you have overlay networks and subnet routing in the mix, you can do anything, such as poking the service from your laptop:
619619+620620+</Conv>
621621+622622+<Picture
623623+ path="xedn/dynamic/c4d36dd3-c8f1-4115-a504-48b9e6412fc8"
624624+ desc="The 'welcome to nginx' page on the url http://nginx.default.svc.alrest.xeserv.us, which is not publicly exposed to you."
625625+/>
626626+627627+Once this is up, you're golden. You can start deploying more things to your cluster and then they can talk to eachother. One of the first things I deployed was a Reddit/Discord bot that I maintain for a community I've been in for a long time. It's a simple stateless bot that only needs a single deployment to run. You can see its source code and deployment manifest [here](https://github.com/Xe/x/tree/master/cmd/sapientwindex).
628628+629629+The only weird part here is that I needed to set up secrets for handling the bot's Discord webhook. I don't have a secret vault set up (looking onto setting up the 1password one out of convenience because I already use it at home), so I yolo-created the secret with `kubectl create secret generic sapientwindex --from-literal=DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/ABC123` and then mounted it into the pod as an environment variable. The relevant yaml snippet is under the `bot` container's `env` key:
630630+631631+```yaml
632632+env:
633633+ - name: DISCORD_WEBHOOK_URL
634634+ valueFrom:
635635+ secretKeyRef:
636636+ name: sapientwindex
637637+ key: DISCORD_WEBHOOK_URL
638638+```
639639+640640+This is a little more verbose than I'd like, but I understand why it has to be this way. Kubernetes is the most generic tool you can make, as such it has to be able to adapt to any workflow you can imagine. Kubernetes manifests can't afford to make too many assumptions, so they simply elect not to as much as possible. As such, you need to spell out all your assumptions by hand.
641641+642642+I'll get this refined in the future with templates or whatever, but for now my favorite part about it is that it works.
643643+644644+<Conv name="Aoi" mood="wut">
645645+ Why are you making your secrets environment variables instead of mounting them
646646+ as a filesystem?
647647+</Conv>
648648+<Conv name="Cadey" mood="aha">
649649+ I want to have this as an environment variable because this bot was made with
650650+ the [12 factor app](https://12factor.net/) methodology in mind. It's a
651651+ stateless bot that only needs a single environment variable to run, so I'm
652652+ going to keep it that way. The bot is also already programmed to read from the
653653+ environment variable (but I could have it read the environment variable from
654654+ the
655655+ [flagconfyg](https://github.com/Xe/x/tree/master/internal/confyg/flagconfyg)
656656+ file if I needed to). If there were more than 10 variables, I'd probably mount
657657+ the secret as a flagconfyg or .env file instead. If I wanted to support
658658+ secrets as a filesystem, I'd need to write some extra code to import a
659659+ directory tree as flag values as my /x/ repo (and other projects of mine) use
660660+ [package flag](https://pkg.go.dev/flag) for managing secrets and other
661661+ configuration variables. I'm lazy.
662662+</Conv>
663663+664664+After I got that working, I connected some other nodes and I've ended up with this:
665665+666666+```
667667+$ kubectl get nodes
668668+NAME STATUS ROLES AGE VERSION
669669+chrysalis Ready control-plane 20h v1.30.0
670670+kos-mos Ready control-plane 20h v1.30.0
671671+ontos Ready control-plane 20h v1.30.0
672672+```
673673+674674+The next big thing to get working is to get a bunch of operators working so that I can have my cluster dig its meaty claws into various other things.
675675+676676+## What the hell is an operator anyways?
677677+678678+In Kubernetes land, an operator is a thing you install into your cluster that makes it integrate with another service or provides some functionality. For example, the [1Password operator](https://developer.1password.com/docs/k8s/k8s-operator/) lets you import 1Password data into your cluster as Kubernetes secrets. It's effectively how you extend Kubernetes to do more things with the same Kubernetes workflow you're already used to.
679679+680680+One of the best examples of this is the 1Password operator I mentioned. It's how I'm using 1Password to store secrets for my apps in my cluster. I can then edit them with the 1Password app on my PC or MacBook and the relevant services will restart automatically with the new secrets.
681681+682682+So I installed the operator with Helm and then it worked the first time. I was surprised, given how terrible Helm is in my experience.
683683+684684+<Conv name="Aoi" mood="wut">
685685+ Why is Helm bad? It's the standard way to install reusable things in
686686+ Kubernetes.
687687+</Conv>
688688+<Conv name="Cadey" mood="coffee">
689689+ Helm uses string templating to template structured data. It's like using `sed`
690690+ to template JSON. It works, but you have to abuse a lot of things like the
691691+ [`indent`](https://helm.sh/docs/chart_template_guide/yaml_techniques/#indenting-and-templates)
692692+ function in order for things to be generically applicable. It's a mess, but
693693+ only when you try and use it in earnest across your stack. It's what made me
694694+ nearly burn out of the industry.
695695+</Conv>
696696+<Conv name="Aoi" mood="coffee">
697697+ Why is so much of this stuff just one or two steps away from being really
698698+ good?
699699+</Conv>
700700+<Conv name="Numa" mood="delet">
701701+ Venture capital! They used to have a way to do structured templates but it was
702702+ deprecated and removed in Helm 3.0, so we all get to suffer together.
703703+</Conv>
704704+705705+The only hard part I ran into was that it wasn't obvious how I should assemble the reference strings for 1Password secrets. When you create the 1Password secret syncing object, it looks like this:
706706+707707+```yaml
708708+apiVersion: onepassword.com/v1
709709+kind: OnePasswordItem
710710+metadata:
711711+ name: sapientwindex
712712+spec:
713713+ itemPath: "vaults/lc5zo4zjz3if3mkeuhufjmgmui/items/cqervqahekvmujrlhdaxgqaffi"
714714+```
715715+716716+This tells the operator to create a secret named `sapientwindex` in the default namespace with the item path `vaults/lc5zo4zjz3if3mkeuhufjmgmui/items/cqervqahekvmujrlhdaxgqaffi`. The item path is made up of the vault ID (`lc5zo4zjz3if3mkeuhufjmgmui`) and the item ID (`cqervqahekvmujrlhdaxgqaffi`). I wasn't sure how to get these in the first place, but I found the vault ID with the `op vaults list` command and then figured out you can enable right-clicking in the 1Password app to get the item ID.
717717+718718+To enable this, go to Settings -> Advanced -> Show debugging tools in the 1Password app. This will let you right-click any secret and choose "Copy item UUID" to get the item ID for these secret paths.
719719+720720+This works pretty great, I'm gonna use this extensively going forward. It's gonna be slightly painful at first, but once I get into the flow of this (and realistically write a generator that pokes the 1password cli to scrape this information more easily) it should all even out.
721721+722722+## Trials and storage tribulations
723723+724724+As I said at the end of [my most recent conference talk](/talks/2024/shashin/), storage is one of the most annoying bullshit things ever. It's extra complicated with Talos Linux in particular because of how it uses the disk. Most of the disks of my homelab are Talos' "ephemeral state" partitions, which are used for temporary storage and wiped when the machine updates. This is great for many things, but not for persistent storage with [PersistentVolumes/PersistntVolumeClaims](https://kubernetes.io/docs/concepts/storage/persistent-volumes/).
725725+726726+<Conv name="Mara" mood="hacker">
727727+ If you haven't used PersistentVolumes before, they are kinda like [Fly
728728+ Volumes](https://fly.io/docs/reference/volumes/) or Docker Volumes. The main
729729+ difference is that a PersistentVolume is usually shared between hosts, so that
730730+ you can mount the same PersistentVolume on Pods located on multiple cluster
731731+ Nodes. It's really neat.
732732+ [StorageClasses](https://kubernetes.io/docs/concepts/storage/storage-classes/)
733733+ let you handle things like data locality, backup policies, and more. This lets
734734+ you set up multiple providers so that you can have some things managed by your
735735+ cluster-local storage provider, some managed by the cloud provider, and some
736736+ super-yolo ones directly mounted to the host filesystem.
737737+</Conv>
738738+739739+I have tried the following things:
740740+741741+- [Longhorn](https://longhorn.io/): a distributed block storage thing for Kubernetes by the team behind Rancher. It's pretty cool, but I got stuck at trying to get it actually running on my cluster. The pods were just permanently stuck in the `Pending` state due to etcd not being able to discover itself.
742742+- [OpenEBS](https://github.com/openebs/openebs): another distributed block storage thing for Kubernetes by some team of some kind. It claims to be the most widely used storage thing for Kubernetes, but I couldn't get it to work either.
743743+744744+Among the things I've realized when debugging this is that _no matter what_, many storage things for Kubernetes will hardcode the cluster DNS name to be `cluster.local`. I made my cluster use the DNS name `alrest.xeserv.us` following the advice of one of my SRE friends to avoid using "fake" DNS names as much as possible . This has caused me no end of trouble, as many things in the Kubernetes ecosystem assume that the cluster DNS name is `cluster.local`. It turns out that many Kubernetes ecosystem tools hard-assume the DNS name because the CoreDNS configs in many popular Kubernetes platforms (like AWS EKS, Azure whatever-the-heck, and GKE) have broken DNS configs that make relative DNS names not work reliably. As a result, people have hardcoded the DNS name to `cluster.local` in many places in both configuration and code.
745745+746746+<Conv name="Aoi" mood="coffee">
747747+ Yet again pragmatism wins out over correctness in the most annoying ways. Why
748748+ does everything have to be so _bad_?
749749+</Conv>
750750+<Conv name="Mara" mood="hacker">
751751+ To be fair to the Kubernetes ecosystem maintainers, they are faced with a
752752+ pretty impossible task. They have to be able to be flexible enough to handle
753753+ random bespoke homelab clusters and whatever the cloud providers think is
754754+ sensible. That is such a wide range of configurations that I don't think it's
755755+ really possible to do anything _but_ make those assumptions about how things
756756+ work. It's a shame that changing the cluster DNS name breaks so much, but it's
757757+ understandable because most cloud providers don't expose that setting to
758758+ users. It always sucks to use "fake" DNS names because they can and will
759759+ become top-level domains [like what happened with
760760+ `.dev`](https://prinsfrank.nl/2019/02/26/With-the-new-dev-domains-googles-dont-be-evil-phase-is-a-distant-memory).
761761+ It would be nice if Kubernetes encouraged people to choose their own "real"
762762+ domain names, but it's understandable that they ended up with `cluster.local`
763763+ because `.local` [is registered as a "special-use domain
764764+ name"](https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml)
765765+ but the IETF.
766766+</Conv>
767767+768768+Fixing this was easy, I had to edit the CoreDNS ConfigMap to look like this:
769769+770770+```yaml
771771+data:
772772+ Corefile: |-
773773+ .:53 {
774774+ errors
775775+ health {
776776+ lameduck 5s
777777+ }
778778+ ready
779779+ log . {
780780+ class error
781781+ }
782782+ prometheus :9153
783783+784784+ kubernetes cluster.local alrest.xeserv.us in-addr.arpa ip6.arpa {
785785+ pods insecure
786786+ fallthrough in-addr.arpa ip6.arpa
787787+ }
788788+ forward . /etc/resolv.conf
789789+ cache 30
790790+ loop
791791+ reload
792792+ loadbalance
793793+ }
794794+```
795795+796796+I prepended the `cluster.local` "domain name" to the `kubernetes` block. Then I deleted the CoreDNS pods in the `kube-system` namespace and they were promptly restarted with the new configuration. This at least got me to the point where normal DNS things worked again.
797797+798798+<Conv name="Mara" mood="hacker">
799799+ I later found out I didn't need to do this. When CoreDNS sees the ConfigMap
800800+ update, it'll automatically reload the config. However, SRE instinct kicks in
801801+ when you're dealing with unknowns and sometimes the placebo effect of
802802+ restarting the damn thing by hand makes you feel better. Feeling better can be
803803+ way more important than actually fixing the problem, especially when you're
804804+ dealing with a lot of new technology.
805805+</Conv>
806806+<Conv name="Numa" mood="delet">
807807+ There's no kill like overkill afterall!
808808+</Conv>
809809+810810+However, this didn't get Longhorn working. The manager container was just stuck trying to get created. Turns out the solution was really stupid and I want to explain what's going on here so that you can properly commiserate with me over the half a day I spent trying to get this working.
811811+812812+Talos Linux sets a default security policy that blocks the Longhorn manager from running. This is because the Longhorn manager runs as root and Talos Linux is paranoid about security. In order to get Longhorn running, I had to add the following annotations to the Longhorn namespace:
813813+814814+```yaml
815815+apiVersion: v1
816816+kind: Namespace
817817+metadata:
818818+ name: longhorn-system
819819+ labels:
820820+ pod-security.kubernetes.io/enforce: privileged
821821+ pod-security.kubernetes.io/enforce-version: latest
822822+ pod-security.kubernetes.io/audit: privileged
823823+ pod-security.kubernetes.io/audit-version: latest
824824+ pod-security.kubernetes.io/warn: privileged
825825+ pod-security.kubernetes.io/warn-version: latest
826826+```
827827+828828+After you do this, you need to delete the longhorn-deployer _Pod_ and then wait about 10-15 minutes for the entire system to converge. For some reason it doesn't automatically restart when labels are changed, but that is very forgiveable given how many weird things are at play with this ecosystem. Either way, getting this working _at all_ was a huge relief.
829829+830830+<Conv name="Aoi" mood="wut">
831831+ Wasn't Longhorn part of the SUSE acquistion?
832832+</Conv>
833833+<Conv name="Cadey" mood="enby">
834834+ Yes, but they also donated Longhorn to the CNCF, so it's going to be
835835+ maintained until it's inevitably deprecated in favor of yet another storage
836836+ product. Hopefully there's an easy migration path, but I'm not going to worry
837837+ about this until I have to.
838838+</Conv>
839839+840840+Once Longhorn starts up, you can create a PersistentVolumeClaim and attach it to a pod:
841841+842842+```yaml
843843+apiVersion: v1
844844+kind: PersistentVolumeClaim
845845+metadata:
846846+ name: longhorn-volv-pvc
847847+ namespace: default
848848+spec:
849849+ accessModes:
850850+ - ReadWriteOnce
851851+ storageClassName: longhorn
852852+ resources:
853853+ requests:
854854+ storage: 256Mi
855855+---
856856+apiVersion: v1
857857+kind: Pod
858858+metadata:
859859+ name: volume-test
860860+ namespace: default
861861+spec:
862862+ restartPolicy: Always
863863+ containers:
864864+ - name: volume-test
865865+ image: nginx:stable-alpine
866866+ imagePullPolicy: IfNotPresent
867867+ livenessProbe:
868868+ exec:
869869+ command:
870870+ - ls
871871+ - /data/lost+found
872872+ initialDelaySeconds: 5
873873+ periodSeconds: 5
874874+ volumeMounts:
875875+ - name: vol
876876+ mountPath: /data
877877+ ports:
878878+ - containerPort: 80
879879+ volumes:
880880+ - name: vol
881881+ persistentVolumeClaim:
882882+ claimName: longhorn-volv-pvc
883883+```
884884+885885+<Conv name="Cadey" mood="facepalm">
886886+ I feel so dumb right now. It was just a security policy mismatch.
887887+</Conv>
888888+<Conv name="Numa" mood="happy">
889889+ Hey, at least it was a dumb problem. Dumb problems are always so much easier
890890+ to deal with than the not-dumb problems. The not-dumb problems end up sucking
891891+ so much and drain you of your soul energy.
892892+</Conv>
893893+894894+Longhorn ended up working, so I [set up backups](https://longhorn.io/docs/1.6.1/snapshots-and-backups/scheduling-backups-and-snapshots/) to [Tigris](https://tigrisdata.com) and then I plan to not think about it until I need to. The only catch is that I need to label every PersistentVolumeClaim with `recurring-job-group.longhorn.io/backup: enabled` to make my backup job run:
895895+896896+```yaml
897897+apiVersion: longhorn.io/v1beta1
898898+kind: RecurringJob
899899+metadata:
900900+ name: backup
901901+ namespace: longhorn-system
902902+spec:
903903+ cron: "0 0 * * *"
904904+ task: "backup"
905905+ groups:
906906+ - default
907907+ retain: 4
908908+ concurrency: 2
909909+```
910910+911911+<Conv name="Cadey" mood="enby">
912912+ Thanks for the backup space Ovais & co! I wonder how efficient this really is
913913+ because most of the blocks (based on unscientific random clicking around in
914914+ the Tigris console) are under the threshold for [being inlined to
915915+ FoundationDB](https://www.tigrisdata.com/docs/overview/#fast-small-object-retrieval).
916916+ I'll have to ask them about it once I get some more significant data workloads
917917+ in the mix. Realistically, it's probably fine and will end up being a decent
918918+ stress test for them.
919919+</Conv>
920920+921921+Hopefully I won't need to think about this for a while. At its best, storage is invisible.
922922+923923+## The ~~factory~~ cluster must grow
924924+925925+I dug `logos` out of mothballs and then I plugged in the Talos Linux USB. I then ran the `logos` command to install Talos Linux on the machine. It worked perfectly and I had a new homelab node up and running in no time. All I had to do was:
926926+927927+- Get it hooked up to Ethernet and power
928928+- Boot it off of the Talos Linux USB stick
929929+- Apply the config with `talosctl` from my macbook
930930+- Wait for it to reboot and everything to green up in `kubectl`
931931+932932+That's it. This is what every homelab OS should strive to be.
933933+934934+I also tried to add my [Win600](/blog/anbernic-win600-review/) to the cluster, but I don't think Talos supports wi-fi. I'm asking in the Matrix channel and in a room full of experts. I was able to get it to connect to ethernet over USB in a hilariously jankriffic setup though:
935935+936936+<Picture
937937+ path="xedn/dynamic/a1f2dea0-158d-4ee4-b708-3802f54a734e"
938938+ desc="An Anbernic Win600 with its screen sideways booted into Talos Linux. It is precariously mounted on the floor with power going in on one end and ethernet going in on the other. It is not a production-worthy setup."
939939+/>
940940+941941+<Conv name="Aoi" mood="coffee">
942942+ Why would you do this to yourself?
943943+</Conv>
944944+<Conv name="Numa" mood="happy">
945945+ Science isn't about why, it's about why not!
946946+</Conv>
947947+948948+I seriously can't believe this works. It didn't work well enough to stay in production, but it's worth a laugh or two at least. I ended up removing this node so that I can have floor space back. I'll have to figure out how to get it on the network properly later, maybe after DevOpsDays KC.
949949+950950+## ingressd and related fucking up
951951+952952+I was going to write about a super elegant hack that I'm doing to get ingress from the public internet to my homelab here, but I fucked up again and I potentially got to do etcd surgery.
953953+954954+The hack I was trying to do was creating a userspace wireguard network for handling HTTP/HTTPS ingress from the public internet. I chose to use the network `10.255.255.0/24` for this (I had a TypeScript file to configure the WireGuard keys and everything). Apparently Talos Linux configured etcd to prefer anything in `10.0.0.0/8` by default. This has lead to the following bad state:
955955+956956+```
957957+$ talosctl etcd members -n 192.168.2.236
958958+NODE ID HOSTNAME PEER URLS CLIENT URLS LEARNER
959959+192.168.2.236 3a43ba639b0b3ec3 chrysalis https://10.255.255.16:2380 https://10.255.255.16:2379 false
960960+192.168.2.236 d07a0bb98c5c225c kos-mos https://10.255.255.17:2380 https://192.168.2.236:2379 false
961961+192.168.2.236 e78be83f410a07eb ontos https://10.255.255.19:2380 https://192.168.2.237:2379 false
962962+192.168.2.236 e977d5296b07d384 logos https://10.255.255.18:2380 https://192.168.2.217:2379 false
963963+```
964964+965965+This is uhhh, not good. The normal strategy for recovering from an etcd split brain involves stopping etcd on all nodes and then recovering one of them, but I can't do that because `talosctl` doesn't let you stop etcd:
966966+967967+```
968968+$ talosctl service etcd -n 192.168.2.196 stop
969969+error starting service: 1 error occurred:
970970+ * 192.168.2.196: rpc error: code = Unknown desc = service "etcd" doesn't support stop operation via API
971971+```
972972+973973+When you get etcd into this state, it is generally very hard to convince it otherwise without doing database surgery and suffering the pain of having fucked it up. Fixing this is a very _doable_ process, but I didn't really wanna deal with it.
974974+975975+I ended up blowing away the cluster and starting over. I tried using TESTNET (192.0.2.0/24) for the IP range but ran into issues where my super hacky userspace WireGuard code wasn't working right. I gave up at this point and ended up using my existing WireGuard mesh for ingress. I'll have to figure out how to do this properly later.
976976+977977+<Conv name="Cadey" mood="facepalm">
978978+ While I was resetting the cluster, I ran into a kinda hilarious problem:
979979+ asking Talos nodes to wipe their disk and reset all state makes them wipe
980980+ _everything_, including the system partition. I did ask it to wipe
981981+ _everything_, but I didn't think it would nuke the OS too. It was kind of a
982982+ hilarious realization when I ended up figuring out what I did, but it's good
983983+ to know that "go die" means that it will kill everything. That's kind of a
984984+ dangerous call to expose without some kind of confirmation, but I guess that
985985+ is at the pay-to-win tier with [Sidero Labs
986986+ support](https://www.siderolabs.com/pricing/). I'll probably end up paying for
987987+ a hobby subscription at some point, just to support the company powered my
988988+ homelab's hopes and dreams. Money does tend to let one buy goods and services.
989989+</Conv>
990990+991991+`ingressd` ended up becoming a simple TCP proxy with added [PROXY protocol](http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support so that ingress-nginx could get the right source IP addresses. It's nothing to write home about, but it's my simple TCP proxy that I probably could have used something off the shelf for.
992992+993993+<Conv name="Numa" mood="delet">
994994+ The not-invented-here is strong with this one.
995995+</Conv>
996996+<Conv name="Cadey" mood="aha">
997997+ Something something future expansion when I have time/energy.
998998+</Conv>
999999+10001000+### Using `ingressd` (why would you do this to yourself)
10011001+10021002+If you want to install `ingressd` for your cluster, here's the high level steps:
10031003+10041004+<Conv name="Mara" mood="hmm">
10051005+ Keep in mind, `ingressd` has no real support. If you run it, you are on your
10061006+ own. Good luck if so!
10071007+</Conv>
10081008+10091009+1. Gather the secret keys needed for this terraform manifest (change the domain for Route 53 to your own domain): https://github.com/Xe/x/blob/master/cmd/ingressd/tf/main.tf
10101010+2. Run `terraform apply` in the directory with the above manifest
10111011+3. Go a level up and run `yeet` to build the ingressd RPM
10121012+4. Install the RPM on your ingressd node
10131013+5. Create the file `/etc/ingressd/ingressd.env` with the following contents:
10141014+10151015+```
10161016+HTTP_TARGET=serviceIP:80
10171017+HTTPS_TARGET=serviceIP:443
10181018+```
10191019+10201020+<Conv name="Mara" mood="hacker">
10211021+ Fetch the service IP from `kubectl get svc -n ingress-nginx` and replace
10221022+ `serviceIP` with it.
10231023+</Conv>
10241024+10251025+This assumes you are subnet routing your Kubernetes node and service network over your WireGuard mesh of choice. If you are not doing that, you can't use `ingressd`.
10261026+10271027+<Conv name="Cadey" mood="aha">
10281028+ This is why I wanted to use a userspace WireGuard connection for this. If I
10291029+ end up implementing this properly, I'm probably gonna end up using two
10301030+ binaries: one on the public ingressd host, and an ingressd-buddy that runs in
10311031+ the cluster.
10321032+</Conv>
10331033+10341034+Also make sure to run the magic firewalld unbreaking commands:
10351035+10361036+```
10371037+firewall-cmd --zone=public --add-service=http
10381038+firewall-cmd --zone=public --add-service=https
10391039+```
10401040+10411041+<Conv name="Cadey" mood="percussive-maintenance">
10421042+ I always forget the magic firewalld unbreaking commands.
10431043+</Conv>
10441044+10451045+## It's always DNS
10461046+10471047+Now that I have [ingress working](http://ingressd.cetacean.club/), it's time for one of the most painful things in the universe: DNS. Since I've used Kubernetes last, [External DNS](https://github.com/kubernetes-sigs/external-dns) is now production-ready. I'm going to use it to manage the DNS records for my services.
10481048+10491049+In typical Kubernetes fashion, it seems that it has gotten incredibly complicated since the last time I used it. It used to be fairly simple, but now installing it requires you to really consider what the heck you are doing. There's also no Helm chart, so you're _really_ on your own.
10501050+10511051+After reading some documentation, I ended up on the following Kubernetes manifest: [external-dns.yaml](https://gist.githubusercontent.com/Xe/8d4d960bcad372a7a2b04265b9eba21c/raw/1b430cf723f1877e764f72d9db720da95f95616b/external-dns.yaml). So that I can have this documented for me as much as it is for you, here is what this does:
10521052+10531053+1. Creates a namespace `external-dns` for `external-dns` to live in.
10541054+2. Creates the [`external-dns` Custom Resource Definitions (CRDs)](https://kubernetes-sigs.github.io/external-dns/v0.14.1/contributing/crd-source/) so that I can make DNS records manually with Kubernetes objects should the spirit move me.
10551055+3. Creates a service account for `external-dns`.
10561056+4. Creates a cluster role and cluster role binding for `external-dns` to be able to read a small subset of Kubernetes objects (services, ingresses, and nodes, as well as its custom resources).
10571057+5. Creates a [1Password secret](https://developer.1password.com/docs/k8s/k8s-operator/) to give `external-dns` Route53 god access.
10581058+6. Creates two deployments of `external-dns`:
10591059+ - One for the CRD powered external DNS to weave DNS records with YAML
10601060+ - One to match on newly created ingress objects and create DNS records for them
10611061+10621062+<Conv name="Aoi" mood="coffee">
10631063+ Jesus christ that's a lot of stuff. It makes sense when you're explaining how
10641064+ it all builds up, but it's a lot.
10651065+</Conv>
10661066+<Conv name="Numa" mood="happy">
10671067+ Welcome to Kubernetes! It's YAML turtles all the way down.
10681068+</Conv>
10691069+10701070+If I ever need to create a DNS record for a service, I can do so with the following YAML:
10711071+10721072+```yaml
10731073+apiVersion: externaldns.k8s.io/v1alpha1
10741074+kind: DNSEndpoint
10751075+metadata:
10761076+ name: something
10771077+spec:
10781078+ endpoints:
10791079+ - dnsName: something.xeserv.us
10801080+ recordTTL: 180
10811081+ recordType: TXT
10821082+ targets:
10831083+ - "We're no strangers to love"
10841084+ - "You know the rules and so do I"
10851085+ - "A full commitment's what I'm thinking of"
10861086+ - "You wouldn't get this from any other guy"
10871087+ - "I just wanna tell you how I'm feeling"
10881088+ - "Gotta make you understand"
10891089+ - "Never gonna give you up"
10901090+ - "Never gonna let you down"
10911091+ - "Never gonna run around and hurt you"
10921092+ - "Never gonna make you cry"
10931093+ - "Never gonna say goodbye"
10941094+ - "Never gonna tell a lie and hurt you"
10951095+```
10961096+10971097+Hopefully I'll never need to do this, but I bet that _something_ will make me need to make a DNS TXT record at some point, and it's probably better to have that managed in configuration management somehow.
10981098+10991099+## cert-manager
11001100+11011101+Now that there's ingress from the outside world and DNS records for my services, it's time to get HTTPS working. I'm going to use [cert-manager](https://cert-manager.io/) for this. It's a Kubernetes native way to manage certificates from Let's Encrypt and other CAs.
11021102+11031103+Unlike nearly everything else in this process, installing cert-manager was relatively painless. I just had to install it with Helm. I also made Helm manage the Custom Resource Definitions, so that way I can easily upgrade them later.
11041104+11051105+<Conv name="Mara" mood="hacker">
11061106+ This is probably a mistake, Helm doesn't handle Custom Resource Definition
11071107+ updates gracefully. This will be corrected in the future, but right now the
11081108+ impetus to care is very low.
11091109+</Conv>
11101110+11111111+The only gotcha here is that there's annotations for Ingresses that you need to add to get cert-manager to issue certificates for them. Here's an example:
11121112+11131113+```yaml
11141114+apiVersion: networking.k8s.io/v1
11151115+kind: Ingress
11161116+metadata:
11171117+ name: kuard
11181118+ annotations:
11191119+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
11201120+spec:
11211121+ # ...
11221122+```
11231123+11241124+<Conv name="Aoi" mood="wut">
11251125+ What's the difference between a label and an annotation anyways? So far it
11261126+ looks like you've been using them interchangeably.
11271127+</Conv>
11281128+<Conv name="Cadey" mood="aha">
11291129+ Labels are intended to be used to help find and select objects, while annotations are for more detailed information. Labels are limited to 64 bytes. The most common label you will see is the `app` or `app.kubernetes.io/name` label which points to the "app" an object is a part of. Annotations are much more intended for storing metadata about the object, and can be up to 256KB in size. They are intended to be used for things like machine-readable data, like the cert-manager issuer annotation.
11301130+</Conv>
11311131+<Conv name="Aoi" mood="wut">
11321132+Why is Longhorn [using labels for subscribing Volumes to backup jobs](https://longhorn.io/docs/1.6.1/snapshots-and-backups/scheduling-backups-and-snapshots/#using-the-kubectl-command) then?
11331133+</Conv>
11341134+<Conv name="Cadey" mood="aha">
11351135+Because Kubernetes labels are indexed in the cluster data store, observe:
11361136+11371137+```
11381138+$ kubectl get Volume --all-namespaces -l=recurring-job-group.longhorn.io/backup=enabled
11391139+NAMESPACE NAME DATA ENGINE STATE ROBUSTNESS SCHEDULED SIZE NODE AGE
11401140+longhorn-system pvc-e1916e66-7f7b-4322-93cd-52dc1fc418f7 v1 attached healthy 2147483648 logos 43h
11411141+```
11421142+11431143+That also extends to other labels, such as `app.kubernetes.io/name`:
11441144+11451145+```
11461146+$ kubectl get all,svc,ing -n mi -l app.kubernetes.io/name=mi
11471147+NAME READY STATUS RESTARTS AGE
11481148+pod/mi-6bd6d8bb44-cg7tf 1/1 Running 0 43h
11491149+11501150+NAME READY UP-TO-DATE AVAILABLE AGE
11511151+deployment.apps/mi 1/1 1 1 43h
11521152+11531153+NAME DESIRED CURRENT READY AGE
11541154+replicaset.apps/mi-6bd6d8bb44 1 1 1 43h
11551155+11561156+NAME CLASS HOSTS ADDRESS PORTS AGE
11571157+ingress.networking.k8s.io/mi-public nginx mi.cetacean.club 100.109.37.97 80, 443 20h
11581158+```
11591159+11601160+Annotations are more useful for meta-information and machine-readable data, like the cert-manager issuer annotation. You could also use it to attribute deployments to a specific git commit or something like that.
11611161+11621162+</Conv>
11631163+11641164+This will make `cert-manager` issue a certificate for the `kuard` ingress using the `letsencrypt-prod` issuer. You can also use `letsencrypt-staging` for testing. The part that you will fuck up is that the documentation mixes `ClusterIssuer` and `Issuer` resources and annotations. Here's what my Let's Encrypt staging and prod issuers look like:
11651165+11661166+```yaml
11671167+apiVersion: cert-manager.io/v1
11681168+kind: ClusterIssuer
11691169+metadata:
11701170+ name: letsencrypt-staging
11711171+spec:
11721172+ acme:
11731173+ # You must replace this email address with your own.
11741174+ # Let's Encrypt will use this to contact you about expiring
11751175+ # certificates, and issues related to your account.
11761176+ email: user@example.com
11771177+ server: https://acme-staging-v02.api.letsencrypt.org/directory
11781178+ privateKeySecretRef:
11791179+ # Secret resource that will be used to store the account's private key.
11801180+ name: letsencrypt-staging-acme-key
11811181+ solvers:
11821182+ - http01:
11831183+ ingress:
11841184+ ingressClassName: nginx
11851185+---
11861186+apiVersion: cert-manager.io/v1
11871187+kind: ClusterIssuer
11881188+metadata:
11891189+ name: letsencrypt-prod
11901190+spec:
11911191+ acme:
11921192+ # You must replace this email address with your own.
11931193+ # Let's Encrypt will use this to contact you about expiring
11941194+ # certificates, and issues related to your account.
11951195+ email: user@example.com
11961196+ server: https://acme-v02.api.letsencrypt.org/directory
11971197+ privateKeySecretRef:
11981198+ # Secret resource that will be used to store the account's private key.
11991199+ name: letsencrypt-prod-acme-key
12001200+ solvers:
12011201+ - http01:
12021202+ ingress:
12031203+ ingressClassName: nginx
12041204+```
12051205+12061206+These `ClusterIssuers` are what the `cert-manager.io/cluster-issuer:` annotation in the ingress object refers to. You can also use `Issuer` resources if you want to scope the issuer to a single namespace, but realistically I know you're lazier than I am so you're going to use `ClusterIssuer`.
12071207+12081208+The flow of all of this looks kinda complicated, but you can visualize it with this handy diagram:
12091209+12101210+
12111211+12121212+Breaking this down, let's assume I've just created this Ingress resource:
12131213+12141214+```yaml
12151215+apiVersion: networking.k8s.io/v1
12161216+kind: Ingress
12171217+metadata:
12181218+ name: kuard
12191219+ annotations:
12201220+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
12211221+spec:
12221222+ ingressClassName: nginx
12231223+ tls:
12241224+ - hosts:
12251225+ - kuard.xeserv.us
12261226+ secretName: kuard-tls
12271227+ rules:
12281228+ - host: kuard.xeserv.us
12291229+ http:
12301230+ paths:
12311231+ - path: /
12321232+ pathType: Prefix
12331233+ backend:
12341234+ service:
12351235+ name: kuard
12361236+ port:
12371237+ name: http
12381238+```
12391239+12401240+<Conv name="Mara" mood="hacker">
12411241+ This is an Ingress named `kuard` with the `letsencrypt-prod` certificate
12421242+ issuer. It's specifically configured to use ingress-nginx (this is probably
12431243+ not required if your cluster has only one ingress defined, but it's better to
12441244+ be overly verbose) and matches the HTTP hostname `kuard.xeserv.us`. It points
12451245+ to the service `kuard`'s named `http` port (whatever that is). The TLS block
12461246+ tells ingress-nginx (and cert-manager) to expect a cert in the secret
12471247+ `kuard-tls` for the domain name `kuard.xeserv.us`.
12481248+</Conv>
12491249+12501250+When I create this Ingress with the cluster-issuer annotation, it's discovered by both external-dns and cert-manager. external-dns creates DNS records in Route 53 (and tracks them using DynamoDB). At the same time, cert-manager creates a Cert resource for the domains I specified in the in the `spec.tls.hosts` field of my Ingress. The Cert resource discoveres that the secret `kuard-tls` has no valid certificate in it, so it creates an Order for a new certificate. The Order creates a Challenge by poking Let's Encrypt to get the required token and then configures its own Ingress to handle the HTTP-01 strategy.
12511251+12521252+Once it's able to verify that it can pass the Challenge locally (this requires external-dns to finish pushing DNS routes and for DNS to globally converge, but usually happens within a minute), it asks Let's Encrypt to test it. Once Let's Encrypt passes the test, it signs my certificate, the Challenge Ingress is deleted, the signed certificate is saved to the right Kubernetes secret, the Order is marked as fulfilled, the Cert is marked ready, and nginx is reloaded to point to that newly minted certificate.
12531253+12541254+<Conv name="Aoi" mood="wut">
12551255+I guess that does make sense when you spell it all out, but that is a _lot_ of boilerplate and interplay to do something that [autocert](https://pkg.go.dev/golang.org/x/crypto/acme/autocert) does for you for free.
12561256+</Conv>
12571257+<Conv name="Cadey" mood="aha">
12581258+The way you should interpret this is that each of the Kubernetes components are _stateless_ as much as possible. All of the state is stored externally in Kubernetes objects. This means that any of the components involved can restart, get moved around between nodes, or crash without affecting any in-flight tasks. I've been understanding this as having all of the inherent complexity of what is going on laid bare in front of you, much like how Rust makes it very obvious what is going on at a syntax level.
12591259+12601260+You're probably used to a lot of this being handwaved away by the platforms you use, but this all isn't really that hard. It's just a verbose presentation of it. Besides, most of this is describing how I'm handwaving all of this away for my own uses.
12611261+12621262+</Conv>
12631263+<Conv name="Aoi" mood="cheer">
12641264+I get it, you're basically making your homelab into your own platform, right? This means that you need to provide all those building blocks for yourself. I guess this explains why there's so many moving parts.
12651265+</Conv>
12661266+12671267+## Shipping the lab
12681268+12691269+At this point, my lab is stable, useful, and ready for me to put jobs on it. I have:
12701270+12711271+- A cluster of four machines running Talos Linux that I can submit jobs to with `kubectl`
12721272+- Persistent storage with Longhorn
12731273+- Backups of said persistent storage to [Tigris](https://www.tigrisdata.com/)
12741274+- Ingress from the public internet with `ingressd` and crimes
12751275+- DNS records managed by `external-dns`
12761276+- HTTPS certificates managed by `cert-manager`
12771277+12781278+And this all adds up to a place where I can just throw jobs at and get the confidence that they will run. I'm going to be using this to run a bunch of other things that have previously been spread across a bunch of VPSes that I don't want to pay for anymore. Even though they are an excellent tax break right now.
12791279+12801280+<Conv name="Cadey" mood="enby">
12811281+ I guess I did end up using Rocky Linux afterall because the ingressd node runs
12821282+ it. It's a bone-stock image with automatic updates and a single RPM built by
12831283+ `yeet`. Realistically I could probably get away with running a few ingressd
12841284+ nodes, but I'm lazy and I don't want to have to manage more than one. High
12851285+ availability is for production, not janky homelab setups.
12861286+</Conv>
12871287+12881288+## The parts of a manifest for my homelab
12891289+12901290+When I deploy things to my homelab cluster, I can divide them into three basic classes:
12911291+12921292+- Automation/bots that don't expose any API or web endpoints
12931293+- Internal services that do expose API or web endpoints
12941294+- Public-facing services that should be exposed to the public internet
12951295+12961296+Automation/bots are the easiest. In the ideal case all I need is a Deployment and a Secret to hold the API keys to poke external services. For an example of that, see [within.website/x/cmd/sapientwindex](https://github.com/Xe/x/tree/master/cmd/sapientwindex).
12971297+12981298+Internal services get a little bit more complicated. Depending on the service in question, it'll probably get:
12991299+13001300+- A Namespace to hold everything
13011301+- A PersistentVolumeClaim for holding state (SQLite, JSONMutexDB, etc.)
13021302+- A Secret or two for the various private/API keys involved in the process
13031303+- A Deployment with one or more containers that actually runs that internal service's code
13041304+- A Service that exposes the internal ports to the cluster on well-known port numbers
13051305+13061306+For most internal services, this is more than good enough. If I need to poke it, I can do so by connecting to `svcname.ns.svc.alrest.xeserv.us`. It'd work great for a Minecraft server or something else like that.
13071307+13081308+However, I also do need to expose things to the public internet sometimes. When I need to do that, I define an Ingress that has the right domain name so the rest of the stack will just automatically make it work.
13091309+13101310+This gives me the ability to just push things to the homelab without fear. Once new jobs get defined, the rest of the stack will converge, order certificates, and otherwise make things Just Work™️. It's kinda glorious now that it's all set up.
13111311+13121312+## What's next?
13131313+13141314+Here are the things I want to play with next:
13151315+13161316+- [KubeVirt](https://kubevirt.io/): I want to run some VMs on my cluster. This looks like it could be the basis for an even better [waifud](/blog/series/waifud/) setup. All it's missing is a decent admin UI, which I can probably make with Tailwind and HTMX. The biggest thing I want to play with is live migration of VMs between nodes.
13171317+- I want to get a Minecraft server running on my cluster and figure out some way to make it accessible to my patrons. I have no idea how I'll go about doing the latter, but I'm sure I'll figure it out. Worst case I think one of you nerds in the (patron-only) Discord has _some_ ideas.
13181318+- I want to resurrect [kubermemes](https://tulpa.dev/cadey/kubermemes) for generating my app deployments. I've had a lot of opinions change since I wrote that so many years ago, but overall the shape of my deployments is going to be "small file with some resource requests" that compiles into "YAML means 'Yeah, A Massive List of stuff'".
13191319+- I may also want to get AI stuff running on Talos Linux. I have two GPUs in that cluster and kinda want to have stable diffusion at home again. It'll be a good way to get back into the swing of things with AI stuff.
13201320+- ??? who knows what else will come up through my binges into weird GitHub projects and Hacker News.
13211321+13221322+I'm willing to declare my homelab a success at this point. It's cool that I can spread the load between my machines so much more cleanly now. I'm excited to see what I can do with this, and I hope that you are excited that you get more blog posts out of it.
13231323+13241324+<Conv name="Aoi" mood="coffee">
13251325+ What a way to kill a week of PTO, eh?
13261326+</Conv>
+2-2
lume/src/notes/2024/homelab-v2/05.mdx
···29293030- Get it hooked up to Ethernet and power
3131- Boot it off of the Talos Linux USB stick
3232-- Apply the config with `talosctl`
3333-- Wait for it to reboot and everything to green up
3232+- Apply the config with `talosctl` from my macbook
3333+- Wait for it to reboot and everything to green up in `kubectl`
34343535That's it. This is what every homelab OS should strive to be.
3636