summaryrefslogtreecommitdiff
path: root/gemfeed/atom.xml
diff options
context:
space:
mode:
Diffstat (limited to 'gemfeed/atom.xml')
-rw-r--r--gemfeed/atom.xml1470
1 files changed, 1102 insertions, 368 deletions
diff --git a/gemfeed/atom.xml b/gemfeed/atom.xml
index f90a0498..36f04d8e 100644
--- a/gemfeed/atom.xml
+++ b/gemfeed/atom.xml
@@ -1,12 +1,1091 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
- <updated>2025-09-29T09:38:00+03:00</updated>
+ <updated>2025-10-02T11:27:20+03:00</updated>
<title>foo.zone feed</title>
<subtitle>To be in the .zone!</subtitle>
<link href="https://foo.zone/gemfeed/atom.xml" rel="self" />
<link href="https://foo.zone/" />
<id>https://foo.zone/</id>
<entry>
+ <title>f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments</title>
+ <link href="https://foo.zone/gemfeed/2025-10-02-f3s-kubernetes-with-freebsd-part-7.html" />
+ <id>https://foo.zone/gemfeed/2025-10-02-f3s-kubernetes-with-freebsd-part-7.html</id>
+ <updated>2025-10-02T11:27:19+03:00</updated>
+ <author>
+ <name>Paul Buetow aka snonux</name>
+ <email>paul@dev.buetow.org</email>
+ </author>
+ <summary>This is the seventh blog post about the f3s series for my self-hosting demands in a home lab. f3s? The 'f' stands for FreeBSD, and the '3s' stands for k3s, the Kubernetes distribution I use on FreeBSD-based physical machines.</summary>
+ <content type="xhtml">
+ <div xmlns="http://www.w3.org/1999/xhtml">
+ <h1 style='display: inline' id='f3s-kubernetes-with-freebsd---part-7-k3s-and-first-pod-deployments'>f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments</h1><br />
+<br />
+<span>This is the seventh blog post about the f3s series for my self-hosting demands in a home lab. f3s? The "f" stands for FreeBSD, and the "3s" stands for k3s, the Kubernetes distribution I use on FreeBSD-based physical machines.</span><br />
+<br />
+<a class='textlink' href='./2024-11-17-f3s-kubernetes-with-freebsd-part-1.html'>2024-11-17 f3s: Kubernetes with FreeBSD - Part 1: Setting the stage</a><br />
+<a class='textlink' href='./2024-12-03-f3s-kubernetes-with-freebsd-part-2.html'>2024-12-03 f3s: Kubernetes with FreeBSD - Part 2: Hardware and base installation</a><br />
+<a class='textlink' href='./2025-02-01-f3s-kubernetes-with-freebsd-part-3.html'>2025-02-01 f3s: Kubernetes with FreeBSD - Part 3: Protecting from power cuts</a><br />
+<a class='textlink' href='./2025-04-05-f3s-kubernetes-with-freebsd-part-4.html'>2025-04-05 f3s: Kubernetes with FreeBSD - Part 4: Rocky Linux Bhyve VMs</a><br />
+<a class='textlink' href='./2025-05-11-f3s-kubernetes-with-freebsd-part-5.html'>2025-05-11 f3s: Kubernetes with FreeBSD - Part 5: WireGuard mesh network</a><br />
+<a class='textlink' href='./2025-07-14-f3s-kubernetes-with-freebsd-part-6.html'>2025-07-14 f3s: Kubernetes with FreeBSD - Part 6: Storage</a><br />
+<a class='textlink' href='./2025-10-02-f3s-kubernetes-with-freebsd-part-7.html'>2025-10-02 f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments (You are currently reading this)</a><br />
+<br />
+<a href='./f3s-kubernetes-with-freebsd-part-1/f3slogo.png'><img alt='f3s logo' title='f3s logo' src='./f3s-kubernetes-with-freebsd-part-1/f3slogo.png' /></a><br />
+<br />
+<h2 style='display: inline' id='table-of-contents'>Table of Contents</h2><br />
+<br />
+<ul>
+<li><a href='#f3s-kubernetes-with-freebsd---part-7-k3s-and-first-pod-deployments'>f3s: Kubernetes with FreeBSD - Part 7: k3s and first pod deployments</a></li>
+<li>⇢ <a href='#introduction'>Introduction</a></li>
+<li>⇢ <a href='#updating'>Updating</a></li>
+<li>⇢ <a href='#installing-k3s'>Installing k3s</a></li>
+<li>⇢ ⇢ <a href='#generating-k3stoken-and-starting-the-first-k3s-node'>Generating <span class='inlinecode'>K3S_TOKEN</span> and starting the first k3s node</a></li>
+<li>⇢ ⇢ <a href='#adding-the-remaining-nodes-to-the-cluster'>Adding the remaining nodes to the cluster</a></li>
+<li>⇢ <a href='#test-deployments'>Test deployments</a></li>
+<li>⇢ ⇢ <a href='#test-deployment-to-kubernetes'>Test deployment to Kubernetes</a></li>
+<li>⇢ ⇢ <a href='#test-deployment-with-persistent-volume-claim'>Test deployment with persistent volume claim</a></li>
+<li>⇢ ⇢ <a href='#scaling-traefik-for-faster-failover'>Scaling Traefik for faster failover</a></li>
+<li>⇢ <a href='#make-it-accessible-from-the-public-internet'>Make it accessible from the public internet</a></li>
+<li>⇢ ⇢ <a href='#openbsd-relayd-configuration'>OpenBSD relayd configuration</a></li>
+<li>⇢ <a href='#deploying-the-private-docker-image-registry'>Deploying the private Docker image registry</a></li>
+<li>⇢ ⇢ <a href='#prepare-the-nfs-backed-storage'>Prepare the NFS-backed storage</a></li>
+<li>⇢ ⇢ <a href='#install-or-upgrade-the-chart'>Install (or upgrade) the chart</a></li>
+<li>⇢ ⇢ <a href='#allow-nodes-and-workstations-to-trust-the-registry'>Allow nodes and workstations to trust the registry</a></li>
+<li>⇢ ⇢ <a href='#pushing-and-pulling-images'>Pushing and pulling images</a></li>
+<li>⇢ <a href='#example-anki-sync-server-from-the-private-registry'>Example: Anki Sync Server from the private registry</a></li>
+<li>⇢ ⇢ <a href='#build-and-push-the-image'>Build and push the image</a></li>
+<li>⇢ ⇢ <a href='#create-the-anki-secret-and-storage-on-the-cluster'>Create the Anki secret and storage on the cluster</a></li>
+<li>⇢ ⇢ <a href='#deploy-the-chart'>Deploy the chart</a></li>
+<li>⇢ <a href='#nfsv4-uid-mapping-for-postgres-backed-and-other-apps'>NFSv4 UID mapping for Postgres-backed (and other) apps</a></li>
+<li>⇢ ⇢ <a href='#helm-charts-currently-in-service'>Helm charts currently in service</a></li>
+</ul><br />
+<h2 style='display: inline' id='introduction'>Introduction</h2><br />
+<br />
+<span>In this blog post, I am finally going to install k3s (the Kubernetes distribution I use) to the whole setup and deploy the first workloads (helm charts, and a private registry) to it.</span><br />
+<br />
+<a class='textlink' href='https://k3s.io'>https://k3s.io</a><br />
+<br />
+<h2 style='display: inline' id='updating'>Updating</h2><br />
+<br />
+<span>Before proceeding, I bring all systems involved up-to-date. On all three Rocky Linux 9 boxes <span class='inlinecode'>r0</span>, <span class='inlinecode'>r1</span>, and <span class='inlinecode'>r2</span>:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>dnf update -y
+reboot
+</pre>
+<br />
+<span>On the FreeBSD hosts, I upgraded from FreeBSD 14.2 to 14.3-RELEASE, running this on all three hosts <span class='inlinecode'>f0</span>, <span class='inlinecode'>f1</span> and <span class='inlinecode'>f2</span>:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>paul@f0:~ % doas freebsd-update fetch
+paul@f0:~ % doas freebsd-update install
+paul@f0:~ % doas reboot
+.
+.
+.
+paul@f0:~ % doas freebsd-update -r <font color="#000000">14.3</font>-RELEASE upgrade
+paul@f0:~ % doas freebsd-update install
+paul@f0:~ % doas freebsd-update install
+paul@f0:~ % doas reboot
+.
+.
+.
+paul@f0:~ % doas freebsd-update install
+paul@f0:~ % doas pkg update
+paul@f0:~ % doas pkg upgrade
+paul@f0:~ % doas reboot
+.
+.
+.
+paul@f0:~ % uname -a
+FreeBSD f0.lan.buetow.org <font color="#000000">14.3</font>-RELEASE FreeBSD <font color="#000000">14.3</font>-RELEASE
+ releng/<font color="#000000">14.3</font>-n<font color="#000000">271432</font>-8c9ce319fef7 GENERIC amd64
+</pre>
+<br />
+<h2 style='display: inline' id='installing-k3s'>Installing k3s</h2><br />
+<br />
+<h3 style='display: inline' id='generating-k3stoken-and-starting-the-first-k3s-node'>Generating <span class='inlinecode'>K3S_TOKEN</span> and starting the first k3s node</h3><br />
+<br />
+<span>I generated the k3s token on my Fedora laptop with <span class='inlinecode'>pwgen -n 32</span> and selected one of the results. Then, on all three <span class='inlinecode'>r</span> hosts, I ran the following (replace SECRET_TOKEN with the actual secret):</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>[root@r0 ~]<i><font color="silver"># echo -n SECRET_TOKEN &gt; ~/.k3s_token</font></i>
+</pre>
+<br />
+<span>The following steps are also documented on the k3s website:</span><br />
+<br />
+<a class='textlink' href='https://docs.k3s.io/datastore/ha-embedded'>https://docs.k3s.io/datastore/ha-embedded</a><br />
+<br />
+<span>To bootstrap k3s on the first node, I ran this on <span class='inlinecode'>r0</span>:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>[root@r0 ~]<i><font color="silver"># curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat ~/.k3s_token) \</font></i>
+ sh -s - server --cluster-init --tls-san=r0.wg0.wan.buetow.org
+[INFO] Finding release <b><u><font color="#000000">for</font></u></b> channel stable
+[INFO] Using v1.<font color="#000000">32.6</font>+k3s1 as release
+.
+.
+.
+[INFO] systemd: Starting k3s
+</pre>
+<br />
+<h3 style='display: inline' id='adding-the-remaining-nodes-to-the-cluster'>Adding the remaining nodes to the cluster</h3><br />
+<br />
+<span>Then I ran on the other two nodes <span class='inlinecode'>r1</span> and <span class='inlinecode'>r2</span>:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>[root@r1 ~]<i><font color="silver"># curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat ~/.k3s_token) \</font></i>
+ sh -s - server --server https://r<font color="#000000">0</font>.wg0.wan.buetow.org:<font color="#000000">6443</font> \
+ --tls-san=r1.wg0.wan.buetow.org
+
+[root@r2 ~]<i><font color="silver"># curl -sfL https://get.k3s.io | K3S_TOKEN=$(cat ~/.k3s_token) \</font></i>
+ sh -s - server --server https://r<font color="#000000">0</font>.wg0.wan.buetow.org:<font color="#000000">6443</font> \
+ --tls-san=r2.wg0.wan.buetow.org
+.
+.
+.
+
+</pre>
+<br />
+<span>Once done, I had a three-node Kubernetes cluster control plane:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>[root@r0 ~]<i><font color="silver"># kubectl get nodes</font></i>
+NAME STATUS ROLES AGE VERSION
+r0.lan.buetow.org Ready control-plane,etcd,master 4m44s v1.<font color="#000000">32.6</font>+k3s1
+r1.lan.buetow.org Ready control-plane,etcd,master 3m13s v1.<font color="#000000">32.6</font>+k3s1
+r2.lan.buetow.org Ready control-plane,etcd,master 30s v1.<font color="#000000">32.6</font>+k3s1
+
+[root@r0 ~]<i><font color="silver"># kubectl get pods --all-namespaces</font></i>
+NAMESPACE NAME READY STATUS RESTARTS AGE
+kube-system coredns-5688667fd4-fs2jj <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 5m27s
+kube-system helm-install-traefik-crd-f9hgd <font color="#000000">0</font>/<font color="#000000">1</font> Completed <font color="#000000">0</font> 5m27s
+kube-system helm-install-traefik-zqqqk <font color="#000000">0</font>/<font color="#000000">1</font> Completed <font color="#000000">2</font> 5m27s
+kube-system local-path-provisioner-774c6665dc-jqlnc <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 5m27s
+kube-system metrics-server-6f4c6675d5-5xpmp <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 5m27s
+kube-system svclb-traefik-411cec5b-cdp2l <font color="#000000">2</font>/<font color="#000000">2</font> Running <font color="#000000">0</font> 78s
+kube-system svclb-traefik-411cec5b-f625r <font color="#000000">2</font>/<font color="#000000">2</font> Running <font color="#000000">0</font> 4m58s
+kube-system svclb-traefik-411cec5b-twrd<font color="#000000">7</font> <font color="#000000">2</font>/<font color="#000000">2</font> Running <font color="#000000">0</font> 4m2s
+kube-system traefik-c98fdf6fb-lt6fx <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 4m58s
+</pre>
+<br />
+<span>In order to connect with <span class='inlinecode'>kubectl</span> from my Fedora laptop, I had to copy <span class='inlinecode'>/etc/rancher/k3s/k3s.yaml</span> from <span class='inlinecode'>r0</span> to <span class='inlinecode'>~/.kube/config</span> and then replace the value of the server field with <span class='inlinecode'>r0.lan.buetow.org</span>. kubectl can now manage the cluster. Note that this step has to be repeated when I want to connect to another node of the cluster (e.g. when <span class='inlinecode'>r0</span> is down).</span><br />
+<br />
+<h2 style='display: inline' id='test-deployments'>Test deployments</h2><br />
+<br />
+<h3 style='display: inline' id='test-deployment-to-kubernetes'>Test deployment to Kubernetes</h3><br />
+<br />
+<span>Let&#39;s create a test namespace:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl create namespace <b><u><font color="#000000">test</font></u></b>
+namespace/test created
+
+&gt; ~ kubectl get namespaces
+NAME STATUS AGE
+default Active 6h11m
+kube-node-lease Active 6h11m
+kube-public Active 6h11m
+kube-system Active 6h11m
+<b><u><font color="#000000">test</font></u></b> Active 5s
+
+&gt; ~ kubectl config set-context --current --namespace=<b><u><font color="#000000">test</font></u></b>
+Context <font color="#808080">"default"</font> modified.
+</pre>
+<br />
+<span>And let&#39;s also create an Apache test pod:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ cat &lt;&lt;END &gt; apache-deployment.yaml
+<i><font color="silver"># Apache HTTP Server Deployment</font></i>
+apiVersion: apps/v<font color="#000000">1</font>
+kind: Deployment
+metadata:
+ name: apache-deployment
+spec:
+ replicas: <font color="#000000">1</font>
+ selector:
+ matchLabels:
+ app: apache
+ template:
+ metadata:
+ labels:
+ app: apache
+ spec:
+ containers:
+ - name: apache
+ image: httpd:latest
+ ports:
+ <i><font color="silver"># Container port where Apache listens</font></i>
+ - containerPort: <font color="#000000">80</font>
+END
+
+&gt; ~ kubectl apply -f apache-deployment.yaml
+deployment.apps/apache-deployment created
+
+&gt; ~ kubectl get all
+NAME READY STATUS RESTARTS AGE
+pod/apache-deployment-5fd955856f-4pjmf <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">0</font> 7s
+
+NAME READY UP-TO-DATE AVAILABLE AGE
+deployment.apps/apache-deployment <font color="#000000">1</font>/<font color="#000000">1</font> <font color="#000000">1</font> <font color="#000000">1</font> 7s
+
+NAME DESIRED CURRENT READY AGE
+replicaset.apps/apache-deployment-5fd955856f <font color="#000000">1</font> <font color="#000000">1</font> <font color="#000000">1</font> 7s
+</pre>
+<br />
+<span>Let&#39;s also create a service: </span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ cat &lt;&lt;END &gt; apache-service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: apache
+ name: apache-service
+spec:
+ ports:
+ - name: web
+ port: <font color="#000000">80</font>
+ protocol: TCP
+ <i><font color="silver"># Expose port 80 on the service</font></i>
+ targetPort: <font color="#000000">80</font>
+ selector:
+ <i><font color="silver"># Link this service to pods with the label app=apache</font></i>
+ app: apache
+END
+
+&gt; ~ kubectl apply -f apache-service.yaml
+service/apache-service created
+
+&gt; ~ kubectl get service
+NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
+apache-service ClusterIP <font color="#000000">10.43</font>.<font color="#000000">249.165</font> &lt;none&gt; <font color="#000000">80</font>/TCP 4s
+</pre>
+<br />
+<span>Now let&#39;s create an ingress:</span><br />
+<br />
+<span class='quote'>Note: I&#39;ve modified the hosts listed in this example after I published this blog post to ensure that there aren&#39;t any bots scraping it.</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ cat &lt;&lt;END &gt; apache-ingress.yaml
+
+apiVersion: networking.k8s.io/v<font color="#000000">1</font>
+kind: Ingress
+metadata:
+ name: apache-ingress
+ namespace: <b><u><font color="#000000">test</font></u></b>
+ annotations:
+ spec.ingressClassName: traefik
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ rules:
+ - host: f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+ - host: standby.f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+ - host: www.f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+END
+
+&gt; ~ kubectl apply -f apache-ingress.yaml
+ingress.networking.k8s.io/apache-ingress created
+
+&gt; ~ kubectl describe ingress
+Name: apache-ingress
+Labels: &lt;none&gt;
+Namespace: <b><u><font color="#000000">test</font></u></b>
+Address: <font color="#000000">192.168</font>.<font color="#000000">1.120</font>,<font color="#000000">192.168</font>.<font color="#000000">1.121</font>,<font color="#000000">192.168</font>.<font color="#000000">1.122</font>
+Ingress Class: traefik
+Default backend: &lt;default&gt;
+Rules:
+ Host Path Backends
+ ---- ---- --------
+ f3s.foo.zone
+ / apache-service:<font color="#000000">80</font> (<font color="#000000">10.42</font>.<font color="#000000">1.11</font>:<font color="#000000">80</font>)
+ standby.f3s.foo.zone
+ / apache-service:<font color="#000000">80</font> (<font color="#000000">10.42</font>.<font color="#000000">1.11</font>:<font color="#000000">80</font>)
+ www.f3s.foo.zone
+ / apache-service:<font color="#000000">80</font> (<font color="#000000">10.42</font>.<font color="#000000">1.11</font>:<font color="#000000">80</font>)
+Annotations: spec.ingressClassName: traefik
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+Events: &lt;none&gt;
+</pre>
+<br />
+<span>Notes: </span><br />
+<br />
+<ul>
+<li>In the ingress, I use plain HTTP (web) for the Traefik rule, as all the "production" traffic will be routed through a WireGuard tunnel anyway, as I will show later.</li>
+</ul><br />
+<span>So I tested the Apache web server through the ingress rule:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ curl -H <font color="#808080">"Host: www.f3s.foo.zone"</font> http://r<font color="#000000">0</font>.lan.buetow.org:<font color="#000000">80</font>
+&lt;html&gt;&lt;body&gt;&lt;h1&gt;It works!&lt;/h<font color="#000000">1</font>&gt;&lt;/body&gt;&lt;/html&gt;
+</pre>
+<br />
+<h3 style='display: inline' id='test-deployment-with-persistent-volume-claim'>Test deployment with persistent volume claim</h3><br />
+<br />
+<span>Next, I modified the Apache example to serve the <span class='inlinecode'>htdocs</span> directory from the NFS share I created in the previous blog post. I used the following manifests. Most of them are the same as before, except for the persistent volume claim and the volume mount in the Apache deployment.</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ cat &lt;&lt;END &gt; apache-deployment.yaml
+<i><font color="silver"># Apache HTTP Server Deployment</font></i>
+apiVersion: apps/v<font color="#000000">1</font>
+kind: Deployment
+metadata:
+ name: apache-deployment
+ namespace: <b><u><font color="#000000">test</font></u></b>
+spec:
+ replicas: <font color="#000000">2</font>
+ selector:
+ matchLabels:
+ app: apache
+ template:
+ metadata:
+ labels:
+ app: apache
+ spec:
+ containers:
+ - name: apache
+ image: httpd:latest
+ ports:
+ <i><font color="silver"># Container port where Apache listens</font></i>
+ - containerPort: <font color="#000000">80</font>
+ readinessProbe:
+ httpGet:
+ path: /
+ port: <font color="#000000">80</font>
+ initialDelaySeconds: <font color="#000000">5</font>
+ periodSeconds: <font color="#000000">10</font>
+ livenessProbe:
+ httpGet:
+ path: /
+ port: <font color="#000000">80</font>
+ initialDelaySeconds: <font color="#000000">15</font>
+ periodSeconds: <font color="#000000">10</font>
+ volumeMounts:
+ - name: apache-htdocs
+ mountPath: /usr/local/apache<font color="#000000">2</font>/htdocs/
+ volumes:
+ - name: apache-htdocs
+ persistentVolumeClaim:
+ claimName: example-apache-pvc
+END
+
+&gt; ~ cat &lt;&lt;END &gt; apache-ingress.yaml
+apiVersion: networking.k8s.io/v<font color="#000000">1</font>
+kind: Ingress
+metadata:
+ name: apache-ingress
+ namespace: <b><u><font color="#000000">test</font></u></b>
+ annotations:
+ spec.ingressClassName: traefik
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+spec:
+ rules:
+ - host: f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+ - host: standby.f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+ - host: www.f3s.foo.zone
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: apache-service
+ port:
+ number: <font color="#000000">80</font>
+END
+
+&gt; ~ cat &lt;&lt;END &gt; apache-persistent-volume.yaml
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: example-apache-pv
+spec:
+ capacity:
+ storage: 1Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/example-apache-volume-claim
+ <b><u><font color="#000000">type</font></u></b>: Directory
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: example-apache-pvc
+ namespace: <b><u><font color="#000000">test</font></u></b>
+spec:
+ storageClassName: <font color="#808080">""</font>
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+END
+
+&gt; ~ cat &lt;&lt;END &gt; apache-service.yaml
+apiVersion: v1
+kind: Service
+metadata:
+ labels:
+ app: apache
+ name: apache-service
+ namespace: <b><u><font color="#000000">test</font></u></b>
+spec:
+ ports:
+ - name: web
+ port: <font color="#000000">80</font>
+ protocol: TCP
+ <i><font color="silver"># Expose port 80 on the service</font></i>
+ targetPort: <font color="#000000">80</font>
+ selector:
+ <i><font color="silver"># Link this service to pods with the label app=apache</font></i>
+ app: apache
+END
+</pre>
+<br />
+<span>I applied the manifests:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl apply -f apache-persistent-volume.yaml
+&gt; ~ kubectl apply -f apache-service.yaml
+&gt; ~ kubectl apply -f apache-deployment.yaml
+&gt; ~ kubectl apply -f apache-ingress.yaml
+</pre>
+<br />
+<span>Looking at the deployment, I could see it failed because the directory didn&#39;t exist yet on the NFS share (note that I also increased the replica count to 2 so if one node goes down there&#39;s already a replica running on another node for faster failover):</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl get pods
+NAME READY STATUS RESTARTS AGE
+apache-deployment-5b96bd6b6b-fv2jx <font color="#000000">0</font>/<font color="#000000">1</font> ContainerCreating <font color="#000000">0</font> 9m15s
+apache-deployment-5b96bd6b6b-ax2ji <font color="#000000">0</font>/<font color="#000000">1</font> ContainerCreating <font color="#000000">0</font> 9m15s
+
+&gt; ~ kubectl describe pod apache-deployment-5b96bd6b6b-fv2jx | tail -n <font color="#000000">5</font>
+Events:
+ Type Reason Age From Message
+ ---- ------ ---- ---- -------
+ Normal Scheduled 9m34s default-scheduler Successfully
+ assigned test/apache-deployment-5b96bd6b6b-fv2jx to r2.lan.buetow.org
+ Warning FailedMount 80s (x12 over 9m34s) kubelet MountVolume.SetUp
+ failed <b><u><font color="#000000">for</font></u></b> volume <font color="#808080">"example-apache-pv"</font> : hostPath <b><u><font color="#000000">type</font></u></b> check failed:
+ /data/nfs/k3svolumes/example-apache is not a directory
+</pre>
+<br />
+<span>That&#39;s intentional—I needed to create the directory on the NFS share first, so I did that (e.g. on <span class='inlinecode'>r0</span>):</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>[root@r0 ~]<i><font color="silver"># mkdir /data/nfs/k3svolumes/example-apache-volume-claim/</font></i>
+
+[root@r0 ~]<i><font color="silver"># cat &lt;&lt;END &gt; /data/nfs/k3svolumes/example-apache-volume-claim/index.html</font></i>
+&lt;!DOCTYPE html&gt;
+&lt;html&gt;
+&lt;head&gt;
+ &lt;title&gt;Hello, it works&lt;/title&gt;
+&lt;/head&gt;
+&lt;body&gt;
+ &lt;h1&gt;Hello, it works!&lt;/h<font color="#000000">1</font>&gt;
+ &lt;p&gt;This site is served via a PVC!&lt;/p&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+END
+</pre>
+<br />
+<span>The <span class='inlinecode'>index.html</span> file gives us some actual content to serve. After deleting the pod, it recreates itself and the volume mounts correctly:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl delete pod apache-deployment-5b96bd6b6b-fv2jx
+
+&gt; ~ curl -H <font color="#808080">"Host: www.f3s.foo.zone"</font> http://r<font color="#000000">0</font>.lan.buetow.org:<font color="#000000">80</font>
+&lt;!DOCTYPE html&gt;
+&lt;html&gt;
+&lt;head&gt;
+ &lt;title&gt;Hello, it works&lt;/title&gt;
+&lt;/head&gt;
+&lt;body&gt;
+ &lt;h1&gt;Hello, it works!&lt;/h<font color="#000000">1</font>&gt;
+ &lt;p&gt;This site is served via a PVC!&lt;/p&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</pre>
+<br />
+<h3 style='display: inline' id='scaling-traefik-for-faster-failover'>Scaling Traefik for faster failover</h3><br />
+<br />
+<span>Traefik (used for ingress on k3s) ships with a single replica by default, but for faster failover I bumped it to two replicas so each worker node runs one pod. That way, if a node disappears, the service stays up while Kubernetes schedules a replacement. Here&#39;s the command I used:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl -n kube-system scale deployment traefik --replicas=<font color="#000000">2</font>
+</pre>
+<br />
+<span>And the result:</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ kubectl -n kube-system get pods -l app.kubernetes.io/name=traefik
+kube-system traefik-c98fdf6fb-97kqk <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">19</font> (53d ago) 64d
+kube-system traefik-c98fdf6fb-9npg2 <font color="#000000">1</font>/<font color="#000000">1</font> Running <font color="#000000">11</font> (53d ago) 61d
+</pre>
+<br />
+<h2 style='display: inline' id='make-it-accessible-from-the-public-internet'>Make it accessible from the public internet</h2><br />
+<br />
+<span>Next, I made this accessible through the public internet via the <span class='inlinecode'>www.f3s.foo.zone</span> hosts. As a reminder from part 1 of this series, I reviewed the section titled "OpenBSD/relayd to the rescue for external connectivity":</span><br />
+<br />
+<a class='textlink' href='./2024-11-17-f3s-kubernetes-with-freebsd-part-1.html'>f3s: Kubernetes with FreeBSD - Part 1: Setting the stage</a><br />
+<br />
+<span class='quote'>All apps should be reachable through the internet (e.g., from my phone or computer when travelling). For external connectivity and TLS management, I&#39;ve got two OpenBSD VMs (one hosted by OpenBSD Amsterdam and another hosted by Hetzner) handling public-facing services like DNS, relaying traffic, and automating Let&#39;s Encrypt certificates.</span><br />
+<br />
+<span class='quote'>All of this (every Linux VM to every OpenBSD box) will be connected via WireGuard tunnels, keeping everything private and secure. There will be 6 WireGuard tunnels (3 k3s nodes times two OpenBSD VMs).</span><br />
+<br />
+<span class='quote'>So, when I want to access a service running in k3s, I will hit an external DNS endpoint (with the authoritative DNS servers being the OpenBSD boxes). The DNS will resolve to the master OpenBSD VM (see my KISS highly-available with OpenBSD blog post), and from there, the relayd process (with a Let&#39;s Encrypt certificate—see my Let&#39;s Encrypt with OpenBSD and Rex blog post) will accept the TCP connection and forward it through the WireGuard tunnel to a reachable node port of one of the k3s nodes, thus serving the traffic.</span><br />
+<br />
+<!-- Generator: GNU source-highlight 3.1.9
+by Lorenzo Bettini
+http://www.lorenzobettini.it
+http://www.gnu.org/software/src-highlite -->
+<pre>&gt; ~ curl https://f3s.foo.zone
+&lt;html&gt;&lt;body&gt;&lt;h1&gt;It works!&lt;/h<font color="#000000">1</font>&gt;&lt;/body&gt;&lt;/html&gt;
+
+&gt; ~ curl https://www.f3s.foo.zone
+&lt;html&gt;&lt;body&gt;&lt;h1&gt;It works!&lt;/h<font color="#000000">1</font>&gt;&lt;/body&gt;&lt;/html&gt;
+
+&gt; ~ curl https://standby.f3s.foo.zone
+&lt;html&gt;&lt;body&gt;&lt;h1&gt;It works!&lt;/h<font color="#000000">1</font>&gt;&lt;/body&gt;&lt;/html&gt;
+</pre>
+<br />
+<span>This is how it works in <span class='inlinecode'>relayd.conf</span> on OpenBSD:</span><br />
+<br />
+<h3 style='display: inline' id='openbsd-relayd-configuration'>OpenBSD relayd configuration</h3><br />
+<br />
+<span>The OpenBSD edge relays keep the Kubernetes-facing addresses for the f3s ingress endpoints in a shared backend table so TLS traffic for every <span class='inlinecode'>f3s</span> hostname lands on the same pool of k3s nodes (pointing to the WireGuard IP addresses of those nodes - remember, they are running locally in my LAN, wheras the OpenBSD edge relays operate in the public internet):</span><br />
+<br />
+<pre>
+table &lt;f3s&gt; {
+ 192.168.2.120
+ 192.168.2.121
+ 192.168.2.122
+}
+</pre>
+<br />
+<span>Inside the <span class='inlinecode'>http protocol "https"</span> block each public hostname gets its Let&#39;s Encrypt certificate and is matched to that backend table. Besides the primary trio, every service-specific hostname (<span class='inlinecode'>anki</span>, <span class='inlinecode'>bag</span>, <span class='inlinecode'>flux</span>, <span class='inlinecode'>audiobookshelf</span>, <span class='inlinecode'>gpodder</span>, <span class='inlinecode'>radicale</span>, <span class='inlinecode'>vault</span>, <span class='inlinecode'>syncthing</span>, <span class='inlinecode'>uprecords</span>) and their <span class='inlinecode'>www</span> / <span class='inlinecode'>standby</span> aliases reuse the same pool so new apps can go live just by publishing an ingress rule, whereas they will all map to a service running in k3s:</span><br />
+<br />
+<pre>
+http protocol "https" {
+ tls keypair f3s.foo.zone
+ tls keypair www.f3s.foo.zone
+ tls keypair standby.f3s.foo.zone
+ tls keypair anki.f3s.foo.zone
+ tls keypair www.anki.f3s.foo.zone
+ tls keypair standby.anki.f3s.foo.zone
+ tls keypair bag.f3s.foo.zone
+ tls keypair www.bag.f3s.foo.zone
+ tls keypair standby.bag.f3s.foo.zone
+ tls keypair flux.f3s.foo.zone
+ tls keypair www.flux.f3s.foo.zone
+ tls keypair standby.flux.f3s.foo.zone
+ tls keypair audiobookshelf.f3s.foo.zone
+ tls keypair www.audiobookshelf.f3s.foo.zone
+ tls keypair standby.audiobookshelf.f3s.foo.zone
+ tls keypair gpodder.f3s.foo.zone
+ tls keypair www.gpodder.f3s.foo.zone
+ tls keypair standby.gpodder.f3s.foo.zone
+ tls keypair radicale.f3s.foo.zone
+ tls keypair www.radicale.f3s.foo.zone
+ tls keypair standby.radicale.f3s.foo.zone
+ tls keypair vault.f3s.foo.zone
+ tls keypair www.vault.f3s.foo.zone
+ tls keypair standby.vault.f3s.foo.zone
+ tls keypair syncthing.f3s.foo.zone
+ tls keypair www.syncthing.f3s.foo.zone
+ tls keypair standby.syncthing.f3s.foo.zone
+ tls keypair uprecords.f3s.foo.zone
+ tls keypair www.uprecords.f3s.foo.zone
+ tls keypair standby.uprecords.f3s.foo.zone
+
+ match request quick header "Host" value "f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "anki.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.anki.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.anki.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "bag.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.bag.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.bag.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "flux.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.flux.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.flux.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "audiobookshelf.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.audiobookshelf.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.audiobookshelf.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "gpodder.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.gpodder.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.gpodder.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "radicale.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "www.radicale.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "standby.radicale.f3s.foo.zone" forward to &lt;f3s&gt;
+ match request quick header "Host" value "