Recover Single-Node ETCD Cluster from WAL Corruption

Warning

This procedure is only applicable to single-node clusters.

Problem Description

When a sudden poweroff of the node occurs, the etcd Write-Ahead Log (WAL) can become corrupted. Since there are no other nodes in the cluster, there is no way to recover the cluster from the corrupted WAL. This can prevent etcd from starting, which in turn prevents the Kubernetes API server and the Kubernetes control plane from starting.

Root Cause

Etcd’s WAL is a file-based log of all changes to the etcd database. It is used to make sure that all changes to the database are durable and can be recovered in case of a crash. When a sudden poweroff occurs, the WAL can become corrupted because the last write to the WAL may not have been fully flushed to a disk. This can cause the etcd database to become corrupted, which prevents etcd from starting.

Investigation Steps

  1. Check that etcd is not running.

    [artesca-os@bootstrap ~]$
    
    sudo crictl ps -q --label io.kubernetes.container.name=etcd --label io.kubernetes.pod.namespace=kube-system --state Running
    
    
    No output.
    
  2. Check the logs from the latest etcd pod to see if there are any errors related to WAL corruption.

    [artesca-os@bootstrap ~]$
    
    sudo crictl logs $(sudo crictl ps -qa --label io.kubernetes.container.name=etcd --label io.kubernetes.pod.namespace=kube-system --state Exited | head -n 1)
    
    
    {"level":"warn","ts":"2023-10-19T08:00:47.960Z","caller":"fileutil/fileutil.go:53","msg":"check file permission","error":"directory \"/var/lib/etcd\" exist, but the permission is \"drwxr-x---\". The recommended permission is \"-rwx------\" to prevent possible unprivileged access to the data"}
    {"level":"info","ts":"2023-10-19T08:00:47.964Z","caller":"etcdserver/backend.go:81","msg":"opened backend db","path":"/var/lib/etcd/member/snap/db","took":"4.171107ms"}
    {"level":"warn","ts":"2023-10-19T08:00:47.964Z","caller":"wal/util.go:90","msg":"ignored file in WAL directory","path":"0000000000000000-0000000000000000.wal.broken"}
    {"level":"info","ts":"2023-10-19T08:00:47.965Z","caller":"etcdserver/server.go:530","msg":"No snapshot found. Recovering WAL from scratch!"}
    {"level":"warn","ts":"2023-10-19T08:00:47.965Z","caller":"wal/util.go:90","msg":"ignored file in WAL directory","path":"0000000000000000-0000000000000000.wal.broken"}
    {"level":"info","ts":"2023-10-19T08:00:47.965Z","caller":"etcdserver/raft.go:529","msg":"restarting local member","cluster-id":"0","local-member-id":"0","commit-index":0}
    panic: cannot use none as id
    

Problem Resolution

  1. Extract the latest backup to a temporary directory.

    [artesca-os@<NODE_NAME> ~]$
    
    last_backup=$(ls -1 /var/lib/metalk8s/backups/*.tar.gz | tail -n 1)
    mkdir -p /tmp/recovery-workspace
    tar -xzf $last_backup -C /tmp/recovery-workspace
    
  2. Back up the current etcd Pod manifest.

    [artesca-os@<NODE_NAME> ~]$
    
    sudo cp /etc/kubernetes/manifests/etcd.yaml /tmp/recovery-workspace/etcd.yaml
    
  3. Create a static pod to restore the etcd backup.

    [artesca-os@<NODE_NAME> ~]$
    
    cat <<EOF > /tmp/recovery-workspace/etcd-restore.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: etcd-restore-snap
      namespace: "kube-system"
    spec:
      restartPolicy: Never
      containers:
        - name: etcd-restore-snap
          image: metalk8s-registry-from-config.invalid/metalk8s-133.0.16/metalk8s-utils:133.0.16
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
          - |
            rm -rf /var/lib/etcd/* && rm -rf /var/lib/etcd/.* 2>/dev/null
            ETCDCTL_API=3 etcdctl --cert /etc/kubernetes/pki/etcd/salt-master-etcd-client.crt --key /etc/kubernetes/pki/etcd/salt-master-etcd-client.key --cacert /etc/kubernetes/pki/etcd/ca.crt snapshot restore /tmp/recovery-workspace/etcd_snapshot --data-dir /var/lib/etcd && touch /tmp/recovery-workspace/success
          volumeMounts:
            - mountPath: /var/lib/etcd
              name: etcd-data
              readOnly: False
            - mountPath: /etc/kubernetes/pki/etcd
              name: etcd-certs
              readOnly: True
            - mountPath: /tmp/recovery-workspace
              name: recovery-workspace
              readOnly: False
      hostNetwork: true
      volumes:
         - hostPath:
             path: /var/lib/etcd
             type: DirectoryOrCreate
           name: etcd-data
         - hostPath:
             path: /etc/kubernetes/pki/etcd
             type: Directory
           name: etcd-certs
         - hostPath:
             path: /tmp/recovery-workspace
             type: Directory
           name: recovery-workspace
    EOF
    sudo cp -f /tmp/recovery-workspace/etcd-restore.yaml /etc/kubernetes/manifests/etcd.yaml
    
  4. Restart kubelet to pick up the new static pod.

    [artesca-os@<NODE_NAME> ~]$
    
    sudo systemctl restart kubelet
    
  5. Wait for the restore pod to finish restoring.

    [artesca-os@<NODE_NAME> ~]$
    
    timeout 120s sh -c "until [ -f /tmp/recovery-workspace/success ]; do sleep 1s; done" \
       && echo "Restore succeeded" || echo "Restore failed"
    
  6. Restore etcd.

    [artesca-os@<NODE_NAME> ~]$
    
    sudo cp -f /tmp/recovery-workspace/etcd.yaml /etc/kubernetes/manifests/etcd.yaml
    
  7. Restart kubelet to pick up the static pod.

    [artesca-os@<NODE_NAME> ~]$
    
    sudo systemctl restart kubelet