Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit eff8046

Browse files
Nirusumsanft
andauthored
dev-docs: etcd disaster recovery (#1544)
* dev-docs: add rough etcd disaster recovery guide * dev-docs: use same spelling from etcd guide * dev-docs: slight rewording * fix typo * Update dev-docs/howto/etcd-disaster-recovery.md --------- Co-authored-by: Moritz Sanft <58110325+msanft@users.noreply.github.com>
1 parent 4b749d9 commit eff8046

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# etcd disaster recovery
2+
When etcd loses quorum and (N-1)/2 control planes are lost,
3+
etcd will not be able to perform any transactions anymore and will basically stall and cause timeouts.
4+
This makes the Constellation control planes unusable, resulting in a frozen cluster. The worker nodes will continue to function for a bit,
5+
but given that they cannot communicate to the control plane anymore, they will eventually also cease to function correctly.
6+
7+
If the missing control plane nodes are still existent and their state disk still exists, you likely do not need this guide.
8+
If the missing are irrecoverably lost (e.g. scaled up and down the control plane instance set), follow through this guide to bring the cluster back up.
9+
10+
## General concept
11+
1. Create snapshot of a state disk from a remaining control plane node.
12+
2. Download the snapshot and decrypt it locally
13+
3. Follow the [Restoring a cluster](https://etcd.io/docs/v3.5/op-guide/recovery/#restoring-a-cluster) guide from etcd.
14+
4. Save the modified virtual disk and upload it back to the CSP.
15+
5. Modify the scale set (or remaining VM singularly, if you can) to use the new uploaded data disk.
16+
6. Reboot, wait a few minutes.
17+
7. Pray it worked ;)
18+
19+
## How I did it once (Azure)
20+
21+
1. If the VM has never been rebooted once after initialization, reboot it once to sync any LUKS passphrase changes to disk (not 100% sure if necessary to sync the change to the passphrase - would have to double-check that later with an experimental cluster)
22+
23+
2. Create a snapshot from the disk using the CLI:
24+
```bash
25+
az snapshot create --resource-group dogfooding --name dogfooding-3 --source /subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/dogfooding/providers/Microsoft.Compute/disks/constell-f2332c74-coconstell-f2332c74-condisk2_dd460a6ae3124aa3a4c23be0ab39634e --location northeurope
26+
```
27+
28+
3. Look up the snapshot online, export it as VHD
29+
Mount the disk:
30+
```bash
31+
modprobe nbd && sudo qemu-nbd -c /dev/nbd0 /home/nils/Downloads/constellation-disk.vhd
32+
```
33+
34+
4. Get the UUID of the disk:
35+
```bash
36+
sudo cryptsetup luksDump /dev/nbd0
37+
```
38+
39+
5. Regenerate the passphrase to unlock the disk (the [code snippet below](#get-disk-decryption-key) might be useful for this)
40+
41+
6. Decrypt the disk:
42+
```bash
43+
sudo cryptsetup luksOpen /dev/nbd0 constellation-state --key-file passphrase
44+
```
45+
46+
7. Mount the decrypted disk (I just did this via the Nautilus)
47+
48+
8. Find the db file from etcd in `/var/lib/etcd/member/snap/db`
49+
50+
9. Perform the etcd [Restoring a cluster](https://etcd.io/docs/v3.5/op-guide/recovery/#restoring-a-cluster) step:
51+
52+
```bash
53+
./etcdutl snapshot restore db --initial-cluster constell-f2332c74-control-plane000001=https://10.9.126.0:2380 --initial-advertise-peer-urls https://10.9.126.0:2380 --data-dir recovery --name constell-f2332c74-control-plane000001 --skip-hash-check=true
54+
```
55+
*(replace name & IP with the name and the private IP of the remaining control plane VM you are to perform the restore process on - this information can be found in the Azure portal)*
56+
57+
10. Copy the contents of the newly created recovery directory to the mounted state disk and remove any remaining old files.
58+
**Make sure the permissions are the same as before!**
59+
60+
11. Unmount the partition:
61+
```bash
62+
sudo umount /your/mount/path
63+
sudo cryptsetup luksClose constellation-state
64+
sudo qemu-nbd -d /dev/nbd0
65+
```
66+
67+
12. Upload the modified VHD back to Azure (I just used Azure Storage Explorer for this).
68+
69+
13. Patch the whole control-plane VMSS to remove LUN 0 from the VMs:
70+
```bash
71+
az vmss disk detach --lun 0 --resource-group dogfooding --vmss-name constell-f2332c74-control-plane
72+
```
73+
74+
14. Update the VM:
75+
```bash
76+
az vmss update-instances -g dogfooding --name constell-f2332c74-control-plane --instance-ids 1
77+
```
78+
79+
15. Attach the uploaded disk as LUN 0 (either via CLI or Azure Portal, I just used the Azure Portal).
80+
81+
16. Start the VM and pray it works ;) It can take a few minutes before etcd becomes fully alive again.
82+
83+
17. Patch the state disk definition back to the VMSS (no idea how, haven't done his yet) so newly created VMs in the VMSS have a clean state disk again.
84+
85+
## Get disk decryption key
86+
```golang
87+
package main
88+
89+
import (
90+
"crypto/sha256"
91+
"encoding/hex"
92+
"encoding/json"
93+
"fmt"
94+
"io"
95+
"os"
96+
97+
"golang.org/x/crypto/hkdf"
98+
)
99+
100+
type MasterSecret struct {
101+
Key []byte `json:"key"`
102+
Salt []byte `json:"salt"`
103+
}
104+
105+
func main() {
106+
uuid := "4ae66293-57aa-4821-b99c-ebc598a6e5a8" // replace me
107+
108+
masterSecretRaw, err := os.ReadFile("constellation-mastersecret.json")
109+
if err != nil {
110+
panic(err)
111+
}
112+
113+
var masterSecret MasterSecret
114+
if err := json.Unmarshal(masterSecretRaw, &masterSecret); err != nil {
115+
panic(err)
116+
}
117+
118+
dek, err := DeriveKey(masterSecret.Key, masterSecret.Salt, []byte("key-"+uuid), 32)
119+
if err != nil {
120+
panic(err)
121+
}
122+
123+
fmt.Println(hex.EncodeToString(dek))
124+
125+
if err := os.WriteFile("passphrase", dek, 0o644); err != nil {
126+
panic(err)
127+
}
128+
}
129+
130+
// DeriveKey derives a key from a secret.
131+
func DeriveKey(secret, salt, info []byte, length uint) ([]byte, error) {
132+
hkdfReader := hkdf.New(sha256.New, secret, salt, info)
133+
key := make([]byte, length)
134+
if _, err := io.ReadFull(hkdfReader, key); err != nil {
135+
return nil, err
136+
}
137+
return key, nil
138+
}
139+
```

0 commit comments

Comments
 (0)