<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mucheru  Maina</title>
    <description>The latest articles on DEV Community by Mucheru  Maina (@mucheru).</description>
    <link>https://dev.to/mucheru</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F615805%2Ffbc6b3ab-6c89-4947-b0cd-41fe58b8cb45.jpeg</url>
      <title>DEV Community: Mucheru  Maina</title>
      <link>https://dev.to/mucheru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mucheru"/>
    <language>en</language>
    <item>
      <title>Getting Started on MOCO Part 2: Backup and Restore to MinIO (and the davfs2 Bug That Broke It)</title>
      <dc:creator>Mucheru  Maina</dc:creator>
      <pubDate>Fri, 31 Jul 2026 12:39:22 +0000</pubDate>
      <link>https://dev.to/mucheru/getting-started-on-moco-part-2-backup-and-restore-to-minio-and-the-davfs2-bug-that-broke-it-31il</link>
      <guid>https://dev.to/mucheru/getting-started-on-moco-part-2-backup-and-restore-to-minio-and-the-davfs2-bug-that-broke-it-31il</guid>
      <description>&lt;p&gt;A 74GB dump finished in 1h23m without a single warning. Then the upload to MinIO died with &lt;code&gt;IncompleteBody&lt;/code&gt;, every time, on every retry. Not once did the dump itself fail. It was always the same spot, right after "Dumping data - done."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/getting-started-on-moco-the-mysql-operator-for-kubernetes-part-1-3kc7"&gt;Part 1&lt;/a&gt; covered what MOCO is and how to stand up a cluster. This part covers wiring that cluster to a real backup target: MinIO backed by a Hetzner storagebox, and the bug that took down backups on two production clusters at once until we found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing up MinIO on the storagebox
&lt;/h2&gt;

&lt;p&gt;We don't run MinIO against local disk, and we don't run the official MinIO Helm chart or Operator either. It's a plain Kubernetes &lt;code&gt;Deployment&lt;/code&gt; running a custom image: Ubuntu base, with &lt;code&gt;davfs2&lt;/code&gt;, the &lt;code&gt;minio&lt;/code&gt; server binary, and the &lt;code&gt;mc&lt;/code&gt; client all installed via a &lt;code&gt;Dockerfile&lt;/code&gt;, plus an entrypoint script that mounts the Hetzner storagebox over WebDAV/FUSE before starting the MinIO server on top of that mount. The image lives in a private registry, referenced in the Deployment spec like any other container image.&lt;/p&gt;

&lt;p&gt;That custom-image choice is the whole reason the davfs2 bug below exists in the first place. The official MinIO Operator expects to own real block storage per pod. Ours hands MinIO a FUSE-mounted network filesystem instead, and FUSE doesn't know or care about MinIO's multipart upload semantics sitting on top of it.&lt;/p&gt;

&lt;p&gt;Stripped down, the &lt;code&gt;Dockerfile&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:latest&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; davfs2 ca-certificates wget &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;wget https://dl.min.io/server/minio/release/linux-amd64/minio &lt;span class="nt"&gt;-O&lt;/span&gt; /usr/bin/minio &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/bin/minio

&lt;span class="k"&gt;RUN &lt;/span&gt;wget https://dl.min.io/client/mc/release/linux-amd64/mc &lt;span class="nt"&gt;-O&lt;/span&gt; /usr/bin/mc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/bin/mc

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; mount-webdav.sh /mount-webdav.sh&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /mount-webdav.sh
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/mount-webdav.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mount-webdav.sh&lt;/code&gt; does the actual work at container startup: mounts the storagebox over WebDAV with davfs2, then execs the MinIO server against that mount point. Note there's no version pinning on either the &lt;code&gt;minio&lt;/code&gt; or &lt;code&gt;mc&lt;/code&gt; binary, so a rebuild today pulls whatever's current at &lt;code&gt;dl.min.io&lt;/code&gt; that day, not necessarily what was running yesterday. That's a separate footgun from the one below, but it's bitten us too.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt;, the entrypoint script, and the davfs2 tuning from the incident below now all live in one place: &lt;a href="https://github.com/muchezz/minio-mounter" rel="noopener noreferrer"&gt;github.com/muchezz/minio-mounter&lt;/a&gt;. If you're running MOCO against a Hetzner storagebox (or any other WebDAV-only backend) rather than building your own version of this from scratch, start there. The image built from that repo is what the Deployment below actually references.&lt;/p&gt;

&lt;p&gt;One caveat before the manifests: we manage our whole fleet with Ansible, so that's what deploys this custom image here. MOCO doesn't care how MinIO gets onto the cluster. The Helm chart, the official MinIO Operator, a raw &lt;code&gt;StatefulSet&lt;/code&gt;, any of it works the same from MOCO's side, since all that matters to a &lt;code&gt;BackupPolicy&lt;/code&gt; is the resulting S3-compatible endpoint. Swap the Ansible role below for whatever your team already uses to deploy workloads.&lt;/p&gt;

&lt;p&gt;Deployment is an Ansible role. Add a &lt;code&gt;minio_storagebox&lt;/code&gt; block to the k8s variables file, for example &lt;code&gt;group_vars/k8s_cluster/k8s_cluster.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;minio_storagebox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storagebox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;u000000"&lt;/span&gt; &lt;span class="c1"&gt;# storagebox where to keep the data&lt;/span&gt;
  &lt;span class="na"&gt;storagebox_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vault_storagebox.u000000&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt; &lt;span class="c1"&gt;# storagebox password&lt;/span&gt;
  &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minio-data"&lt;/span&gt; &lt;span class="c1"&gt;# folder where data is stored&lt;/span&gt;
  &lt;span class="na"&gt;minio_root_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vault_relevant_var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt; &lt;span class="c1"&gt;# root password from minio&lt;/span&gt;
  &lt;span class="na"&gt;minio_kms_secret_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vault_relevant_var&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt; &lt;span class="c1"&gt;# encryption key for at-rest encryption. Generate with `openssl rand -base64 32`, then set as encryption-key:generated_secret_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensitive values go in the vault, not in plain group_vars. The custom mounter image also needs a pull token, which lives in &lt;code&gt;defaults/main.yml:dockerconfigjson_b64&lt;/code&gt;. If it ever expires, that's a separate runbook (renewing GitLab deploy tokens on k8s).&lt;/p&gt;

&lt;p&gt;The role's tasks do five things, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-create the remote directory&lt;/strong&gt; over SFTP, delegated to localhost, before anything touches Kubernetes. WebDAV mounting a directory that doesn't exist yet on the storagebox fails, so this runs first and is allowed to fail quietly if the directory's already there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the namespace&lt;/strong&gt;, &lt;code&gt;minio-storagebox&lt;/code&gt; by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a secret&lt;/strong&gt; holding &lt;code&gt;minio_root_user&lt;/code&gt;, &lt;code&gt;minio_root_password&lt;/code&gt;, &lt;code&gt;minio_kms_secret_key&lt;/code&gt;, and a combined &lt;code&gt;secrets&lt;/code&gt; string (the WebDAV URL plus storagebox credentials, which the mount script reads on startup).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a &lt;code&gt;dockerconfigjson&lt;/code&gt; pull secret&lt;/strong&gt; named &lt;code&gt;regcred&lt;/code&gt;, since the mounter image sits in a private registry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create the &lt;code&gt;Deployment&lt;/code&gt; and &lt;code&gt;Service&lt;/code&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Deployment template is the part that actually matters for how MinIO ends up sitting on FUSE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-deployment"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RollingUpdate&lt;/span&gt;
    &lt;span class="na"&gt;rollingUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;maxSurge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minioclient&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/muchezz/minio-mounter:latest&lt;/span&gt;
          &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WEBDAV_URL&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;webdav_url&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MOUNT_POINT&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MINIO_ROOT_USER&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secretname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio_root_user&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MINIO_ROOT_PASSWORD&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secretname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio_root_password&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MINIO_KMS_SECRET_KEY&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secretname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio_kms_secret_key&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/data&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/secrets&lt;/span&gt;
              &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fuse&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/fuse&lt;/span&gt;
      &lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regcred&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;data&lt;/span&gt;
          &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets&lt;/span&gt;
          &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;secretname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fuse&lt;/span&gt;
          &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/fuse&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines here are load-bearing and easy to miss on a skim: &lt;code&gt;securityContext.privileged: true&lt;/code&gt;, and the &lt;code&gt;fuse&lt;/code&gt; volume mounting the node's &lt;code&gt;/dev/fuse&lt;/code&gt; device straight into the container via &lt;code&gt;hostPath&lt;/code&gt;. davfs2 mounts a FUSE filesystem, and FUSE needs access to that device node plus enough privilege to actually create the mount from inside the container. Without both, the entrypoint script's &lt;code&gt;mount -t davfs&lt;/code&gt; call just fails at startup. &lt;code&gt;/data&lt;/code&gt; itself is a plain &lt;code&gt;emptyDir&lt;/code&gt;; nothing is persisted there directly, it's the mount point the WebDAV filesystem gets attached to once the container starts.&lt;/p&gt;

&lt;p&gt;The Service is unremarkable by comparison, just the S3 API port and the console port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-service"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9000&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9001&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9001&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;console&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 80 on the Service forwards to MinIO's S3 API on 9000 inside the pod, which is why the &lt;code&gt;endpoint_url&lt;/code&gt; MOCO talks to later is just &lt;code&gt;http://minio-storagebox-service.minio-storagebox.svc&lt;/code&gt; with no port. Console traffic on 9001 stays separate, and we only ever reach it through a port-forward, never a Service exposed outside the cluster.&lt;/p&gt;

&lt;p&gt;Add the role to the relevant playbook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;k8s_cluster&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linear&lt;/span&gt;
  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;some/other-role&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;some-other-tag&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;k8s/minio-storagebox&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;k8s-minio-storagebox&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it against the target cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook k8s_cluster.yml &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.k8s_cluster &lt;span class="nt"&gt;-t&lt;/span&gt; k8s-minio-storagebox &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That deploys MinIO. Next, create the bucket MOCO will actually write to. Port-forward the console instead of exposing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; minio-storagebox port-forward svc/minio-storagebox-service 9001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in with &lt;code&gt;minio&lt;/code&gt; / the root password from the vault, create a bucket, and turn on encryption: SSE-KMS with the preconfigured &lt;code&gt;encryption-key&lt;/code&gt;. Once that's done, the bucket behaves like any S3-compatible target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;endpoint_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://minio-storagebox-service.minio-storagebox.svc&lt;/span&gt;
&lt;span class="na"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio&lt;/span&gt;
&lt;span class="na"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio_root_password&lt;/span&gt;
&lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;whateveryoucreated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pointing MOCO at the bucket
&lt;/h2&gt;

&lt;p&gt;MOCO doesn't take a bucket config on the cluster spec directly. It goes through a &lt;code&gt;BackupPolicy&lt;/code&gt; CR, referenced from &lt;code&gt;MySQLCluster.spec.backupPolicyName&lt;/code&gt;. One BackupPolicy can be shared across multiple clusters, since MOCO prefixes object keys with &lt;code&gt;moco/&lt;/code&gt; so they don't collide.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;moco.cybozu.com/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BackupPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;jobConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backup-owner&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;
        &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio-backup-creds&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-access-key&lt;/span&gt;
    &lt;span class="na"&gt;bucketConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db-backups&lt;/span&gt;
      &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
      &lt;span class="na"&gt;endpointURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://minio-storagebox-service.minio-storagebox.svc&lt;/span&gt;
      &lt;span class="na"&gt;usePathStyle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;usePathStyle: true&lt;/code&gt; matters here. MinIO expects &lt;code&gt;endpoint/bucket/key&lt;/code&gt;, not the virtual-hosted &lt;code&gt;bucket.endpoint/key&lt;/code&gt; style S3 defaults to, and skipping that flag means every request 404s before it even gets to the davfs2 layer.&lt;/p&gt;

&lt;p&gt;Reference the policy from the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backupPolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;moco-controller&lt;/code&gt; turns that into a &lt;code&gt;CronJob&lt;/code&gt;. Each run dumps the instance with MySQL Shell's dump utility, tars it, pushes it to the bucket, then separately ships binlogs since the last backup for point-in-time recovery. Two objects per run, not one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restoring
&lt;/h2&gt;

&lt;p&gt;Restore isn't a flag on the existing cluster. You stand up a new &lt;code&gt;MySQLCluster&lt;/code&gt; with a &lt;code&gt;spec.restore&lt;/code&gt; block pointing at where the old backup lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;moco.cybozu.com/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MySQLCluster&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db-restored&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;restore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;sourceNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db&lt;/span&gt;
    &lt;span class="na"&gt;sourceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db&lt;/span&gt;
    &lt;span class="na"&gt;restorePoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-28T02:15:00Z"&lt;/span&gt;
    &lt;span class="na"&gt;jobConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backup-owner&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;
          &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minio-backup-creds&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-access-key&lt;/span&gt;
      &lt;span class="na"&gt;bucketConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;bucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-db-backups&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
        &lt;span class="na"&gt;endpointURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://minio-storagebox-service.minio-storagebox.svc&lt;/span&gt;
        &lt;span class="na"&gt;usePathStyle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;workVolume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sourceNamespace&lt;/code&gt;/&lt;code&gt;sourceName&lt;/code&gt; don't need the original cluster to still exist. They're just how MOCO locates the right prefix in the bucket. &lt;code&gt;restorePoint&lt;/code&gt; in RFC3339 gives you PITR: MOCO restores the last full backup before that timestamp, then replays binlogs up to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upload that never finished
&lt;/h2&gt;

&lt;p&gt;Backups ran clean for weeks. Then &lt;code&gt;app-db&lt;/code&gt;'s nightly job started failing at the same point every night: dump completes, upload starts, upload dies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IncompleteBody: The request body terminated unexpectedly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same error, same stage, every retry. That ruled out a one-off network blip; something structural was cutting the stream short partway through the multipart upload.&lt;/p&gt;

&lt;p&gt;The mounter pod runs davfs2 with defaults, and nobody had ever had a reason to look at them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; minio-storagebox &amp;lt;minio-mounter-pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/davfs2/davfs2.conf | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'cache_size|buf_size|delay_upload'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;buf_size&lt;/span&gt;        &lt;span class="err"&gt;16&lt;/span&gt;                 &lt;span class="c"&gt;# KiByte  (commented out, default)
&lt;/span&gt;&lt;span class="err"&gt;cache_size&lt;/span&gt;      &lt;span class="err"&gt;50&lt;/span&gt;                &lt;span class="c"&gt;# MiByte  (commented out, default)
&lt;/span&gt;&lt;span class="err"&gt;delay_upload&lt;/span&gt;    &lt;span class="err"&gt;10&lt;/span&gt;                &lt;span class="err"&gt;(commented&lt;/span&gt; &lt;span class="err"&gt;out,&lt;/span&gt; &lt;span class="err"&gt;default)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three commented out, all three running on the default. A 50MB cache trying to buffer a 74GB multipart upload. MOCO's backup client declares a &lt;code&gt;Content-Length&lt;/code&gt; per part; davfs2 was flushing its cache mid-part and handing MinIO fewer bytes than promised. MinIO had no choice but to reject it.&lt;/p&gt;

&lt;p&gt;Fix, applied live inside the running pod first to confirm it worked before baking it into the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; minio-storagebox &amp;lt;minio-mounter-pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
cat &amp;gt;&amp;gt; /etc/davfs2/davfs2.conf &amp;lt;&amp;lt; EOF

cache_size      4096
buf_size        64
delay_upload    0
use_locks       0
EOF
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;davfs2 only reads its config at mount time, so the change doesn't do anything until you remount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; minio-storagebox &amp;lt;minio-mounter-pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
umount -l /data &amp;amp;&amp;amp; sleep 5 &amp;amp;&amp;amp; mount -t davfs $WEBDAV_URL /data
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clear the incomplete upload MinIO was still holding onto, then re-run the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; minio-storagebox &amp;lt;minio-mounter-pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  mc &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--incomplete&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt;/backups/moco/app-db/

kubectl create job moco-backup-app-db-manual-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cronjob/moco-backup-app-db &lt;span class="nt"&gt;-n&lt;/span&gt; app-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 74.1GB dump, same duration, but this time the upload went through. &lt;code&gt;delay_upload 0&lt;/code&gt; turned out to matter almost as much as the bigger cache. With the default 10-second delay, davfs2 was batching writes in a way that made the mid-stream flush worse, not better, once the upload actually got large.&lt;/p&gt;

&lt;p&gt;That fix lived in the running pod, which meant it wouldn't survive a restart. The permanent version went into the custom mounter image so every fresh pod starts with sane davfs2 settings instead of relying on someone remembering to patch it again at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we watch now
&lt;/h2&gt;

&lt;p&gt;Two things came out of chasing this. First, &lt;code&gt;df -h&lt;/code&gt; on the mount mid-upload is worth watching before a backup fails, not after. Cache pressure shows up there before MinIO ever throws an error. Second, we alert on backup job duration now, not just success/failure. A job that silently starts taking longer, or one where the upload phase drags relative to the dump phase, is usually the cache filling up again before it's bad enough to fail outright.&lt;/p&gt;

&lt;p&gt;MOCO itself never lied to us here. The CR status, the &lt;code&gt;CronJob&lt;/code&gt;, the job logs all pointed at the right layer once we looked. The failure was one level down, in infrastructure MOCO doesn't know exists and has no reason to. Worth remembering when the operator's own status says everything's fine and the backup still isn't landing.&lt;/p&gt;

&lt;p&gt;Ref - &lt;a href="https://cybozu-go.github.io/moco/backup.html" rel="noopener noreferrer"&gt;https://cybozu-go.github.io/moco/backup.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>mysql</category>
      <category>moco</category>
      <category>devops</category>
    </item>
    <item>
      <title>Getting started on MOCO, the MySQL Operator for Kubernetes Part 1</title>
      <dc:creator>Mucheru  Maina</dc:creator>
      <pubDate>Tue, 14 Jan 2025 05:59:49 +0000</pubDate>
      <link>https://dev.to/aws-builders/getting-started-on-moco-the-mysql-operator-for-kubernetes-part-1-3kc7</link>
      <guid>https://dev.to/aws-builders/getting-started-on-moco-the-mysql-operator-for-kubernetes-part-1-3kc7</guid>
      <description>&lt;p&gt;MOCO (MySQL Operator for Kubernetes) is a robust, cloud-native solution designed to simplify the management of MySQL clusters in Kubernetes environments. It automates the provisioning, scaling, backup, and maintenance of MySQL instances while ensuring high availability and reliability. MOCO leverages Kubernetes resources to create, monitor, and manage MySQL clusters.&lt;/p&gt;

&lt;p&gt;MOCO supports specific versions of MySQL and Kubernetes. As of the latest information, it supports MySQL versions 8.0.28, 8.0.37, 8.0.39, 8.0.40, and 8.4.3, and Kubernetes versions 1.29, 1.30, and 1.31&lt;/p&gt;

&lt;h2&gt;
  
  
  How MOCO Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Provisioning Clusters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;MOCO provisions MySQL clusters by creating Kubernetes StatefulSets for each cluster. The process involves:&lt;/li&gt;
&lt;li&gt;Defining a MySQLCluster custom resource (CR) with desired configurations.&lt;/li&gt;
&lt;li&gt;MOCO controller creates StatefulSets and persistent volume claims (PVCs) for the cluster nodes.&lt;/li&gt;
&lt;li&gt;Configuring MySQL instances with semi-synchronous replication for high availability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment Services
&lt;/h2&gt;

&lt;p&gt;MOCO deploys the following services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary Service: Routes traffic to the primary node.&lt;/li&gt;
&lt;li&gt;Replica Service: Routes traffic to replica nodes for read queries.&lt;/li&gt;
&lt;li&gt;Backup Service: Handles backups through sidecars integrated into the StatefulSet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Types of Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single Primary with Replicas: Default mode with one primary and multiple replicas.&lt;/li&gt;
&lt;li&gt;Multi-Region Clusters: For cross-region replication.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backup and restore
&lt;/h2&gt;

&lt;p&gt;MOCO can take full and incremental backups regularly. The backup data are stored in Amazon S3 compatible object storages.&lt;br&gt;
MOCO supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled backups to object storage (e.g., S3).&lt;/li&gt;
&lt;li&gt;On-demand backups triggered through the Kubernetes API.&lt;/li&gt;
&lt;li&gt;Restorations from backups via simple CR updates.&lt;/li&gt;
&lt;li&gt;Point-in-Time Recovery: Ensures robust data protection in disaster recovery scenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Object storage bucket&lt;/strong&gt;&lt;br&gt;
Bucket is a management unit of objects in S3. MOCO stores backups in a specified bucket.&lt;/p&gt;

&lt;p&gt;MOCO does not remove backups. To remove old backups automatically, you can set a lifecycle configuration to the bucket.&lt;/p&gt;

&lt;p&gt;Read more &lt;a href="https://cybozu-go.github.io/moco/usage.html?highlight=errant#backup-and-restore" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling Errant Pods&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Errant pods are MySQL nodes with divergent data. MOCO detects and isolates such pods automatically, preventing replication issues. Manual intervention can also remove these pods if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication Maintenance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MOCO uses semi-synchronous replication for consistency. The primary writes changes to replicas before committing transactions. Failovers are handled by promoting a replica to primary, ensuring minimal disruption. MOCO also monitors replication delays, helping maintain sync and performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateless or Stateful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MOCO deployments are stateful because MySQL requires persistent data storage. StatefulSets in Kubernetes ensure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent storage using PVCs.&lt;/li&gt;
&lt;li&gt;Stable network identities for MySQL nodes.&lt;/li&gt;
&lt;li&gt;Ordered scaling and rolling updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Quick setup
&lt;/h2&gt;

&lt;p&gt;You can choose between two installation methods.&lt;/p&gt;

&lt;p&gt;MOCO depends on cert-manager. If cert-manager is not installed on your cluster, install it as follows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install using raw manifests:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;$ curl -fsLO https://github.com/cybozu-go/moco/releases/latest/download/moco.yaml&lt;br&gt;
$ kubectl apply -f moco.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install using Helm chart:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;$ helm repo add moco https://cybozu-go.github.io/moco/&lt;br&gt;
$ helm repo update&lt;br&gt;
$ helm install --create-namespace --namespace moco-system moco moco/moco&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customize manifests&lt;/strong&gt;&lt;br&gt;
If you want to edit the manifest, &lt;a href="https://github.com/cybozu-go/moco/tree/main/config" rel="noopener noreferrer"&gt;config/&lt;/a&gt; directory contains the source YAML for &lt;a href="https://kustomize.io/" rel="noopener noreferrer"&gt;kustomize&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a Cluster
&lt;/h2&gt;

&lt;p&gt;An empty cluster always has a writable instance called the primary. All other instances are called replicas. Replicas are read-only and replicate data from the primary.&lt;/p&gt;

&lt;p&gt;The following YAML is to create a three-instance cluster. It has an anti-affinity for Pods so that all instances will be scheduled to different Nodes. It also sets the limits for memory and CPU to make the Pod &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/" rel="noopener noreferrer"&gt;Guaranteed&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: moco.cybozu.com/v1beta2
kind: MySQLCluster
metadata:
  namespace: default
  name: test
spec:
  # replicas is the number of mysqld Pods.  The default is 1.
  replicas: 3
  podTemplate:
    spec:
      # Make the data directory writable. If moco-init fails with "Permission denied", uncomment the following settings.
      # securityContext:
      #   fsGroup: 10000
      #   fsGroupChangePolicy: "OnRootMismatch"  # available since k8s 1.20
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app.kubernetes.io/name
                operator: In
                values:
                - mysql
              - key: app.kubernetes.io/instance
                operator: In
                values:
                - test
            topologyKey: "kubernetes.io/hostname"
      containers:
      # At least a container named "mysqld" must be defined.
      - name: mysqld
        image: ghcr.io/cybozu-go/moco/mysql:8.4.3
        # By limiting CPU and memory, Pods will have Guaranteed QoS class.
        # requests can be omitted; it will be set to the same value as limits.
        resources:
          limits:
            cpu: "10"
            memory: "10Gi"
  volumeClaimTemplates:
  # At least a PVC named "mysql-data" must be defined.
  - metadata:
      name: mysql-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, MOCO uses preferredDuringSchedulingIgnoredDuringExecution to prevent Pods from being placed on the same Node.&lt;br&gt;
There are other example manifests in thier &lt;a href="https://github.com/cybozu-go/moco/tree/main/examples" rel="noopener noreferrer"&gt;examples directory&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the cluster
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;kubectl moco&lt;/strong&gt;&lt;br&gt;
From outside of your Kubernetes cluster, you can access MOCO MySQL instances using &lt;code&gt;kubectl-moco&lt;/code&gt;. &lt;code&gt;kubectl-moco&lt;/code&gt; is a plugin for kubectl. Pre-built binaries are available on GitHub releases.&lt;/p&gt;

&lt;p&gt;The following is an example to run mysql command interactively to access the primary instance of test MySQLCluster in foo namespace.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl moco -n foo mysql -it test&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Connecting to mysqld over network&lt;br&gt;
MOCO prepares two Services for each MySQLCluster. For example, a MySQLCluster named test in foo Namespace has the following Services.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Service Name  DNS Name    Description&lt;br&gt;
moco-test-primary   moco-test-primary.foo.svc   Connect to the primary instance.&lt;br&gt;
moco-test-replica   moco-test-replica.foo.svc   Connect to replica instances.&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cluster status
&lt;/h2&gt;

&lt;p&gt;You can see the health and availability status of MySQLCluster as follows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl get mysqlcluster&lt;br&gt;
NAME   AVAILABLE   HEALTHY   PRIMARY   SYNCED REPLICAS   ERRANT REPLICAS&lt;br&gt;
test   True        True      0         3&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cluster is available when the primary Pod is running and ready.&lt;/li&gt;
&lt;li&gt;The cluster is healthy when there is no problems.&lt;/li&gt;
&lt;li&gt;PRIMARY is the index of the current primary instance Pod.&lt;/li&gt;
&lt;li&gt;SYNCED REPLICAS is the number of ready Pods.&lt;/li&gt;
&lt;li&gt;ERRANT REPLICAS is the number of instances having errant transactions.
You can also use &lt;code&gt;kubectl describe mysqlcluster&lt;/code&gt;to see the recent events on the cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Logs
&lt;/h2&gt;

&lt;p&gt;Error logs from mysqld can be viewed as follows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl logs moco-test-0 mysqld&lt;/code&gt;&lt;br&gt;
Slow logs from mysqld can be viewed as follows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl logs moco-test-0 slow-log&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switchover&lt;/strong&gt;&lt;br&gt;
Switchover is an operation to change the live primary to one of the replicas.&lt;/p&gt;

&lt;p&gt;MOCO automatically switch the primary when the Pod of the primary instance is to be deleted.&lt;/p&gt;

&lt;p&gt;Users can manually trigger a switchover with &lt;code&gt;kubectl moco switchover CLUSTER_NAME&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failover&lt;/strong&gt;&lt;br&gt;
Failover is an operation to replace the dead primary with the most advanced replica. MOCO automatically does this as soon as it detects that the primary is down.&lt;/p&gt;

&lt;p&gt;The most advanced replica is a replica who has retrieved the most up-to-date transaction from the dead primary. Since MOCO configures loss-less semi-synchronous replication, the failover is guaranteed not to lose any user data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-initializing an errant replica&lt;/strong&gt;&lt;br&gt;
Delete the PVC and Pod of the errant replica, like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl delete --wait=false pvc mysql-data-moco-test-0&lt;br&gt;
$ kubectl delete --grace-period=1 pods moco-test-0&lt;/code&gt;&lt;br&gt;
Depending on your Kubernetes version, StatefulSet controller may create a pending Pod before PVC gets deleted. Delete such pending Pods until PVC is actually removed.&lt;/p&gt;

&lt;p&gt;Ref - &lt;a href="https://cybozu-go.github.io/moco/index.html" rel="noopener noreferrer"&gt;https://cybozu-go.github.io/moco/index.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>k8s</category>
      <category>mysql</category>
      <category>moco</category>
    </item>
    <item>
      <title>What is Kured (KUbernetes REboot Daemon) in k8s?</title>
      <dc:creator>Mucheru  Maina</dc:creator>
      <pubDate>Fri, 01 Mar 2024 11:34:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-is-kured-kubernetes-reboot-daemon-in-k8s-40ab</link>
      <guid>https://dev.to/aws-builders/what-is-kured-kubernetes-reboot-daemon-in-k8s-40ab</guid>
      <description>&lt;p&gt;Defination from the &lt;a href="https://kured.dev/" rel="noopener noreferrer"&gt;official page&lt;/a&gt; states that &lt;strong&gt;kured&lt;/strong&gt; is a Kubernetes daemonset that performs safe automatic node reboots when the need to do so is indicated by the package management system of the underlying OS.&lt;/p&gt;

&lt;p&gt;By periodically rebooting nodes, Kured ensures that any pending updates or configuration changes take effect, resulting in a more efficient and reliable cluster.&lt;/p&gt;

&lt;p&gt;Here are the key points to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kured monitors the operating system for security patches, kernel updates, and system-level changes in Kubernetes nodes. It proactively identifies the need for reboots to keep the cluster secure and up-to-date.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a reboot is required, Kured gracefully cordons the node, marking it as unschedulable for new pods without disrupting existing ones. It then proceeds to drain the node, evicting existing pods in a controlled manner to ensure a smooth reboot process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kured includes built-in safety mechanisms to prevent unnecessary reboots and allows users to define maintenance windows for avoiding disruptions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The continuous monitoring by Kured ensures that the Kubernetes cluster operates with the latest updates, enhancing performance, security, and stability.&lt;br&gt;
Organizations can leverage Kubernetes clusters more effectively while minimizing risks associated with outdated software and configurations.&lt;/p&gt;

&lt;p&gt;Setting up Kured is a straightforward process that involves deploying it as a DaemonSet in the Kubernetes cluster. This deployment strategy ensures that Kured runs on every node within the cluster, effectively monitoring and managing the rebooting process for each individual node. &lt;/p&gt;

&lt;p&gt;Here is how you can do that&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# ClusterRole for kured
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kured
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "patch"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["list", "delete", "get"]
- apiGroups: ["apps"]
  resources: ["daemonsets"]
  verbs: ["get"]
- apiGroups: [""]
  resources: ["pods/eviction"]
  verbs: ["create"]

# ClusterRoleBinding for kured
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kured
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kured
subjects:
- kind: ServiceAccount
  name: kured
  namespace: kube-system

# Role for kured in kube-system namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: kube-system
  name: kured
rules:
- apiGroups: ["apps"]
  resources: ["daemonsets"]
  resourceNames: ["kured"]
  verbs: ["update"]

# RoleBinding for kured in kube-system namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: kube-system
  name: kured
subjects:
- kind: ServiceAccount
  namespace: kube-system
  name: kured
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: kured

# ServiceAccount for kured
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kured
  namespace: kube-system

# DaemonSet for kured
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kured
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: kured
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        name: kured
    spec:
      serviceAccountName: kured
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      - key: node-role.kubernetes.io/control-plane
        effect: NoSchedule
      - key: "node-role.kubernetes.io/mysql"
        operator: "Equal"
        effect: "NoSchedule"
      hostPID: true
      restartPolicy: Always
      containers:
      - name: kured
        image: ghcr.io/kubereboot/kured:{{ kured_version }}
        imagePullPolicy: IfNotPresent
        securityContext:
          privileged: true
        env:
        - name: KURED_NODE_ID
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        command:
        - /usr/bin/kured
        - --reboot-days=mon,tue,wed,thu
        - --reboot-delay=90s
        - --start-time=3am
        - --end-time=5am
        - --time-zone=UTC
        - --prometheus-url={{ prometheus_url }}
        - --alert-filter-regexp=^Watchdog$
        - --period=15m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explanation:&lt;br&gt;
This part allows you to define maintenance windows for avoiding disruptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  command:
        - /usr/bin/kured
        - --reboot-days=mon,tue,wed,thu
        - --reboot-delay=90s
        - --start-time=3am
        - --end-time=5am
        - --time-zone=UTC
        - --prometheus-url={{ prometheus_url }}
        - --alert-filter-regexp=^Watchdog$
        - --period=15m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>kubernetes</category>
      <category>kured</category>
      <category>devops</category>
    </item>
    <item>
      <title>Karpenter: The Better Autoscaling Solution for Kubernetes- Part 1</title>
      <dc:creator>Mucheru  Maina</dc:creator>
      <pubDate>Sat, 25 Feb 2023 04:33:48 +0000</pubDate>
      <link>https://dev.to/aws-builders/karpenter-the-better-autoscaling-solution-for-kubernetes-part-1-4pd5</link>
      <guid>https://dev.to/aws-builders/karpenter-the-better-autoscaling-solution-for-kubernetes-part-1-4pd5</guid>
      <description>&lt;p&gt;If you're running Kubernetes, you're likely familiar with the standard cluster autoscaler. While it's a useful tool, it has its limitations. Enter Karpenter, an open-source autoscaling solution that offers many advantages over the standard cluster autoscaler. It is a flexible, high-performance Kubernetes cluster autoscaler built with AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Karpenter Works
&lt;/h2&gt;

&lt;p&gt;Karpenter takes a different approach to autoscaling than the standard cluster autoscaler. Instead of adding or removing nodes based on demand, Karpenter provisions nodes based on application requirements. This means that it can optimize resource utilization and reduce costs.&lt;/p&gt;

&lt;p&gt;Karpenter works by creating custom Kubernetes resources called "provisioners." Provisioners are used to define the resources that Karpenter should provision, such as nodes or virtual machines. When an application needs more resources, Karpenter checks the provisioners to see if any need to be created. If so, Karpenter will create the new resources and add them to the cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7swc4blu56qrvxs7r7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7swc4blu56qrvxs7r7q.png" alt="karpenter." width="799" height="357"&gt;&lt;/a&gt;&lt;br&gt;
Image Courtesy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Comparing Karpenter to Cluster Autoscaler
&lt;/h2&gt;

&lt;p&gt;Karpenter offers several advantages over the standard cluster autoscaler as detailed below.&lt;/p&gt;
&lt;h3&gt;
  
  
  Optimal Resource Utilization
&lt;/h3&gt;

&lt;p&gt;One of the most significant advantages of Karpenter is its ability to optimize resource utilization. It can do this by automatically provisioning nodes based on application needs. This means that you can avoid overprovisioning, which can lead to wasted resources and increased costs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Customizable Scaling
&lt;/h3&gt;

&lt;p&gt;Karpenter offers the ability to customize scaling behaviors based on your specific needs. You can configure scaling based on metrics such as CPU or memory usage, or you can use your own custom metrics.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cost Savings
&lt;/h3&gt;

&lt;p&gt;Because Karpenter optimizes resource utilization, it can lead to significant cost savings. By avoiding overprovisioning, you can reduce the number of nodes required to run your applications, which can result in lower cloud bills.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ease of Use
&lt;/h3&gt;

&lt;p&gt;Karpenter is easy to use and deploy. It can be installed using Helm, and it integrates seamlessly with Kubernetes.&lt;/p&gt;

&lt;p&gt;The standard cluster autoscaler can be more challenging to set up, and it may require more manual configuration.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Get Started with Karpenter
&lt;/h2&gt;

&lt;p&gt;There are different ways to get started with Karpenter. This article will just highlight the steps. &lt;strong&gt;(Watch out for part 2 with a step-by-step guide on how to install and configure Karpenter)&lt;/strong&gt; We will use Helm Chart to install Karpernter. Here are some steps before having it operational:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the KarpenterNode IAM Role - Instances launched by Karpenter must run with an InstanceProfile that grants permissions necessary to run containers and configure networking.&lt;/li&gt;
&lt;li&gt;Create the IAM role for Karpenter Controller -  Associate the Kubernetes Service Account and the IAM role using &lt;a href="https://docs.aws.amazon.com/emr/latest/EMR-on-EKS-DevelopmentGuide/setting-up-enable-IAM.html" rel="noopener noreferrer"&gt;IRSA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Update aws-auth ConfigMap - to allow the nodes that use the KarpenterRole IAM Role to join the cluster&lt;/li&gt;
&lt;li&gt;Deploy Karpenter Helm Chart:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm template karpenter oci://public.ecr.aws/karpenter/karpenter --version ${KARPENTER_VERSION}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Create a default Provisioner, (example)
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  labels:
    intent: apps
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["spot"]
    - key: karpenter.k8s.aws/instance-size
      operator: NotIn
      values: [nano, micro, small, medium, large]
  limits:
    resources:
      cpu: 1000
      memory: 1000Gi
  ttlSecondsAfterEmpty: 30
  ttlSecondsUntilExpired: 2592000
  providerRef:
    name: default
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    alpha.eksctl.io/cluster-name: ${CLUSTER_NAME}
  securityGroupSelector:
    alpha.eksctl.io/cluster-name: ${CLUSTER_NAME}
  tags:
    KarpenerProvisionerName: "default"
    NodeType: "karpenter-workshop"
    IntentLabel: "apps"
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once you've installed Karpenter, you can begin using it to optimize your Kubernetes cluster's resource utilization and reduce costs.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Provisioner configuration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Karpenter configuration comes in the form of a Provisioner CRD (Custom Resource Definition). A single Karpenter provisioner is capable of handling many different pod shapes. You can play with it to suit whatever needs you have. For example&lt;/p&gt;

&lt;p&gt;One can limit Karpenter to use either on-demand or spot instances, you can use the &lt;code&gt;spot&lt;/code&gt; field in the &lt;code&gt;provisioner&lt;/code&gt; definition. Here's an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec:
  labels:
    type: karpenter
  requirements:
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["on-demand"]
        # - key: karpenter.sh/capacity-type
    #   operator: In
    #   values: ["spot"]
    - key: "node.kubernetes.io/instance-type"
      operator: In
      values: ["c5.large", "m5.large", "m5.xlarge"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example we're setting the&amp;nbsp;&lt;code&gt;karpenter.sh/capacity-type&lt;/code&gt;to initially limit Karpenter to provisioning On-Demand instances, and&amp;nbsp;&lt;code&gt;karpenter.k8s.aws/instance-type&lt;/code&gt;to limit to specific instance types.&lt;/p&gt;

&lt;p&gt;One can also limit Karpenter to specific instance types, regions and zones you can use the &lt;code&gt;instanceTypes&lt;/code&gt; field in the &lt;code&gt;provisioner&lt;/code&gt; definition. Here's an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: karpenter.sh/v1alpha1
kind: Provisioner
metadata:
  name: example-provisioner
spec:
  constraints:
    - type: "awsec2"
      region: "us-west-2"
      zones:
        - "a"
      instanceTypes:
        - "t2.micro"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the &lt;code&gt;instanceTypes&lt;/code&gt; field is set to &lt;code&gt;t2.micro&lt;/code&gt;, which means that the provisioner will only use &lt;code&gt;t2.micro&lt;/code&gt; instances. You can add additional instance types to the list if you want to allow for more flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;While Karpenter offers many advantages over the standard cluster autoscaler, it also has some limitations. Some of the key limitations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited support for custom metrics: While Karpenter does offer support for custom metrics, it is more limited than some other solutions. This can make it challenging to implement certain types of custom scaling behaviors.&lt;/li&gt;
&lt;li&gt;Lack of integration with some cloud providers: Karpenter is designed to work with Kubernetes, but it may not integrate seamlessly with all cloud providers. This can make it more challenging to deploy Karpenter in certain environments.&lt;/li&gt;
&lt;li&gt;Complexity: Karpenter is a powerful tool, but it can also be complex to configure and use. It may require more expertise and resources than some other scaling solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Despite these limitations, Karpenter is still an excellent choice for many Kubernetes users. Its ability to optimize resource utilization, customize scaling behaviors, and reduce costs make it a compelling option for many organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Karpenter is a powerful autoscaling solution that offers many advantages over the standard cluster autoscaler. With its ability to optimize resource utilization, customized scaling, and ease of use, Karpenter is a must-have tool for Kubernetes users. Follow the steps above to get started with Karpenter today!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay tuned for Part 2 of this blog, where we'll provide a step-by-step guide on how to install and configure Karpenter. Part 2 will be more technical, so if you're interested in getting started with Karpenter, be sure to check it out!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-karpenter-an-open-source-high-performance-kubernetes-cluster-autoscaler/" rel="noopener noreferrer"&gt;&lt;strong&gt;Introducing Karpenter an open source high performance Kubernetes cluster autoscaler&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;&lt;strong&gt;Karpenter&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Follow me at &lt;a href="https://www.linkedin.com/in/mucheruj/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://twitter.com/mucheeru" rel="noopener noreferrer"&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>eks</category>
    </item>
  </channel>
</rss>
