<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Woodpecker on ln --help</title>
    <link>https://blog.mei-home.net/tags/woodpecker/</link>
    <description>Recent content in Woodpecker on ln --help</description>
    <generator>Hugo -- 0.147.2</generator>
    <language>en</language>
    <lastBuildDate>Sat, 16 Aug 2025 21:10:15 +0200</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/woodpecker/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Improving Multi-Arch Image Build Performance by not Emulating</title>
      <link>https://blog.mei-home.net/posts/improving-container-image-build-perf-with-buildah/</link>
      <pubDate>Sat, 16 Aug 2025 21:10:15 +0200</pubDate>
      <guid>https://blog.mei-home.net/posts/improving-container-image-build-perf-with-buildah/</guid>
      <description>I&amp;#39;ve recently improved my container image build performance by not emulating anymore</description>
      <content:encoded><![CDATA[<p>Wherein I update my container image build pipeline in Woodpecker with buildah.</p>
<p>A couple of weekends ago, I naively thought: Hey, how about stepping away from
my <a href="https://blog.mei-home.net/tags/series-tinkerbell/">Tinkerbell experiments</a> for a weekend
and quickly setting up a <a href="https://joinbookwyrm.com/">Bookwyrm</a> instance?</p>
<p>As such things tend to turn out, that rookie move turned into a rather deep
rabbit hole, mostly on account of my container image build pipeline not really
being up to snuff.</p>
<h2 id="the-current-setup">The current setup</h2>
<p>Before going into details on the problem and ultimate solution, I&rsquo;d like to
sketch out my setup. For a detailed view, have a look at <a href="https://blog.mei-home.net/posts/k8s-migration-15-ci/#docker-repo-example">this post</a>.</p>
<p>I&rsquo;m running <a href="https://woodpecker-ci.org/">Woodpecker CI</a> in my Kubernetes cluster,
running container image builds via the <a href="https://woodpecker-ci.org/plugins/docker-buildx">docker-buildx plugin</a>.</p>
<p>As I&rsquo;m running Woodpecker with the <a href="https://woodpecker-ci.org/docs/administration/configuration/backends/kubernetes">Kubernetes backend</a>,
each step in a pipeline will be executed in its own Pod. Each pipeline, in turn,
gets a PersistentVolume mounted, which is shared between all steps of that
pipeline. In my pipelines for the container image builds, I only run the docker-buildx
plugin as a step, once for PRs where the image is only build but not pushed, and
once for pushes onto main, where the image is build and pushed.</p>
<p>The docker-buildx plugin uses Docker&rsquo;s <code>buildx</code> command, and the BuildKit that
makes available to run the image build. Important to note for this post is that
BuildKit will happily build multi-arch images. It does so utilizing Qemu.</p>
<p>Now the issue with that is: The majority of my Homelab consists of Raspberry Pi 4
and a single low power x86 machine. As you might imagine, that makes emulation
very slow, especially on the Pis, which do not have any virtualization instructions.</p>
<p>Now onto the problems I&rsquo;m having with that setup.</p>
<h2 id="the-problems">The problems</h2>
<p>Let&rsquo;s start with the problem which
triggered this particular rabbit hole, the Bookwyrm image build. I won&rsquo;t go into
the details of the image here, that will come in the next post when I describe
the Bookwyrm setup.</p>
<p>The initial issue was one I had seen before on occasion. In this scenario, the
build just gets canceled, with no indication of what went wrong in the Woodpecker
logs for the build step. After quite a lot of digging, I finally found these lines
in the logs of the machine running one of the failed CI Pods:</p>
<pre tabindex="0"><code>kubelet[1088]: I0728 21:07:42.763129    1088 eviction_manager.go:366] &#34;Eviction manager: attempting to reclaim&#34; resourceName=&#34;ephemeral-storage&#34;
kubelet[1088]: I0728 21:07:42.763296    1088 container_gc.go:88] &#34;Attempting to delete unused containers&#34;
kubelet[1088]: I0728 21:07:43.131475    1088 image_gc_manager.go:404] &#34;Attempting to delete unused images&#34;
kubelet[1088]: I0728 21:07:43.172539    1088 eviction_manager.go:377] &#34;Eviction manager: must evict pod(s) to reclaim&#34; resourceName=&#34;ephemeral-storage&#34;
kubelet[1088]: I0728 21:07:43.174677    1088 eviction_manager.go:395] &#34;Eviction manager: pods ranked for eviction&#34; pods=[&#34;woodpecker/wp-01k194yzh8bg8tzngrf7x6w3k4&#34;,&#34;monitoring/grafana-pg-cluster-1&#34;,&#34;harbor/harbor-pg-cluster-1&#34;,&#34;harbor/harbor-registry-5cb6c944f5-wm6np&#34;,&#34;wallabag/wallabag-679f44d9d5-9gl8m&#34;,&#34;harbor/harbor-portal-578db97949-d52sp&#34;,&#34;forgejo/forgejo-74948996b9-r94c2&#34;,&#34;harbor/harbor-jobservice-6cb7fc6d4b-gsswv&#34;,&#34;harbor/harbor-core-6569d4f449-grtrr&#34;,&#34;woodpecker/woodpecker-agent-1&#34;,&#34;taskd/taskd-6f9699f5f4-qkjkr&#34;,&#34;kube-system/cilium-5tx4t&#34;,&#34;fluentbit/fluentbit-fluent-bit-frskm&#34;,&#34;rook-ceph/csi-cephfsplugin-8f4jh&#34;,&#34;rook-ceph/csi-rbdplugin-cnxfz&#34;,&#34;kube-system/cilium-envoy-gx7ck&#34;]
crio[780]: time=&#34;2025-07-28 21:07:43.179344359+02:00&#34; level=info msg=&#34;Stopping container: 7ba324965ba9ed751bd08ac4b464631b2d5dfa05d31f36d98253b68a0d5ec7d0 (timeout: 30s)&#34; id=b69f9664-c0ae-4505-9363-6966afa90b77 name=/runtime.v1.RuntimeService/StopContainer
crio[780]: time=&#34;2025-07-28 21:07:43.837431719+02:00&#34; level=info msg=&#34;Stopped container 7ba324965ba9ed751bd08ac4b464631b2d5dfa05d31f36d98253b68a0d5ec7d0: woodpecker/wp-01k194yzh8bg8tzngrf7x6w3k4/wp-01k194yzh8bg8tzngrf7x6w3k4&#34; id=b69f9664-c0ae-4505-9363-6966afa90b77 name=/runtime.v1.RuntimeService/StopContainer
kubelet[1088]: I0728 21:07:44.097018    1088 eviction_manager.go:616] &#34;Eviction manager: pod is evicted successfully&#34; pod=&#34;woodpecker/wp-01k194yzh8bg8tzngrf7x6w3k4&#34;
</code></pre><p>The Pod just ran out of space while building the images. The fix was relatively
simple, as Woodpecker already provides a Pipeline Volume. In the case of the
Kubernetes backend, that volume is a PVC created per pipeline and then mounted
into the Pods for all of the steps. In my case, that&rsquo;s a 50 GB CephFS volume. But
I wasn&rsquo;t using that volume for anything, as the storage for BuildKit, running my
image builds, was still at the default <code>/var/lib/docker</code>.</p>
<p>So hooray, just move the docker storage to the pipeline volume. I did so by
using the parameter the docker-buildx plugin already provides, <code>storage_path</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">storage_path</span>: <span style="color:#e6db74">&#34;/woodpecker/docker-storage&#34;</span>
</span></span></code></pre></div><p>And just like that, I had fixed the problem. Or not.
<figure>
    <img loading="lazy" src="timeout.png"
         alt="A screenshot of Woodpecker&#39;s CI run UI. It shows that the commit being build is from the &#39;bookwyrm-image&#39; branch. There are three steps in the pipeline: clone, clone Bookwyrm repo and build image. All three are seemingly successful, with clone taking 16 seconds, clone bookwyrm repo clocking in at 21s and build image taking 59:21. The overall workflow takes exactly 1h and is red. On the right is the build log for the image, showing a pip invocation. The last few lines indicate the build of the Python wheel for libsass, showing a lot of &#39;still running...&#39; outputs. The timestamps indicate that by the time of the timeout, the build was running for 21 minutes."/> <figcaption>
            <p>21 minutes and running for a libsass build.</p>
        </figcaption>
</figure>
</p>
<p>So much for that all too short moment of triumph. The storage issue was fixed,
but the image still could not be build. Looking through previous runs, I saw
that the issue wasn&rsquo;t just the duration of the <code>pip</code> install, but also the
initial pull of the Python image. In one of the test builds, the initial pull
took over 50 minutes all on its own. Not much time left for the actual
setup. The root cause was at least not I/O saturation. The CI run I was looking
at ran from 22:25 to 23:25 in the below graph:
<figure>
    <img loading="lazy" src="disk-io-cephfs.png"
         alt="A screenshot of a Grafana time series plot. It shows the time from 22:20 to 00:00. There are three plots shown, each representing one of the HDDs in my system. The metric is I/O utilization. At the beginning, it sits at around 20% to 35%, but at 23:23 it goes up to 80%, shortly followed by going up to around 100% for all three HDDs around 23:28. It stays there until around 23:56, when it goes back to below 10%."/> <figcaption>
            <p>I/O utilization on the HDDs in my Ceph cluster, home of the CephFS data pool.</p>
        </figcaption>
</figure>

The region of 100% I/O saturation in the end, starting at 23:25, is the CephFS
cleanup after the pipeline had failed and the image needed to be cleaned up. The
actual CI run is the 20% to 35% utilization before that.</p>
<p>But I still had the feeling that storage was at least part of the problem. So I
tried to use Ceph RBDs instead of CephFS, which also had the advantage of running
on SATA SSDs instead of HDDs. But that also did not bring any real improvements.
Sure, the build got a lot further and did not spend all its time just extracting
the Python image, but it still didn&rsquo;t finish within the 1h deadline.</p>
<p>I finally ended up figuring that the reason it was still timing out was emulation.</p>
<h2 id="removing-emulation-from-my-image-build-pipelines">Removing emulation from my image build pipelines</h2>
<p>As I&rsquo;ve mentioned above, the docker-buildx Woodpecker plugin I was using used
<a href="https://docs.docker.com/build/buildkit/">Docker&rsquo;s BuildKit</a> under the hood.
BuildKit has the ability to do multi-arch builds out of the box, and uses Qemu
for the non-native architectures. This gets pretty slow on a Raspberry Pi or a
low power x86 machine. So my next plan was to go for parallel builds of all
archs on hosts with the same arch.</p>
<p>BuildKit and docker-buildx already have support for doing this, via BuildKit&rsquo;s
<a href="https://docs.docker.com/build/builders/drivers/remote/">Remote Builders</a>. But
as per the docker-buildx <a href="https://codeberg.org/woodpecker-plugins/docker-buildx/src/branch/main/docs.md#using-remote-builders">documentation</a>,
this can only be done via SSH. I initially thought that this would work with
BuildKit daemons set up to receive external connections, but I was mistaken.
Instead of using BuildKit&rsquo;s build-in remote driver functionality, docker-buildx
instead sets up normal builders with their connection strings pointing to the
remote machines for which SSH was configured. BuildKit would then use those
remote machine&rsquo;s Docker sockets to run the builds.</p>
<p>After some thinking, I decided to dump docker-buildx altogether. I really didn&rsquo;t
like the idea of somehow setting up inter-Pod SSH connections. That just felt
all kinds of wrong.</p>
<p>So I decided: I&rsquo;ll just do it myself, using <a href="https://buildah.io/">Buildah</a>. I&rsquo;ve
had that on my list anyway, so here we go, a bit earlier than planned. Some
inspiration for what follows was found in <a href="https://danmanners.com/posts/2022-08-tekton-cicd-multiarch-builds/">this blog post</a>.
It uses Tekton as the task engine, not Woodpecker, but still was a good starting
point. It was especially useful for answering how to put together the images
produced for different architectures in one manifest.</p>
<p>I started out by building the image for Buildah. The Containerfile ended up
looking like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">ARG</span> alpine_ver<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> alpine:$alpine_ver</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk --no-cache update<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	<span style="color:#f92672">&amp;&amp;</span> apk --no-cache add buildah netavark iptables bash jq<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>I then set up a simple test project in Woodpecker:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build amd64 image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/buildah/buildah:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">buildah build -t testing:0.1 --build-arg alpine_ver=3.22.1 -f testing/Containerfile testing/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: []
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backend_options</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>
</span></span></code></pre></div><p>The Containerfile looked something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">ARG</span> alpine_ver<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> alpine:$alpine_ver</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk --no-cache update<span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	<span style="color:#f92672">&amp;&amp;</span> apk --no-cache add buildah<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Basically, a copy of my Buildah image, just to have something to test.
One thing which surprised me to find out: Woodpecker doesn&rsquo;t actually allow
setting a platform per step. So I got lucky that the Kubernetes backend allows
me to specify the <code>nodeSelector</code> for the step&rsquo;s Pod.</p>
<p>Right away, the first run produced the following error:</p>
<pre tabindex="0"><code>Error: error writing &#34;0 0 4294967295\n&#34; to /proc/16/uid_map: write /proc/16/uid_map: operation not permittedtime=&#34;2025-08-07T20:31:45Z&#34; level=error msg=&#34;writing \&#34;0 0 4294967295\\n\&#34; to /proc/16/uid_map: write /proc/16/uid_map: operation not permitted&#34;
</code></pre><p>Clearly, my dream of rootless image builds would not be fulfilled today, so I
wanted to enable the project to be allowed to run privileged pipelines. Up to now,
I had the docker-buildx plugin in a separate instance-wide list of privileged
plugins. But my new container was, at this point, a simple step, not a plugin.</p>
<p>So my first step was to set my own user as an admin, because I had never needed
admin privileges for Woodpecker before. This I did via the <code>WOODPECKER_ADMIN</code>
environment variable in my <code>values.yaml</code> file for the Woodpecker chart:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">server</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_ADMIN</span>: <span style="color:#e6db74">&#34;my-user&#34;</span>
</span></span></code></pre></div><p>After that, the trusted project settings appeared in the Woodpecker settings
page:
<figure>
    <img loading="lazy" src="trusted-settings.png"
         alt="A screenshot of Woodpecker&#39;s project settings page. It shows the &#39;Project&#39; tab being selected. Under the &#39;Trusted&#39; heading, the &#39;Security&#39; option checkbox is checked. The &#39;Network&#39; and &#39;Volumes&#39; options are left unchecked."/> <figcaption>
            <p>Trusted settings in the project configuration of Woodpecker. The options under the &lsquo;Trusted&rsquo; heading only show up for admin users.</p>
        </figcaption>
</figure>

Enabling the <code>Security</code> option allowed me to run the Buildah containers in
privileged mode, by adding the <code>privileged: true</code> option.</p>
<p>The next error I got was this one:</p>
<pre tabindex="0"><code>Error: &#39;overlay&#39; is not supported over overlayfs, a mount_program is required: backing file system is unsupported for this graph driver
time=&#34;2025-08-07T20:57:11Z&#34; level=warning msg=&#34;failed to shutdown storage: \&#34;&#39;overlay&#39; is not supported over overlayfs, a mount_program is required: backing file system is unsupported for this graph driver\&#34;&#34;
</code></pre><p>At this point, my pipeline volume was still on a Ceph RBD, as I had not yet realized
that, with the plan of running multiple Buildah steps for the different platforms
in parallel, I would need RWX volumes for the pipelines. So I decided that the
right solution would be to move the storage onto my pipeline volume, where before
it just sat in the container&rsquo;s own filesystem, leading to the above &ldquo;OverlayFS on OverlayFS&rdquo; error. I did this by adding <code>--root /woodpecker</code>
to the Buildah command.</p>
<p>And then I got the next one:</p>
<pre tabindex="0"><code>STEP 1/2: FROM alpine:3.22.1
Error: creating build container: could not find &#34;netavark&#34; in one of [/usr/local/libexec/podman /usr/local/lib/podman /usr/libexec/podman /usr/lib/podman].  To resolve this error, set the helper_binaries_dir key in the `[engine]` section of containers.conf to the directory containing your helper binaries.
</code></pre><p>This was fixed rather easily by adding <code>netavark</code> to the Buildah image. I had a
similar error next, about <code>iptables</code> not being available. So I installed that
one as well.</p>
<p>But that wasn&rsquo;t all. Oh no, here&rsquo;s another error:</p>
<pre tabindex="0"><code>buildah --root /woodpecker build -t testing:0.1 --build-arg alpine_ver=3.22.1 -f testing/Containerfile testing/
STEP 1/2: FROM alpine:3.22.1
WARNING: image platform (linux/arm64/v8) does not match the expected platform (linux/amd64)
STEP 2/2: RUN apk --no-cache update	&amp;&amp; apk --no-cache add buildah
exec container process `/bin/sh`: Exec format error
Error: building at STEP &#34;RUN apk --no-cache update	&amp;&amp; apk --no-cache add buildah&#34;: while running runtime: exit status 1
</code></pre><p>That one confused me a little bit, to be honest. It wasn&rsquo;t difficult to fix, I just
had to add the <code>--platform linux/amd64</code> option to the Buildah command. What
confused me was that Buildah didn&rsquo;t somehow figure that out for itself.</p>
<p>And this was the point where I realized that my two CI steps, one for amd64, one
for arm64, did not run in parallel. The one started only after the other had failed.
One <code>kubectl describe -n woodpecker pods wp-...</code> later, I saw that that was
because the Pod which launched second failed to mount the pipeline volume. And
that in turn was because I had switched to an SSD-backed Ceph RBD for the volume,
to improve speed. But RBDs are, by their nature as block devices, RWO, and cannot
be mounted by multiple Pods.</p>
<p>I switched the volumes back to CephFS and was met with the same error I had
seen previously and &ldquo;fixed&rdquo; by moving Buildah&rsquo;s storage onto the pipeline volume:</p>
<pre tabindex="0"><code>time=&#34;2025-08-07T21:56:14Z&#34; level=error msg=&#34;&#39;overlay&#39; is not supported over &lt;unknown&gt; at \&#34;/woodpecker/overlay\&#34;&#34;
Error: kernel does not support overlay fs: &#39;overlay&#39; is not supported over &lt;unknown&gt; at &#34;/woodpecker/overlay&#34;: backing file system is unsupported for this graph driver
time=&#34;2025-08-07T21:56:14Z&#34; level=warning msg=&#34;failed to shutdown storage: \&#34;kernel does not support overlay fs: &#39;overlay&#39; is not supported over &lt;unknown&gt; at \\\&#34;/woodpecker/overlay\\\&#34;: backing file system is unsupported for this graph driver\&#34;&#34;
</code></pre><p>I&rsquo;m not sure why it said &ldquo;unknown&rdquo;, but the filesystem was CephFS. After some
searching, I found out that OverlayFS and CephFS are seemingly incompatible. But
the issue was fixable by adding <code>--storage-driver=vfs</code> to the Buildah command.
The VFS driver is a bit older than OverlayFS, and a bit slower. But at least
it works on CephFS.</p>
<p>And believe it or not, that was the last error. After adding the <code>--storage</code>
option, the build ran through cleanly. At this point, my Woodpecker workflow
looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;.woodpecker/testing.yaml&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;testing/*&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">variables</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;alpine-version</span> <span style="color:#e6db74">&#39;3.22.1&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build amd64 image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/buildah:0.4</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">buildah --root /woodpecker build --storage-driver=vfs --platform linux/amd64 -t testing:0.1 --build-arg alpine_ver=3.22.1 -f testing/Containerfile testing/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: []
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privileged</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backend_options</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build arm64 image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/buildah:0.4</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">buildah --root /woodpecker build --storage-driver=vfs --platform linux/arm64 -t testing:0.1 --build-arg alpine_ver=3.22.1 -f testing/Containerfile testing/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: []
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privileged</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backend_options</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#e6db74">&#34;arm64&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">push image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/buildah:0.4</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">sleep 10000</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: [<span style="color:#e6db74">&#34;build amd64 image&#34;</span>, <span style="color:#e6db74">&#34;build arm64 image&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privileged</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span></code></pre></div><p>With this configuration, the two builds for amd64 and arm64 are run in parallel,
and the final <code>push image</code> step would be responsible for combining the
images into a single manifest and pushing it all to my Harbor instance.</p>
<p>I ran a test build and then exec&rsquo;d into the Pod when the pipeline arrived at the
<code>push image</code> step. I used the following commands to combine the manifests
and push them up to Harbor:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>buildah --root /woodpecker --storage-driver<span style="color:#f92672">=</span>vfs manifest create harbor.example.com/homelab/testing:0.1
</span></span><span style="display:flex;"><span>buildah --root /woodpecker --storage-driver<span style="color:#f92672">=</span>vfs manifest add harbor.example.com/homelab/testing:0.1 3883d7a9067d
</span></span><span style="display:flex;"><span>buildah --root /woodpecker --storage-driver<span style="color:#f92672">=</span>vfs manifest add harbor.example.com/homelab/testing:0.1 0130169db3bb
</span></span><span style="display:flex;"><span>buildah login https://harbor.example.com
</span></span><span style="display:flex;"><span>buildah --root /woodpecker --storage-driver<span style="color:#f92672">=</span>vfs manifest push harbor.example.com/homelab/testing:0.1 docker://harbor.example.com/homelab/testing:0.1
</span></span></code></pre></div><p>The problematic thing about this approach was that I had no way of knowing the
correct values for the image names in the <code>manifest add</code> commands, where I used
the image hashes in this example. I could of course set separate names for the
image, e.g. with the platform in the name. But then I would have to remember to
do that every time I create a new pipeline.</p>
<p>Instead, I decided to go one step further and check how painful it would be
to turn my simple command-based steps into a Woodpecker plugin.</p>
<h2 id="building-a-woodpecker-plugin">Building a Woodpecker plugin</h2>
<p>And it turns out: It isn&rsquo;t complicated at all. The docs for <a href="https://woodpecker-ci.org/docs/usage/plugins/creating-plugins">new Woodpecker plugins</a>
is rather short and sweet. Plugins need to be containerized, and they need to have
their program set as the entrypoint in the image. And that&rsquo;s it. Any options given
in the step are forwarded to the step container via environment variables, so there&rsquo;s
nothing special to be done at all.</p>
<p>That was good news, as I was a bit afraid I would have to write some Go. But no,
just pure bash was enough.</p>
<p>In the final result, my pipeline for the testing image will look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;.woodpecker/testing.yaml&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;testing/*&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">variables</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;alpine-version</span> <span style="color:#e6db74">&#39;3.22.1&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;image-version</span> <span style="color:#e6db74">&#39;0.2&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;buildah-config</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">context</span>: <span style="color:#ae81ff">testing/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">containerfile</span>: <span style="color:#ae81ff">testing/Containerfile</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build_args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alpine_ver</span>: <span style="color:#75715e">*alpine-version</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build amd64 image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/woodpecker-plugin-buildah:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;&lt;</span>: <span style="color:#75715e">*buildah-config</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">platform</span>: <span style="color:#ae81ff">linux/amd64</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: []
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backend_options</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#e6db74">&#34;amd64&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build arm64 image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/woodpecker-plugin-buildah:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;&lt;</span>: <span style="color:#75715e">*buildah-config</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">platform</span>: <span style="color:#ae81ff">linux/arm64</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: []
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">backend_options</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#e6db74">&#34;arm64&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">push image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">harbor.example.com/homelab/woodpecker-plugin-buildah:latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">manifest_platforms</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#e6db74">&#34;linux/arm64&#34;</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#e6db74">&#34;linux/amd64&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#ae81ff">1.5</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">harbor.example.com/homelab/testing</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">username</span>: <span style="color:#ae81ff">ci</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">password</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">container-registry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>: [<span style="color:#e6db74">&#34;build amd64 image&#34;</span>, <span style="color:#e6db74">&#34;build arm64 image&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">privileged</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span></code></pre></div><p>When a Woodpecker plugin is launched, it gets all of the values under <code>settings:</code>
handed in as environment variables.
A normal key/value pair like <code>type: push</code> would appear as <code>PLUGIN_TYPE=&quot;push&quot;</code> in
the plugin&rsquo;s container.
Lists like the <code>tags</code> or <code>manifest_platforms</code> appear as comma-separated lists in,
e.g. <code>PLUGIN_TAGS=&quot;latest,1.5&quot;</code>.
Objects are a bit more complicated, and they&rsquo;re handed over as JSON objects, e.g.
<code>PLUGIN_BUILD_ARGS='{&quot;alpine_ver&quot;: &quot;3.22.1&quot;}''</code>.</p>
<p>First, there is a bit of a preamble in the script, to check whether required
config options have been set and Buildah is available:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>DATA_ROOT<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/woodpecker&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ! command -v buildah; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;buildah not found, exiting.&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_TYPE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;PLGUIN_TYPE not set, exiting.&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><p>Then, depending on the <code>PLUGIN_TYPE</code> variable, either the <code>build</code> or the <code>push</code>
function is executed, while either builds the image for a single platform or
combines multiple platforms into a single manifest and pushes it all to the
given registry:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_TYPE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;build&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Running build...&#34;</span>
</span></span><span style="display:flex;"><span>  build <span style="color:#f92672">||</span> exit $?
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> <span style="color:#f92672">[[</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_TYPE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;push&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Running push...&#34;</span>
</span></span><span style="display:flex;"><span>  push <span style="color:#f92672">||</span> exit $?
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Unknown type </span><span style="color:#e6db74">${</span>PLUGIN_TYPE<span style="color:#e6db74">}</span><span style="color:#e6db74">, exiting&#34;</span>
</span></span><span style="display:flex;"><span>  exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>exit <span style="color:#ae81ff">0</span>
</span></span></code></pre></div><p>And here is the <code>build</code> function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>build<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_CONTEXT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_CONTEXT not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_PLATFORM<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_PLATFORM not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_CONTAINERFILE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_CONTAINERFILE not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -n <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_BUILD_ARGS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    BUILD_ARGS<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>get_build_args <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_BUILD_ARGS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  command<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;buildah \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">--root </span><span style="color:#e6db74">${</span>DATA_ROOT<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">build \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">--storage-driver=vfs \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">--platform </span><span style="color:#e6db74">${</span>PLUGIN_PLATFORM<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">-t </span><span style="color:#e6db74">${</span>PLUGIN_PLATFORM<span style="color:#e6db74">}</span><span style="color:#e6db74">:0.0 \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span><span style="color:#e6db74">${</span>BUILD_ARGS<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">-f </span><span style="color:#e6db74">${</span>PLUGIN_CONTAINERFILE<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span><span style="color:#e6db74">${</span>PLUGIN_CONTEXT<span style="color:#e6db74">}</span><span style="color:#e6db74"> \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Running command: </span><span style="color:#e6db74">${</span>command<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">${</span>command<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> $?
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>It again starts out with some checks to make sure the required variables are set.
Then it runs the <code>buildah build</code> command as in the previous setup with the manual
command. The one &ldquo;special&rdquo; thing I&rsquo;m doing here is that I tag the new image with
the <code>PLUGIN_PLATFORM</code> variable and the <code>:0.0</code> version. The storage for the builders
is entirely temporary, so I will never have multiple versions in the storage,
and this allows me to make the names of the images predictable in the later
<code>push</code> step. So at the end of the function&rsquo;s run, I would have images <code>linux/amd64:0.0</code>
and <code>linux/arm64:0.0</code> in the same storage.</p>
<p>Which then brings us to the <code>push</code> function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>push<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_REPO<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_REPO not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_TAGS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_TAGS not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    TAGS<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_TAGS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | tr <span style="color:#e6db74">&#39;,&#39;</span> <span style="color:#e6db74">&#39; &#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_MANIFEST_PLATFORMS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_MANIFEST_PLATFORMS not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    PLATFORMS<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>echo <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_MANIFEST_PLATFORMS<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> | tr <span style="color:#e6db74">&#39;,&#39;</span> <span style="color:#e6db74">&#39; &#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_USERNAME<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_USERNAME not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -z <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_PASSWORD<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;PLUGIN_PASSWORD not set, aborting.&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Logging in...&#34;</span>
</span></span><span style="display:flex;"><span>  buildah login -p <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_PASSWORD<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -u <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_USERNAME<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_REPO<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#f92672">||</span> <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Creating manifest...&#34;</span>
</span></span><span style="display:flex;"><span>  buildah --root <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>DATA_ROOT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> --storage-driver<span style="color:#f92672">=</span>vfs manifest create newimage <span style="color:#f92672">||</span> <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> plt in <span style="color:#e6db74">${</span>PLATFORMS<span style="color:#e6db74">}</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    echo <span style="color:#e6db74">&#34;Adding platform </span><span style="color:#e6db74">${</span>plt<span style="color:#e6db74">}</span><span style="color:#e6db74">...&#34;</span>
</span></span><span style="display:flex;"><span>    buildah --root <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>DATA_ROOT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> --storage-driver<span style="color:#f92672">=</span>vfs manifest add newimage <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>plt<span style="color:#e6db74">}</span><span style="color:#e6db74">:0.0&#34;</span> <span style="color:#f92672">||</span> <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Pushing to registry...&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> tag in <span style="color:#e6db74">${</span>TAGS<span style="color:#e6db74">}</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    buildah --root <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>DATA_ROOT<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> --storage-driver<span style="color:#f92672">=</span>vfs manifest push newimage docker://<span style="color:#e6db74">${</span>PLUGIN_REPO<span style="color:#e6db74">}</span>:<span style="color:#e6db74">${</span>tag<span style="color:#e6db74">}</span> <span style="color:#f92672">||</span> <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  buildah logout <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>PLUGIN_REPO<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>Here I need to do some more things than in the build step. First is the login,
which is done via <code>buildah login</code>. Something which slightly annoys me here is
the fact that Buildah only seems to support either interactive input of the
password, or providing it via a CLI flag, but not e.g. via an environment
variable.</p>
<p>When the login succeeds, the code iterates over all platforms and adds the
<code>$PLATFORM:0.0</code> image to the new manifest. Once that&rsquo;s all done, the resulting
manifest containing all the required platform&rsquo;s images is pushed to the repository
given in the <code>repo</code> option for the plugin.</p>
<p>I prefer having a plugin like this, because Woodpecker&rsquo;s &ldquo;command form&rdquo; steps
cannot re-use Yaml anchors like I was able to do here, so there would have
been a lot more repetition in the pipeline setups.</p>
<h2 id="performance">Performance</h2>
<p>After I got the plugin working, I started migrating my existing image builds over
to the new plugin. I started out with my <a href="https://www.fluentd.org/">Fluentd</a>
image, where I take the official Fluentd image and install a few additional
plugins into it before deploying into my Kubernetes cluster. The image file
looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">ARG</span> fluentd_ver<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> fluent/fluentd:${fluentd_ver}</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> root</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ln -s /usr/bin/dpkg-split /usr/sbin/dpkg-split<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ln -s /usr/bin/dpkg-deb /usr/sbin/dpkg-deb<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ln -s /bin/rm /usr/sbin/rm<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> ln -s /bin/tar /usr/sbin/tar<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> buildDeps<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sudo make gcc g++ libc-dev&#34;</span> apt-get update <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	<span style="color:#f92672">&amp;&amp;</span> apt-get install -y --no-install-recommends $buildDeps curl <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	<span style="color:#f92672">&amp;&amp;</span> gem install <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>       fluent-plugin-grafana-loki <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>       fluent-plugin-record-modifier <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-multi-format-parser <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-rewrite-tag-filter <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-route <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-http-healthcheck <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-kv-parser <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	     fluent-plugin-parser-logfmt <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>	<span style="color:#f92672">&amp;&amp;</span> gem sources --clear-all <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  <span style="color:#f92672">&amp;&amp;</span> SUDO_FORCE_REMOVE<span style="color:#f92672">=</span>yes <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      apt-get purge -y --auto-remove <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>                    -o APT::AutoRemove::RecommendsImportant<span style="color:#f92672">=</span>false <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>                    $buildDeps <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  <span style="color:#f92672">&amp;&amp;</span> rm -rf /var/lib/apt/lists/* <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  <span style="color:#f92672">&amp;&amp;</span> rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span><span style="color:#e6db74"> fluent</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>And that&rsquo;s where I discovered that my performance wasn&rsquo;t exactly up to snuff
still:
<figure>
    <img loading="lazy" src="shared-disk-build.png"
         alt="A screenshot of Woodpecker&#39;s CI run UI. On the left, it shows the Fluentd build and its steps. The clone steps finishes in 15s, but the two build steps for amd64 and arm64 take 22:57 and 23:32 respectively. The final &#39;push image&#39; steps takes 04:49 minutes and failed. To the right are some logs of the adm64 image build, showing the executed buildah command and the initial pull of the fluentd/fluentd:v1.19.0-debian-1.0 image. To the very right of the output, a relative timestamp shows that the first step after the image pull, &#39;USER root&#39;, happens 1087s after the start of the step&#39;s run."/> <figcaption>
            <p>The fluentd image build takes around 23 minutes, with the lion&rsquo;s share of 1087s/18 minutes taken by the pull of the fluentd image.</p>
        </figcaption>
</figure>

So here is a problem: The Fluentd build takes over 23 minutes. That&rsquo;s a lot, and
from the logs it looks like the initial image pull of the official Fluentd image
takes 18 minutes on its own. Even though not shown here, it&rsquo;s a similar situation
on the arm64 build. I checked my connection, and the image was pulled from my local
Harbor pull-through cache, it was not just a case of DockerHub being slow.</p>
<p>The problem here again seems to be CephFS and/or the nature of container images
on disk. Because for a long time, the Ceph cluster was adding 10k objects per
15s interval:
<figure>
    <img loading="lazy" src="cephfs-buildah-run.png"
         alt="A screenshot of a Grafana time series plot. It shows the object count changes per Ceph pool. Of interest here is the CephFS bulk pool. Starting at about 10:32, it produces 10k new objects per 15s, and does so almost continuously until 10:54."/> <figcaption>
            <p>Objects added in a 15s interval to the pools of my Ceph cluster. Orange/top line is my CephFS storage pool.</p>
        </figcaption>
</figure>

In total, this single two-image build added about 180k objects to the cluster:
<figure>
    <img loading="lazy" src="objects-in-cluster.png"
         alt="Another screenshot of a Grafana time series plot. This time it shows the number of objects in the entire cluster. It starts out at about 2.03 million objects. At around 10:32, it starts rising at a pretty consistent rate, until it hits its peak of about 2.20 million objects at around 10:54. Afterwards, it&#39;s stable for a little while at around 2.19 million, before it goes down steadily again to the previous 2.03 million in the span of just 10 minutes."/> <figcaption>
            <p>The CI run produced about 180k new objects in the Ceph storage cluster.</p>
        </figcaption>
</figure>
</p>
<p>After seeing all of this, I decided that the current setup might not be ideal
when it comes to storage. One thought I had was that both builds using the same
<code>--root</code> parameter on the shared volume might be part of the problem, thinking
that perhaps Buildah did some locking of the storage area?
So I switched the different platform builds to different directories on the
shared volume. That did work somewhat, reducing the duration down to about 15
minutes:
<figure>
    <img loading="lazy" src="separate-dirs-build.png"
         alt="Another screenshot of the Woodpecker UI, showing the same Fluentd build as before. This time, the amd64 and arm64 image build steps only took 15:06 and 14:56 respectively. The push image step still failed. The relative timestamp on the right now shows that the &#39;USER root&#39; step of the Dockerfile started after 659 seconds this time."/> <figcaption>
            <p>Still with a shared volume, but not with a shared directory on that volume, the builds take less time.</p>
        </figcaption>
</figure>

The builds go from about 24 minutes to only 15 minutes. The initial pull of the
Fluentd image goes down to about 11 minutes, from the previous 18 minutes.</p>
<p>This still seemed pretty long, so I started to consider the creation of a new
CephFS with the data pool on SSDs, to hopefully improve the performance. But then
I had a thought: How about removing the parallelism entirely?
If I were to not run the steps in parallel, I could use a Ceph RBD instead,
which would likely already be faster. I also already have a StorageClass for
SSD-backed RBDs in my cluster, so no additional config would be necessary.
And finally, using a Ceph RBD instead of CephFS, I would be able to use the faster
OverlayFS storage driver for Buildah.</p>
<p>So I did all of that, switched the StorageClass for Woodpecker&rsquo;s pipeline volumes
to my SSD RBD class, and then disabled parallelism for the steps. The results
were rather impressive:
<figure>
    <img loading="lazy" src="finally-fast.png"
         alt="Another screenshot of the Woodpecker UI, this time showing the image build steps only taking 01:42 minutes and 01:53 minutes. The push image step is successful now as well, taking 02:07 minutes. To the right, the logs of the image pull for the Fluentd image are shown again. The pull now took only 18s."/> <figcaption>
            <p>Both builds done sequentially on an SSD-backed Ceph RBD are faster than the same builds done in parallel, but on a CephFS volume with the VFS storage driver.</p>
        </figcaption>
</figure>
</p>
<p>The entire pipeline has run through in about six minutes. Less time than the previous
setup needed just for pulling down the Fluentd image.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>Even with all the weird errors I had to fix and the wrong turns I took, this
was fun, and the fact that I ended up without any parallelism was surprising.
I really enjoyed working on this one.</p>
<p>There are still a few improvements to be made, and some things to dig into. One
burning question I currently have is why the parallelized version, using the
VFS storage driver running on a CephFS shared volume, was so much slower. Was it
mostly the slower VFS storage driver? Or was it CephFS? And if it was CephFS,
what was actually the bottleneck? Because I wasn&rsquo;t able to find one, neither in
IO utilization, nor network, nor CPU on any of the nodes involved. I checked
both, the nodes running the Buildah Pods and the Ceph nodes, and none seemed to
show any overloads in any resource. So I&rsquo;m a bit stumped.</p>
<p>Then there&rsquo;s also the fact that my Woodpecker steps still need to run in privileged
mode. I don&rsquo;t like that, but I wasn&rsquo;t able to figure out exactly what to do to
remove that requirement. From everything I&rsquo;ve read, this should be possible with
Buildah, but might need some additional configuration on the Kubernetes nodes.
I will have to check this in the future.</p>
<p>But for now, finally back to working on setting up a Bookwyrm instance.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Nomad to k8s, Part 15: Migrating my CI</title>
      <link>https://blog.mei-home.net/posts/k8s-migration-15-ci/</link>
      <pubDate>Sun, 26 Jan 2025 22:50:33 +0100</pubDate>
      <guid>https://blog.mei-home.net/posts/k8s-migration-15-ci/</guid>
      <description>Migrating my Drone CI install on Nomad to a Woodpecker CI on Kubernetes</description>
      <content:encoded><![CDATA[<p>Wherein I migrate my Drone CI setup on Nomad to a Woodpecker CI setup on k8s.</p>
<p>This is part 16 of my <a href="https://blog.mei-home.net/tags/k8s-migration/">k8s migration series</a>.</p>
<p>Finally, another migration blog post! I&rsquo;m still rather happy that I&rsquo;m getting
into it again.
For several years now, I&rsquo;ve been running a CI setup to automate a number of
tasks related to some personal projects. CI stands for <a href="https://en.wikipedia.org/wiki/Continuous_integration">Continuous Integration</a>,
and Wikipedia says this about it:</p>
<blockquote>
<p>Continuous integration (CI) is the practice of integrating source code changes frequently and ensuring that the integrated codebase is in a workable state.</p></blockquote>
<p>I&rsquo;m pretty intimately familiar with the concept on a rather large scale, as I&rsquo;m
working in a CI team at a large company.</p>
<p>In the Homelab, I&rsquo;m using CI for a variety of use cases, ranging from the
traditional automated test cases for software I&rsquo;ve written to just a convenient
automation for things like container image builds. I will go into details on a
few of those use cases later on, when I describe how I&rsquo;ve migrated some of my
projects.</p>
<p>The basic principle of CI for me is: You push a commit to a Git repository,
and a piece of software automatically launches a variety of test jobs. These
can range from UT jobs, over automated linter runs up to automated deploys of
the updated software.</p>
<h2 id="from-drone-ci-to-woodpecker-ci">From Drone CI to Woodpecker CI</h2>
<p>Since I started running a CI, I&rsquo;ve been using <a href="https://www.drone.io/">Drone CI</a>.
It&rsquo;s a relatively simple CI system, compared to what one could build e.g. with
<a href="https://zuul-ci.org/">Zuul</a>, <a href="https://www.jenkins.io/">Jenkins</a> and <a href="https://www.gerritcodereview.com/">Gerrit</a>.</p>
<p>Drone CI consists of two components, the Drone CI server providing web hooks for
the Git Forge to call and launching the jobs, and agents, which take the jobs
and run them. In my deployment on Nomad, I was using the <a href="https://github.com/drone-runners/drone-runner-docker">drone-runner-docker</a>.
It mounts the host&rsquo;s Docker socket into the agent and uses it to launch Docker
containers for each step of the CI pipeline.</p>
<p>It has always worked well for me and mostly got out of my way. So I didn&rsquo;t switch
to <a href="https://woodpecker-ci.org/">Woodpecker CI</a> because of features. There aren&rsquo;t
that many different features anyway, because Woodpecker is a community fork of
Drone CI.
Rather, Drone CI started to have quite a bad smell. What bothered me the most
was that their release notes were basically empty and said things like
&ldquo;integrated UI updates&rdquo;.
Then there is whatever happens after they were bought by Harness. Then there&rsquo;s
the fact that the component which needs to mount your host&rsquo;s Docker socket hasn&rsquo;t
been updated in over a year.</p>
<p>In contrast, Woodpecker is a community project and had a far nicer smell, so I
decided that while I was at it, I would not just migrate Drone to k8s but also
switch to Woodpecker.</p>
<p>One of the things I genuinely looked forward to was the backend. With the migration
to k8s, I could finally make use of my entire cluster. With Drone&rsquo;s Docker runner,
I always had to reserve a lot of resources for the CI job execution on the nodes
where the agents were launched.
Now, with the Kubernetes backend, it doesn&rsquo;t matter (much, more later) where
the agents are running - the only thing they do is launching Pods to run each
step of the pipeline, but where those are scheduled is left to Kubernetes.</p>
<p>I will go into more detail later, when talking about my CI job migrations,
but let me still give a short example of what I&rsquo;m actually talking about.</p>
<p>Here&rsquo;s a slight variation of the example pipeline from the <a href="https://woodpecker-ci.org/docs/usage/intro">Woodpecker docs</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branch</span>: <span style="color:#ae81ff">master</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">debian</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">echo &#34;This is the build step&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">echo &#34;binary-data-123&#34; &gt; executable</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">a-test-step</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">golang:1.16</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">echo &#34;Testing ...&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./executable</span>
</span></span></code></pre></div><p>This pipeline tells Woodpecker that it should only be run when a Git push is
done to the <code>master</code> branch of the repository. This file would be committed to
the repository it&rsquo;s used in, but there are also options to tell Woodpecker
to listen on events for other repositories. So you could theoretically even have
a separate &ldquo;CI&rdquo; repository with all the pipelines. But that&rsquo;s generally not a
good idea.</p>
<p>The pipeline itself will execute two separate steps, called &ldquo;build&rdquo; and &ldquo;a-test-step&rdquo;.
The <code>image:</code> parameter defines which container image is executed, in this case
Debian and the golang image. And then follows a list of commands to be run.
In this case, they&rsquo;re pretty nonsensical and will lead to failed pipelines,
but it&rsquo;s only here for demonstration purposes anyway. In the Woodpecker web UI,
this is what the pipeline looks like:</p>
<figure>
    <img loading="lazy" src="first_run.png"
         alt="A screenshot of the Woodpecker web UI. It is separated into two main areas. The left one shows an overview of the pipeline and its steps. At the top left, it shows that the pipeline was launched by a push from user mmeier. Below that follows the list of steps, showing in order: clone, build, a-test-step. Both clone and build have a green check mark next to them, while a-test-step has a red X. The a-test-step step is also highlighted. On the right side, a window header &#39;Step Logs&#39; shows the logs from the a-test-step execution. It starts out echoing the string &#39;Testing ...&#39;, followed by &#39;/bin/sh: 18: ./executable: Permission denied&#39;."/> <figcaption>
            <p>Screenshot of my first Woodpecker CI pipeline execution.</p>
        </figcaption>
</figure>

<h2 id="database-deployment">Database deployment</h2>
<p>To begin with, Woodpecker needs a bit of infrastructure set up, namely a
Postgres database. Smaller deployments can also be run on SQLite, I&rsquo;m using
Postgres mostly out of habit.</p>
<p>As I&rsquo;ve <a href="https://blog.mei-home.net/posts/k8s-migration-8-cloud-native-pg/">written about before</a>,
I&rsquo;m using <a href="https://cloudnative-pg.io/">CloudNativePG</a> for my Postgres DB needs.
In the recent <a href="https://cloudnative-pg.io/documentation/1.25/release_notes/v1.25/">1.25 release</a>,
CNPG introduced support for creating multiple databases in a single Cluster.
But because I&rsquo;ve already started with &ldquo;one Cluster per app&rdquo;, I decided to stay
with that approach for the duration of the k8s migration and look into merging
it all into one Cluster later.</p>
<p>Because I&rsquo;ve written about it in detail before, here&rsquo;s just the basic options
for the CNPG Cluster CRD I&rsquo;m using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Cluster</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">woodpecker-pg-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">instances</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">imageName</span>: <span style="color:#e6db74">&#34;ghcr.io/cloudnative-pg/postgresql:16.2-10&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">bootstrap</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">initdb</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">database</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">owner</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">200M</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">150m</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">postgresql</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">max_connections</span>: <span style="color:#e6db74">&#34;200&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">shared_buffers</span>: <span style="color:#e6db74">&#34;50MB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">effective_cache_size</span>: <span style="color:#e6db74">&#34;150MB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">maintenance_work_mem</span>: <span style="color:#e6db74">&#34;12800kB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checkpoint_completion_target</span>: <span style="color:#e6db74">&#34;0.9&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">wal_buffers</span>: <span style="color:#e6db74">&#34;1536kB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">default_statistics_target</span>: <span style="color:#e6db74">&#34;100&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">random_page_cost</span>: <span style="color:#e6db74">&#34;1.1&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">effective_io_concurrency</span>: <span style="color:#e6db74">&#34;300&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">work_mem</span>: <span style="color:#e6db74">&#34;128kB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">huge_pages</span>: <span style="color:#e6db74">&#34;off&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">max_wal_size</span>: <span style="color:#e6db74">&#34;128MB&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">wal_keep_size</span>: <span style="color:#e6db74">&#34;512MB&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">storage</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">size</span>: <span style="color:#ae81ff">1.</span><span style="color:#ae81ff">5G</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">rbd-fast</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backup</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">barmanObjectStore</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">endpointURL</span>: <span style="color:#ae81ff">http://rook-ceph-rgw-rgw-bulk.rook-cluster.svc:80</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">destinationPath</span>: <span style="color:#e6db74">&#34;s3://backup-cnpg/&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">s3Credentials</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">accessKeyId</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">rook-ceph-object-user-rgw-bulk-cnpg-backup-woodpecker</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">key</span>: <span style="color:#ae81ff">AccessKey</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">secretAccessKey</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">rook-ceph-object-user-rgw-bulk-cnpg-backup-woodpecker</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">key</span>: <span style="color:#ae81ff">SecretKey</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">retentionPolicy</span>: <span style="color:#e6db74">&#34;30d&#34;</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">postgresql.cnpg.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ScheduledBackup</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">woodpecker-pg-backup</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">method</span>: <span style="color:#ae81ff">barmanObjectStore</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">immediate</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>: <span style="color:#e6db74">&#34;0 30 1 * * *&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">backupOwnerReference</span>: <span style="color:#ae81ff">self</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cluster</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">woodpecker-pg-cluster</span>
</span></span></code></pre></div><p>As always, I&rsquo;m configuring backups right away.
For CNPG to work, the operator needs network access to the Postgres instance
started up in the Woodpecker namespace, so a network policy is also needed:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumNetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;woodpecker-pg-cluster-allow-operator-ingress&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">endpointSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cnpg.io/cluster</span>: <span style="color:#ae81ff">woodpecker-pg-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">fromEndpoints</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">io.kubernetes.pod.namespace</span>: <span style="color:#ae81ff">cnpg-operator</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">app.kubernetes.io/name</span>: <span style="color:#ae81ff">cloudnative-pg</span>
</span></span></code></pre></div><p>While we&rsquo;re on the topic of network policies, here&rsquo;s my generic deny-all
policy I&rsquo;m using in most namespaces:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumNetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;woodpecker-deny-all-ingress&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">endpointSelector</span>: {}
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">fromEndpoints</span>:
</span></span><span style="display:flex;"><span>      - {}
</span></span></code></pre></div><p>This allows all intra-namespace access between Pods, but no ingress from any
Pods in other namespaces.</p>
<p>And because Woodpecker provides a web UI, I also need to provide access to the
<code>server</code> Pod from my Traefik ingress:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumNetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;woodpecker-traefik-access&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">endpointSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;app.kubernetes.io/name&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;server&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">fromEndpoints</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">homelab/ingress</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">io.kubernetes.pod.namespace</span>: <span style="color:#ae81ff">traefik-ingress</span>
</span></span></code></pre></div><p>Hm, writing all of this up I&rsquo;m realizing that I completely forgot to write a
post about some &ldquo;standard things&rdquo; I will be doing for most apps. I had planned
to do that for the migration of my Audiobookshelf instance to k8s, but
completely forgot to write any post about it at all. Will put it on the pile. &#x1f604;</p>
<p>Before getting to the Woodpecker Helm chart, we also need to do a bit of
yak shaving with regards to the CNPG DB secrets. Helpfully, CNPG always
creates a secret with the necessary credentials to access the database,
in multiple formats. An example would look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">data</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dbname</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">host</span>: <span style="color:#ae81ff">woodpecker-pg-cluster-rw</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jdbc-uri</span>: <span style="color:#ae81ff">jdbc:postgresql://woodpecker-pg-cluster-rw.woodpecker:5432/woodpecker?password=1234&amp;user=woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">password</span>: <span style="color:#ae81ff">1234</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pgpass</span>: <span style="color:#ae81ff">woodpecker-pg-cluster-rw:5432:woodpecker:woodpecker:1234</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">port</span>: <span style="color:#ae81ff">5432</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uri</span>: <span style="color:#ae81ff">postgresql://woodpecker:1234@woodpecker-pg-cluster-rw.woodpecker:5432/woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">user</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">username</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span></code></pre></div><p>I would love to be able to use the values from that Secret verbatim, specifically
the <code>uri</code> property, to set the <code>WOODPECKER_DATABASE_DATASOURCE</code> variable from
it. But sadly, the <a href="https://github.com/woodpecker-ci/helm">Woodpecker Helm chart</a>
is one of those which do allow Secrets to be used to set environment variables -
but only via <code>envFrom.secretRef</code>. Which feeds the Secret&rsquo;s keys in as env
variables, but doesn&rsquo;t allow to set specific env variables to specific keys
from the secret, via <code>env.valueFrom.secretKeyRef</code>.</p>
<p>I think this should be a
functionality every Helm chart provides, specifically for cases like this. I&rsquo;ve
got two tools which automatically create Secrets in my cluster, CNPG for DB
credentials and configs, and Rook, which creates Secrets and ConfigMaps for
S3 buckets and Ceph users created through its CRDs.
But every tool/Helm chart seems to have their own ideas about the env variables
certain things should be stored in. The S3 credential env vars in the case of
Rook&rsquo;s S3 buckets should work in most cases because they&rsquo;re pretty standardized,
but everything else is pretty much hit-and-miss.</p>
<p>And, with the <code>env.valueFrom</code> functionality for both Secrets and ConfigMaps,
Kubernetes already provides the necessary utility to assign specific keys from
them to specific env vars. A number of Helm charts just need to allow me to
make use of that, instead of insisting on Secrets with a specific group of keys.</p>
<p>Anyway, in the case of Secrets, I&rsquo;ve found a pretty roundabout way to achieve
what I want, namely being able to use automatically created credentials.
And I&rsquo;m using my <a href="https://external-secrets.io/latest/">External Secrets</a>
deployment for this, more specifically the ability to configure a Kubernetes
namespace as a SecretStore:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">SecretStore</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">secrets-store</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">provider</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remoteNamespace</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">auth</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">serviceAccount</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">server</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">caProvider</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">ConfigMap</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-root-ca.crt</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">key</span>: <span style="color:#ae81ff">ca.crt</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceAccount</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">rbac.authorization.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Role</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker-role</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">apiGroups</span>: [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">secrets</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">verbs</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">get</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">list</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">watch</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">apiGroups</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">authorization.k8s.io</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">selfsubjectrulesreviews</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">verbs</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">create</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">rbac.authorization.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">RoleBinding</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">roleRef</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">apiGroup</span>: <span style="color:#ae81ff">rbac.authorization.k8s.io</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Role</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker-role</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">subjects</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceAccount</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ext-secrets-woodpecker</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span></code></pre></div><p>This SecretStore then allows me to use External Secret&rsquo;s ExternalSecret
templating to take the CNPG Secret created automatically and bring it into a
format to make it usable with the Woodpecker Helm chart. I decided that I would
use the <code>envFrom.secretRef</code> method to turn all of the Secret&rsquo;s keys into env
variables:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ExternalSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;woodpecker-db-secret&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">secretStoreRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">secrets-store</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">SecretStore</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">refreshInterval</span>: <span style="color:#e6db74">&#34;1h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">creationPolicy</span>: <span style="color:#e6db74">&#39;Owner&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">data</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">secretKey</span>: <span style="color:#ae81ff">WOODPECKER_DATABASE_DATASOURCE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remoteRef</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">key</span>: <span style="color:#ae81ff">woodpecker-pg-cluster-app</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">property</span>: <span style="color:#ae81ff">uri</span>
</span></span></code></pre></div><p>That ExternalSecret takes the <code>uri</code> key from the automatically created CNPG
Secret and writes its content into a new Secret&rsquo;s <code>WOODPECKER_DATABASE_DATASOURCE</code>
key.
And just like that, I have a Secret in the right format to use it with
Woodpecker&rsquo;s Helm chart.</p>
<p>After I implemented the above, I had another thought how I could do the same
thing without taking the detour via ExternalSecret. The Helm chart does provide
options to add extra volume mounts. Furthermore, Woodpecker has the
<code>WOODPECKER_DATABASE_DATASOURCE_FILE</code> variable, which allows reading the
connection string from a file. So I could have mounted the CNPG DB Secret as a
volume and then provided the path to the file with the <code>uri</code> key in this
variable. Sadly I found this a bit late, but I will keep this possibility in
mind should I come across another Helm chart which lacks the possibility
to assign arbitrary Secret keys to env variables.</p>
<h2 id="temporary-storageclass">Temporary StorageClass</h2>
<p>Woodpecker needs some storage for every pipeline executed. That storage is
shared between all steps and is used to clone the repository and share
intermediate artifacts between steps.</p>
<p>With the Kubernetes backend, Woodpecker uses PersistentVolumeClaims, one per
pipeline run. It also automatically cleans those up after the pipeline has run
through.
The issue for me is that in my Rook Ceph setup, the StorageClasses all have their
reclaim policy set to <code>Retain</code>. This is mostly because I&rsquo;m not the smartest guy
under the sun, and there&rsquo;s a real chance that I might accidentally remove a
PVC with data I would really like to keep.
But that&rsquo;s a problem for these temporary PVCs, which are only relevant for the
duration of a single pipeline run. Using my standard StorageClasses would mean
ending up with a lot of unused PersistentVolumes.</p>
<p>So I had to create another StorageClass with the reclaim policy set to <code>Delete</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">storage.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">StorageClass</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">homelab-fs-temp</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">provisioner</span>: <span style="color:#ae81ff">rook-ceph.cephfs.csi.ceph.com</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">reclaimPolicy</span>: <span style="color:#ae81ff">Delete</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">clusterID</span>: <span style="color:#ae81ff">rook-cluster</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fsName</span>: <span style="color:#ae81ff">homelab-fs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pool</span>: <span style="color:#ae81ff">homelab-fs-bulk</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/provisioner-secret-name</span>: <span style="color:#ae81ff">rook-csi-cephfs-provisioner</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/provisioner-secret-namespace</span>: <span style="color:#e6db74">&#34;{{ .Release.Namespace }}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/controller-expand-secret-name</span>: <span style="color:#ae81ff">rook-csi-cephfs-provisioner</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/controller-expand-secret-namespace</span>: <span style="color:#e6db74">&#34;{{ .Release.Namespace }}&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/node-stage-secret-name</span>: <span style="color:#ae81ff">rook-csi-cephfs-node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">csi.storage.k8s.io/node-stage-secret-namespace</span>: <span style="color:#e6db74">&#34;{{ .Release.Namespace }}&#34;</span>
</span></span></code></pre></div><p>This uses CephFS as the provider, because I like those volumes to be RWX capable,
which is not the case for RBD based volumes.</p>
<p>Using this StorageClass, the PersistentVolume is deleted when the PVC is
deleted, freeing the space for the next pipeline run.</p>
<h2 id="gitea-configuration">Gitea configuration</h2>
<p>Because Woodpecker needs access to Gitea, there&rsquo;s some configuration
necessary as well, mainly related to the fact that Woodpecker doesn&rsquo;t have its
own authentication and instead relies on the forge it&rsquo;s connected to.</p>
<p>To begin with, Woodpecker needs to be added as an OAuth2 application. This can
be done by any user, under the <code>https://gitea.example.com/user/settings/applications</code>
URL. The configuration is the same as for any other OAuth2 provider, Woodpecker
needs a client ID and a client secret.</p>
<p>The application can be given any name, and the redirect URL has to be
<code>https://&lt;your-woodpecker-url&gt;/authorize</code>:</p>
<figure>
    <img loading="lazy" src="gitea_add_app.png"
         alt="A screenshot of Gitea&#39;s OAuth2 client app creation form. In the &#39;Application Name&#39; field, it shows &#39;Woodpecker Blog Example&#39;, and in the &#39;Redirect URIs&#39; field, it shows &#39;https://ci.example.com/authorize&#39;. The &#39;Confidential Client&#39; option is enabled."/> <figcaption>
            <p>Gitea&rsquo;s OAuth2 creation form.</p>
        </figcaption>
</figure>

<p>After clicking <em>Create Application</em>, Gitea creates the app and shows the
necessary information:</p>
<figure>
    <img loading="lazy" src="gitea_add_info.png"
         alt="A screenshot of Gitea&#39;s OAuth2 app information screen. It shows the randomly generated &#39;Client ID&#39; and &#39;Client Secret&#39; and allows changing the &#39;Application Name&#39; and &#39;Redirect URIs&#39; fields."/> <figcaption>
            <p>Gitea&rsquo;s OAuth2 information page.</p>
        </figcaption>
</figure>

<p>I then copied the <em>Client ID</em> and <em>Client Secret</em> fields into my Vault instance
and provided them to Kubernetes with another ExternalSecret:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ExternalSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;gitea-secret&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">secretStoreRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">hashi-vault-store</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterSecretStore</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">refreshInterval</span>: <span style="color:#e6db74">&#34;1h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">creationPolicy</span>: <span style="color:#e6db74">&#39;Owner&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">data</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">secretKey</span>: <span style="color:#ae81ff">WOODPECKER_GITEA_CLIENT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remoteRef</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">key</span>: <span style="color:#ae81ff">secret/gitea-oauth</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">property</span>: <span style="color:#ae81ff">clientid</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">secretKey</span>: <span style="color:#ae81ff">WOODPECKER_GITEA_SECRET</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remoteRef</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">key</span>: <span style="color:#ae81ff">secret/gitea-oauth</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">property</span>: <span style="color:#ae81ff">clientSecret</span>
</span></span></code></pre></div><p>That was all the Gitea config necessary. There&rsquo;s going to be one more step
when accessing Woodpecker for the first time. Because it uses OAuth2, it will
redirect you to Gitea to log in, and Gitea will then need confirmation that
Woodpecker can access your account info and repositories.</p>
<h2 id="deploying-woodpecker">Deploying Woodpecker</h2>
<p>For deploying Woodpecker itself, I&rsquo;m using the <a href="https://github.com/woodpecker-ci/helm">official Helm chart</a>.
It&rsquo;s split into two subcharts, one for the agents which run the pipelines and
one for the server. Let&rsquo;s start with the server part of the <code>values.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">server</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">metrics</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_OPEN</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_HOST</span>: <span style="color:#e6db74">&#39;https://ci.example.com&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_DISABLE_USER_AGENT_REGISTRATION</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_DATABASE_DRIVER</span>: <span style="color:#e6db74">&#34;postgres&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_GITEA</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_GITEA_URL</span>: <span style="color:#e6db74">&#34;https://gitea.example.com&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_PLUGINS_PRIVILEGED</span>: <span style="color:#e6db74">&#34;woodpeckerci/plugin-docker-buildx:latest-insecure&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">extraSecretNamesForEnvFrom</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">gitea-secret</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">woodpecker-db-secret</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">persistentVolume</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">rbd-bulk</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">annotations</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">traefik.ingress.kubernetes.io/router.entrypoints</span>: <span style="color:#ae81ff">secureweb</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">hosts</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">host</span>: <span style="color:#ae81ff">ci.example.com</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">100m</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">128Mi</span>
</span></span></code></pre></div><p>As I do so often, I explicitly set <code>metrics.enabled</code> to <code>false</code>, so that later
I can go through my Homelab repo and slowly enable metrics for the apps I&rsquo;m
interested in, just by grepping for <code>metrics</code>.</p>
<p>Woodpecker is entirely configured through environment variables. I&rsquo;ve configured
those which don&rsquo;t contain secrets right in the <code>values.yaml</code>, and the secrets
are added via the <code>extraSecretNamesForEnvFrom</code> list. Those are the Gitea OAuth2
and CNPG DB Secrets. The server itself also needs some storage space, which I
put on my bulk storage pool with the <code>persistentVolume</code> option. I&rsquo;m also
configuring the Ingress and resources.</p>
<p>A short comment on the resources: Make sure that you know what you&rsquo;re doing. &#x1f605;
I initially had the <code>cpu: 100m</code> resource set under <code>limits</code> accidentally. And
then I was wondering yesterday why the Woodpecker server was restarted so often
due to failed liveness probes. Turns out that the <code>100m</code> is not enough CPU
when the Pod happens to run on a Pi 4 and I&rsquo;m also clicking around in the Web UI.
The liveness probe then doesn&rsquo;t get a timely answer and starts failing, ultimately
restarting the Pod.</p>
<p>The second part of a Woodpecker deployment are the agents. Those are the part
of Woodpecker that runs the actual pipelines, launching the containers for each
step. Woodpecker supports multiple backends. The first one is the traditional
Docker backend, which needs the agent to have access to the Docker socket.
That&rsquo;s the config I&rsquo;ve been running up to now with my Drone setup.
The two biggest downsides for me were the fact that a piece of software explicitly
intended to execute arbitrary code would have full access to the host&rsquo;s Docker
daemon.
The second one was that the agent could only run pipelines on its own host, which
meant that it couldn&rsquo;t distribute the different steps in my entire Nomad cluster.</p>
<p>Now, with Woodpecker, I&rsquo;m making use of the <a href="https://woodpecker-ci.org/docs/administration/backends/kubernetes">Kubernetes Backend</a>.
With this backend, the agents themselves only work as an interface to the
k8s API, launching one Pod for each step and creating the PVC used as shared
storage for all steps of a pipeline.</p>
<p>One quirk of the Kubernetes backend is that it adds a NodeSelector to the
architecture of the agent which is launching the pipeline. So when the agent
executing a pipeline happens to be an ARM64 machine, all Pods for that pipeline
will also run on ARM64 machines. But this can be controlled for individual
steps as well.</p>
<p>Here is the agent portion of the Woodpecker Helm <code>values.yaml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">agent</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicaCount</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_BACKEND</span>: <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_MAX_WORKFLOWS</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_BACKEND_K8S_NAMESPACE</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_BACKEND_K8S_VOLUME_SIZE</span>: <span style="color:#ae81ff">10G</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_BACKEND_K8S_STORAGE_CLASS</span>: <span style="color:#ae81ff">homelab-fs-temp</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">WOODPECKER_BACKEND_K8S_STORAGE_RWX</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">persistence</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">rbd-bulk</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">accessModes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">ReadWriteOnce</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceAccount</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">create</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rbasc</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">create</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">100m</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">128Mi</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">topologySpreadConstraints</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">maxSkew</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">topologyKey</span>: <span style="color:#e6db74">&#34;kubernetes.io/arch&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">whenUnsatisfiable</span>: <span style="color:#e6db74">&#34;DoNotSchedule&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labelSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;app.kubernetes.io/name&#34;: </span><span style="color:#ae81ff">agent</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;app.kubernetes.io/instance&#34;: </span><span style="color:#ae81ff">woodpecker</span>
</span></span></code></pre></div><p>Here I&rsquo;m configuring two agents to be run, and one each on a different
architecture. In my cluster, this leads to one agent running on AMD64 and one
running on ARM64, through the <code>topologySpreadConstraints</code>. I&rsquo;m also telling
the agents which StorageClass to use, as I explained above I had to create
a new one with retention disabled. I&rsquo;m setting a default 10 GB size for the
volume.</p>
<p>Before continuing with some CI pipeline configs, let&rsquo;s have a short look at
the Pods Woodpecker launches. I&rsquo;ve captured the Pod for the following Woodpecker
CI step:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">debian</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">echo &#34;This is the build step&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">echo &#34;binary-data-123&#34; &gt; executable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">chmod u+x ./executable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">sleep 120</span>
</span></span></code></pre></div><p>It looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">step</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wp-01jhkac6pf4jyfywavjg6be5cq</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">/bin/sh</span>
</span></span><span style="display:flex;"><span>    - -<span style="color:#ae81ff">c</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">echo $CI_SCRIPT | base64 -d | /bin/sh -e</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_AUTHOR</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">mmeier</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_AUTHOR_AVATAR</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/avatars/d941e68cc8aa38efdee91c3e3c97159e</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_AUTHOR_EMAIL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">mmeier@noreply.gitea.example.com</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_BRANCH</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">master</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_MESSAGE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        Add a sleep to inspect the Pod</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_REF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">refs/heads/master</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_SHA</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">353b9f67102ba120ffe9284aa711eb87c2542573</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_COMMIT_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests/commit/353b9f67102ba120ffe9284aa711eb87c2542573</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_FORGE_TYPE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">gitea</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_FORGE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_MACHINE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">woodpecker-agent-1</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_CREATED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736888948&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_EVENT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_FILES</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#39;[&#34;.woodpecker/my-first-workflow.yaml&#34;]&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_FINISHED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736888960&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_FORGE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests/commit/353b9f67102ba120ffe9284aa711eb87c2542573</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_NUMBER</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;3&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_PARENT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_STARTED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736888951&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_STATUS</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">success</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PIPELINE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://ci.example.com/repos/1/pipeline/3</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_AUTHOR</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">mmeier</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_AUTHOR_AVATAR</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/avatars/d941e68cc8aa38efdee91c3e3c97159e</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_AUTHOR_EMAIL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">mmeier@noreply.gitea.example.com</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_BRANCH</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">master</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_MESSAGE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        Possibly fix permission error</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_REF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">refs/heads/master</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_SHA</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">b680ab9b9a7aa300d80a43bd389de0e57f767e4f</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_COMMIT_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests/commit/b680ab9b9a7aa300d80a43bd389de0e57f767e4f</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_CREATED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736800786&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_EVENT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_FINISHED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736800827&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_FORGE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests/commit/b680ab9b9a7aa300d80a43bd389de0e57f767e4f</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_NUMBER</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_PARENT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_STARTED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736800790&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_STATUS</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">failure</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_PREV_PIPELINE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://ci.example.com/repos/1/pipeline/2</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">adm/ci-tests</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_CLONE_SSH_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">ssh://gituser@git.example.com:1234/adm/ci-tests.git</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_CLONE_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests.git</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_DEFAULT_BRANCH</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">master</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_NAME</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">ci-tests</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_OWNER</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">adm</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_PRIVATE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_REMOTE_ID</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;94&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_SCM</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">git</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_TRUSTED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_REPO_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://gitea.example.com/adm/ci-tests</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_STEP_FINISHED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736888960&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_STEP_NUMBER</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_STEP_STARTED</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1736888951&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_STEP_STATUS</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">success</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_STEP_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://ci.example.com/repos/1/pipeline/3</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SYSTEM_HOST</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">ci.example.com</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SYSTEM_NAME</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">woodpecker</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SYSTEM_PLATFORM</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">linux/amd64</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SYSTEM_URL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">https://ci.example.com</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SYSTEM_VERSION</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">2.8.1</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_WORKFLOW_NAME</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">my-first-workflow</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_WORKFLOW_NUMBER</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_WORKSPACE</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">/woodpecker/src/gitea.example.com/adm/ci-tests</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">HOME</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">/root</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SCRIPT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">CmlmIFsgLW4gIiRDSV9ORVRSQ19NQUNISU5FIiBdOyB0aGVuCmNhdCA8PEVPRiA+ICRIT01FLy5uZXRyYwptYWNoaW5lICRDSV9ORVRSQ19NQUNISU5FCmxvZ2luICRDSV9ORVRSQ19VU0VSTkFNRQpwYXNzd29yZCAkQ0lfTkVUUkNfUEFTU1dPUkQKRU9GCmNobW9kIDA2MDAgJEhPTUUvLm5ldHJjCmZpCnVuc2V0IENJX05FVFJDX1VTRVJOQU1FCnVuc2V0IENJX05FVFJDX1BBU1NXT1JECnVuc2V0IENJX1NDUklQVAoKZWNobyArICdlY2hvICJUaGlzIGlzIHRoZSBidWlsZCBzdGVwIicKZWNobyAiVGhpcyBpcyB0aGUgYnVpbGQgc3RlcCIKCmVjaG8gKyAnZWNobyAiYmluYXJ5LWRhdGEtMTIzIiA+IGV4ZWN1dGFibGUnCmVjaG8gImJpbmFyeS1kYXRhLTEyMyIgPiBleGVjdXRhYmxlCgplY2hvICsgJ2NobW9kIHUreCAuL2V4ZWN1dGFibGUnCmNobW9kIHUreCAuL2V4ZWN1dGFibGUKCmVjaG8gKyAnc2xlZXAgMTIwJwpzbGVlcCAxMjAK</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">SHELL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">/bin/sh</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">debian</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">imagePullPolicy</span>: <span style="color:#ae81ff">Always</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wp-01jhkac6pf4jyfywavjg6be5cq</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">resources</span>: {}
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">terminationMessagePath</span>: <span style="color:#ae81ff">/dev/termination-log</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">terminationMessagePolicy</span>: <span style="color:#ae81ff">File</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumeMounts</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/woodpecker</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wp-01jhkac6pf4jyfywavjasgpcwn-0-default</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">mountPath</span>: <span style="color:#ae81ff">/var/run/secrets/kubernetes.io/serviceaccount</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-api-access-n75dj</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">readOnly</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">workingDir</span>: <span style="color:#ae81ff">/woodpecker/src/gitea.example.com/adm/ci-tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dnsPolicy</span>: <span style="color:#ae81ff">ClusterFirst</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enableServiceLinks</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">imagePullSecrets</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">regcred</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeName</span>: <span style="color:#ae81ff">sehith</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#ae81ff">amd64</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">preemptionPolicy</span>: <span style="color:#ae81ff">PreemptLowerPriority</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">priority</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">restartPolicy</span>: <span style="color:#ae81ff">Never</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedulerName</span>: <span style="color:#ae81ff">default-scheduler</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">securityContext</span>: {}
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceAccount</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceAccountName</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">terminationGracePeriodSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tolerations</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">NoExecute</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">key</span>: <span style="color:#ae81ff">node.kubernetes.io/not-ready</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Exists</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tolerationSeconds</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">NoExecute</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">key</span>: <span style="color:#ae81ff">node.kubernetes.io/unreachable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Exists</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tolerationSeconds</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wp-01jhkac6pf4jyfywavjasgpcwn-0-default</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">wp-01jhkac6pf4jyfywavjasgpcwn-0-default</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-api-access-n75dj</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">projected</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">defaultMode</span>: <span style="color:#ae81ff">420</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">sources</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">serviceAccountToken</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">expirationSeconds</span>: <span style="color:#ae81ff">3607</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">token</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">configMap</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">items</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#ae81ff">ca.crt</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">path</span>: <span style="color:#ae81ff">ca.crt</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">kube-root-ca.crt</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">downwardAPI</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">items</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">fieldRef</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">fieldPath</span>: <span style="color:#ae81ff">metadata.namespace</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">path</span>: <span style="color:#ae81ff">namespace</span>
</span></span></code></pre></div><p>There are a number of noteworthy things in here. First perhaps the handling of
the script to execute for the job:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  - <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">/bin/sh</span>
</span></span><span style="display:flex;"><span>    - -<span style="color:#ae81ff">c</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">echo $CI_SCRIPT | base64 -d | /bin/sh -e</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">CI_SCRIPT</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">CmlmIFsgLW4gIiRDSV9ORVRSQ19NQUNISU5FIiBdOyB0aGVuCmNhdCA8PEVPRiA+ICRIT01FLy5uZXRyYwptYWNoaW5lICRDSV9ORVRSQ19NQUNISU5FCmxvZ2luICRDSV9ORVRSQ19VU0VSTkFNRQpwYXNzd29yZCAkQ0lfTkVUUkNfUEFTU1dPUkQKRU9GCmNobW9kIDA2MDAgJEhPTUUvLm5ldHJjCmZpCnVuc2V0IENJX05FVFJDX1VTRVJOQU1FCnVuc2V0IENJX05FVFJDX1BBU1NXT1JECnVuc2V0IENJX1NDUklQVAoKZWNobyArICdlY2hvICJUaGlzIGlzIHRoZSBidWlsZCBzdGVwIicKZWNobyAiVGhpcyBpcyB0aGUgYnVpbGQgc3RlcCIKCmVjaG8gKyAnZWNobyAiYmluYXJ5LWRhdGEtMTIzIiA+IGV4ZWN1dGFibGUnCmVjaG8gImJpbmFyeS1kYXRhLTEyMyIgPiBleGVjdXRhYmxlCgplY2hvICsgJ2NobW9kIHUreCAuL2V4ZWN1dGFibGUnCmNobW9kIHUreCAuL2V4ZWN1dGFibGUKCmVjaG8gKyAnc2xlZXAgMTIwJwpzbGVlcCAxMjAK</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">SHELL</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">value</span>: <span style="color:#ae81ff">/bin/sh</span>
</span></span></code></pre></div><p>Running the <code>CI_SCRIPT</code> content through <code>base64 -d</code> results in this shell script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -n <span style="color:#e6db74">&#34;</span>$CI_NETRC_MACHINE<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>cat <span style="color:#e6db74">&lt;&lt;EOF &gt; $HOME/.netrc
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">machine $CI_NETRC_MACHINE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">login $CI_NETRC_USERNAME
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">password $CI_NETRC_PASSWORD
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">0600</span> $HOME/.netrc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>unset CI_NETRC_USERNAME
</span></span><span style="display:flex;"><span>unset CI_NETRC_PASSWORD
</span></span><span style="display:flex;"><span>unset CI_SCRIPT
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo + <span style="color:#e6db74">&#39;echo &#34;This is the build step&#34;&#39;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;This is the build step&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo + <span style="color:#e6db74">&#39;echo &#34;binary-data-123&#34; &gt; executable&#39;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;binary-data-123&#34;</span> &gt; executable
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo + <span style="color:#e6db74">&#39;chmod u+x ./executable&#39;</span>
</span></span><span style="display:flex;"><span>chmod u+x ./executable
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo + <span style="color:#e6db74">&#39;sleep 120&#39;</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">120</span>
</span></span></code></pre></div><p>This shows that the commands from the <code>commands:</code> list from the <code>step</code> object
in the Woodpecker file is converted into a shell script by copying the commands
into the script and adding an <code>echo</code> for each of them.</p>
<p>Looking at this and thinking about my own work on a large CI I&rsquo;m sometimes
wondering what we&rsquo;d do without the <code>base64</code> command. &#x1f605;</p>
<p>Another aspect of the setup is all of the available environment variables,
supplying a lot of information not just on the commit currently being CI tested,
but also the previous commit. Most of the <code>CI_</code> variables also have equivalents
prefixed with <code>DRONE_</code>, for backwards compatibility. I removed them in the
output above to not make the snippet too long.</p>
<p>Finally there&rsquo;s proof of what I said above about the agent&rsquo;s architecture. This
pipeline was run by the agent on my AMD64 node, resulting in the NodeSelector for
AMD64 nodes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  <span style="color:#f92672">nodeName</span>: <span style="color:#ae81ff">sehith</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kubernetes.io/arch</span>: <span style="color:#ae81ff">amd64</span>
</span></span></code></pre></div><p>Also nice to see that the Pod was running on <code>sehith</code>, which isn&rsquo;t the node the
agent ran on, showing that the Pods are just submitted for scheduling to k8s,
being able to run on any (AMD64 in this case) node.</p>
<p>Before ending the post, let&rsquo;s have a look at some example CI configurations.</p>
<h2 id="ci-configurations">CI configurations</h2>
<p>Each repository using Woodpecker needs to be enabled. This is done from
Woodpecker&rsquo;s web UI:
<figure>
    <img loading="lazy" src="enable_repo.png"
         alt="A screenshot of Woodpecker&#39;s repo enabling UI. IT shows a search field at the top and a list of repositories at the bottom. Some of them have a label saying &#39;Already enabled&#39;, while others have an &#39;Enable&#39; button next to them."/> <figcaption>
            <p>Woodpecker&rsquo;s repo addition UI.</p>
        </figcaption>
</figure>

When clicking the <em>Enable</em> button, Woodpecker will contact Gitea and add a
webhook configuration for the repository. With that, Gitea will call the
webhook with information about the event which triggered it and the state of
the repository.</p>
<p>The Woodpecker configuration files for a specific repository are expected in
the <code>.woodpecker/</code> directory at the repository root by default.</p>
<h3 id="blog-repo-example">Blog repo example</h3>
<p>Here&rsquo;s the configuration I&rsquo;m using to build and publish this blog:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo Site Build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#34;harbor.mei-home.net/homelab/hugo:0.125.4-r3&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">hugo</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Missing alt text check</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">python:3</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pip install lxml beautifulsoup4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">python3 scripts/alt_text.py ./public/posts/</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo Site Upload</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#34;harbor.mei-home.net/homelab/hugo:latest&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">AWS_ACCESS_KEY_ID</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">access-key</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">AWS_SECRET_ACCESS_KEY</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">secret-key</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">s3cmd -c /s3cmd.conf sync -r --delete-removed --delete-after --no-mime-magic ./public/ s3://blog/</span>
</span></span></code></pre></div><p>To start with, the page needs to be build, using Hugo in an image I build
myself, based on Alpine with a couple of tools installed. Then I&rsquo;m running
a short Python script which uses <a href="https://pypi.org/project/beautifulsoup4/">beautifulsoup4</a>
to scan through the generated HTML and make sure that each image has alt text,
and that there&rsquo;s actually something in that alt text. Finally, I push the
generated site up to an S3 bucket in my Ceph cluster from where it is served.</p>
<p>The <code>when:</code> at the beginning is important, it determines under which
conditions the pipeline is executed. This can be configured for specific
branches or certain events, like a push or an update of a pull request.
The different conditions can also be combined. In addition to configuring
conditions on the entire pipeline, they can also be configured just on
certain steps, as we will see later.</p>
<p>One thing I find a little bit lacking at the moment, specifically for the
Kubernetes use case, is the secrets management. It&rsquo;s currently only possible
via the web UI or the CLI. There&rsquo;s no way to provide specific Kubernetes Secrets to
certain steps in a certain pipeline. But there is an open issue to implement
support for Kubernetes Secrets <a href="https://github.com/woodpecker-ci/woodpecker/issues/3582">on Github</a>.
Until that is implemented, the UI needs to be used. It looks like this:
<figure>
    <img loading="lazy" src="secrets.png"
         alt="A screenshot of Woodpecker&#39;s secret configuration UI. It contains a field for a name for the secret and values. In addition, it can be made available only for certain images used in steps. Furthermore, the secret can be restricted to certain events triggering a pipeline run, e.g. only Pushes or Tags or Pull Requests."/> <figcaption>
            <p>Woodpecker&rsquo;s secret addition UI.</p>
        </figcaption>
</figure>

Secrets can be configured for specific repositories, specific orgs where the
forge supports them and for all pipelines.</p>
<p>When looking at a specific repository, all of the pipelines which ran for it
are listed:
<figure>
    <img loading="lazy" src="pipeline_list.png"
         alt="A screenshot of the pipeline list for the mmeier/blog repository. It shows for pipelines. The first one is called &#39;CI: Migrate to Woodpecker&#39; and the most recent one &#39;Publish post on hl-backup-operator deployment&#39;. All of them show that they were pushed directly to the Master branch and took about 1 - 2 minutes each. Each pipeline also shows the Git SHA1 of the commit it tested."/> <figcaption>
            <p>Woodpecker&rsquo;s pipeline list for my blog repo.</p>
        </figcaption>
</figure>

This gives a nice overview of the pipelines which ran recently, here with the
example of my blog repository, including the most recent run for publishing
the post on the backup operator deployment.</p>
<p>Clicking on one of the pipeline runs then shows the overview of that pipeline&rsquo;s
steps and the step logs:
<figure>
    <img loading="lazy" src="pipeline_example.png"
         alt="A screenshot of the pipeline run publishing the hl-backup-operator blog article. At the top right is the subject line of the commit message triggering the pipeline again, &#39;Publish post on hl-backup-operator deployment&#39;. On the left is a list of the steps, showing &#39;clone&#39;, &#39;Hugo Site Build&#39;, &#39;Missing alt text check&#39;, &#39;Hugo Site Upload&#39;. The &#39;Hugo Site Build&#39; step is highlighted, and the logs for that step, showing Hugo&#39;s build output, are shown on the right side."/> <figcaption>
            <p>Woodpecker&rsquo;s pipeline view.</p>
        </figcaption>
</figure>
</p>
<p>This pipeline is not very complex and runs through in about two minutes. So
let&rsquo;s have a look at another pipeline with a bit more complexity.</p>
<h3 id="docker-repo-example">Docker repo example</h3>
<p>Another repository where I&rsquo;m making quite some use of CI is my Docker repository.
In that repo, I&rsquo;ve got a couple of Dockerfiles for cases where I&rsquo;m adding something
to upstream images or building my own where no upstream container is available.</p>
<p>This repository&rsquo;s CI is a bit more complicated mostly because it does the same
thing for multiple different Docker files, and because it needs to do different
things for pull requests and commits pushed to the Master branch.</p>
<p>And that&rsquo;s where the problems begin, at least to a certain extend. As I&rsquo;ve shown
above, you can provide a <code>when</code> config to tell Woodpecker under which conditions
to run the pipeline. And if you leave that out completely, you don&rsquo;t end up
with the pipeline being run for all commits. No. You end up with the pipeline being
run twice for some commits.</p>
<p>Consider, for example, this configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">woodpeckerci/plugin-docker-buildx:latest-insecure</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;&lt;</span>: <span style="color:#75715e">*dockerx-config</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dry-run</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">pull_request</span>
</span></span></code></pre></div><p>Ignore the config under settings, and concentrate on the fact that there&rsquo;s no
<code>when</code> config on the pipeline level, only on the step level. And there&rsquo;s only
one step, that&rsquo;s supposed to run on pull requests. The result of this config
is that two pipelines will be started - including Pod launches, PVC creation
and so on:
<figure>
    <img loading="lazy" src="doubled_pipelines.png"
         alt="A screenshot of Woodpecker showing two pipelines. One failed, one successful. Both show being run for the same commit. One shows that it was launched by a push event to the &#39;woodpecker-ci&#39; branch and the other that it was pushed to pull request 77."/> <figcaption>
            <p>The two pipelines started for the previous configuration, both for the same commit.</p>
        </figcaption>
</figure>

The pipeline <em>#1</em> was launched for the &ldquo;push&rdquo; event to the <em>woodpecker-ci</em> branch,
and the other for the update of the pull request that push belonged to. The push
event pipeline only launched the <em>clone</em> step, while the pull request pipeline
launched the <em>build image</em> step and the clone step.</p>
<p>The root cause for this behavior is that Gitea always triggers the webhook for
all fitting events, one for each event. And consequently, Woodpecker then
launches one pipeline for each event.</p>
<p>A similar effect can be observed when combining both, pull requests and push
events in one <code>when</code> clause on the pipeline level.</p>
<p>Now, you might be saying: Okay, then just configure the triggers only on the
steps, not on the entire pipeline. But that also doesn&rsquo;t really work. Without
a <code>when</code> clause, as shown above, two pipelines are always started for commits
in pull requests. And even though one of the pipelines won&rsquo;t do much, it would
still do something. In my case, it would launch a Pod for the clone step and
also create a PVC and clone the repo - for nothing.</p>
<p>The next idea I came up with: Okay, then let&rsquo;s set the pipeline&rsquo;s <code>when</code> to trigger
on push events, because that would trigger the pipeline for both - pushes to
branches like master and pushes to pull requests. And then just add <code>when</code>
clauses to each step with either the pull request or push event, depending on
when it is supposed to run.
But that also won&rsquo;t work - any given pipeline only ever sees one event. If I
trigger on push events on the pipeline level, the steps triggering on the
pull request event will never trigger.</p>
<p>I finally figured out a way to do this. I always trigger the pipeline on push
events. And then I use Woodpecker&rsquo;s <a href="https://woodpecker-ci.org/docs/usage/workflow-syntax#evaluate">evaluate clause</a>
to trigger only on certain branches.</p>
<p>With all of that said, this is what the config is looking like for the pipeline
which builds my Hugo container:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">event</span>: <span style="color:#ae81ff">push</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;.woodpecker/hugo.yaml&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;hugo/*&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">variables</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;alpine-version</span> <span style="color:#e6db74">&#39;3.21.2&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;app-version</span> <span style="color:#e6db74">&#39;0.139.0-r0&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#75715e">&amp;dockerx-config</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">debug</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">harbor.example.com/homelab/hugo</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">registry</span>: <span style="color:#ae81ff">harbor.example.com</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">username</span>: <span style="color:#ae81ff">ci</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">password</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">container-registry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dockerfile</span>: <span style="color:#ae81ff">hugo/Dockerfile</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">context</span>: <span style="color:#ae81ff">hugo/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">mirror</span>: <span style="color:#ae81ff">https://harbor-mirror.example.com</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">buildkit_config</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      debug = true
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      [registry.&#34;docker.io&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        mirrors = [&#34;harbor.example.com/dockerhub-cache&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      [registry.&#34;quay.io&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        mirrors = [&#34;harbor.example.com/quay.io-cache&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      [registry.&#34;ghcr.io&#34;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        mirrors = [&#34;harbor.example.com/github-cache&#34;]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">latest</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#75715e">*app-version</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build_args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">hugo_ver</span>: <span style="color:#75715e">*app-version</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alpine_ver</span>: <span style="color:#75715e">*alpine-version</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">platforms</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;linux/amd64&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;linux/arm64&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">build image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">woodpeckerci/plugin-docker-buildx:latest-insecure</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;&lt;</span>: <span style="color:#75715e">*dockerx-config</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dry-run</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH != CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">release image</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">woodpeckerci/plugin-docker-buildx:latest-insecure</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">settings</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;&lt;</span>: <span style="color:#75715e">*dockerx-config</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dry-run</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">when</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">evaluate</span>: <span style="color:#e6db74">&#39;CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH&#39;</span>
</span></span></code></pre></div><p>First, what does this pipeline do for pull requests and main branch pushes?
For pull requests, it uses the <a href="https://woodpecker-ci.org/plugins/Docker%20Buildx">buildx plugin</a>
to build a Docker container from the directory <code>hugo/Dockerfile</code> in the repository.
That&rsquo;s what happens in the <em>build image</em> step. Notably, no push to a registry
happens here.
In the case of pushes to the repo&rsquo;s default branch, which is provided by Gitea
in the webhook call, the same plugin and build is used, but this time the
newly build images are pushed to my Harbor registry. For more details on that
setup, <a href="https://blog.mei-home.net/posts/k8s-migration-11-harbor/">see this post</a>.</p>
<p>In the <code>when</code> clause for the pipeline, as I&rsquo;ve explained above, I&rsquo;m triggering
on the push event, to circumvent the problem with multiple pipelines being
executed for commits in pull requests.
In addition, I&rsquo;m also making use of path-based triggers. Because I&rsquo;ve got multiple
container images defined in one repository, I&rsquo;d like to avoid running the builds
for images which haven&rsquo;t changed unnecessarily. That&rsquo;s done by triggering the
pipeline only on changes in its own config file and changes in the <code>hugo/</code> directory.
So if the Hugo image definition and CI config haven&rsquo;t changed, the pipeline won&rsquo;t
be triggered.</p>
<p>As you can see I&rsquo;m building images for both, AMD64 and ARM64. And before I
close this section, I have to tell a slightly embarrassing story. I initially
tried to run two pipelines - one for each architecture, so that they could
both run in parallel on different nodes fitting their architecture. This would
avoid the cost of emulating a foreign architecture, making the builds faster
overall.
This seemed like an excellent idea. And it worked really, really well. The
pipelines got a couple of minutes faster. Until I had a look at my Harbor
instance. And as some of you might have already figured out, I found that of
course there was not one tag with images for both architectures.
Instead, the tag contained whatever pipeline finished last. Because of course,
two different Docker pushes override each other, instead of doing a merge.
This is a problem I need to have another look at later. Someone on the Fediverse
already showed me that there is a multistep way to do this manually.</p>
<p>Another point that I still need to improve is image caching. I think that there&rsquo;s
still some potential for optimization in my setup. But that&rsquo;s also something for
after the k8s migration is done.</p>
<p>Before I close out this section, I would like to point out a pretty nice feature
Woodpecker has: A linter for the pipeline definitions, for example like this:</p>
<figure>
    <img loading="lazy" src="linter.png"
         alt="A screenshot of Woodpecker&#39;s linter output. It shows a number of issues with the pipeline config. For example that steps.1.environment and steps.2.environment are both of an invalid type. It expected an array, but got a null. And for all of the steps it outputs a &#39;bad_habit&#39; warning about the fact that neither the pipeline nor any of the steps have a &#39;when&#39; clause."/> <figcaption>
            <p>Example output of Woodpecker&rsquo;s config linter.</p>
        </figcaption>
</figure>

<p>The configuration spitting out those warnings is this one for my blog:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">submodules</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">alpine/git</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">git submodule update --init --recursive</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo Site Build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#34;harbor.example.com/homelab/hugo:0.125.4-r3&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">hugo</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Missing alt text check</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">python:3</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pip install lxml beautifulsoup4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">python3 scripts/alt_text.py ./public/posts/</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Hugo Site Upload</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#34;harbor.example.com/homelab/hugo:latest&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">AWS_ACCESS_KEY_ID</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">access-key</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">AWS_SECRET_ACCESS_KEY</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">from_secret</span>: <span style="color:#ae81ff">secret-key</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">commands</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">s3cmd -c /s3cmd.conf sync -r --delete-removed --delete-after --no-mime-magic ./public/ s3://blog/</span>
</span></span></code></pre></div><p>The main issues are the empty <code>environment</code> keys, as well as the fact that I did
not set any <code>when</code> clause.</p>
<h2 id="conclusion">Conclusion</h2>
<p>And that&rsquo;s it. Again a pretty long one, but I had never written about my CI setup
and wanted to take this chance to do so, also because I had gotten some
questions on the Fediverse from people what a CI actually does, and some interest
in what Woodpecker looks like.</p>
<p>Oh and also, I just have a propensity for long-winded writing. &#x1f605;</p>
<p>With this post, the Woodpecker/CI migration to k8s is done, and I&rsquo;m quite happy
with it. Especially the fact that my CI pipeline steps now get distributed over
the entire cluster instead of just running on the nodes with the agents.</p>
<p>For the next step I will likely take my Gitea instance and migrate it over, but
as this blog post took longer than I thought it would, it might have to wait
until next weekend.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
