Immutable root with atomic upgrades: Difference between revisions

From Alpine Linux
No edit summary
No edit summary
Line 52: Line 52:
<pre># btrfs subvolume create /mnt/commons/@var@lib@flatpak</pre>
<pre># btrfs subvolume create /mnt/commons/@var@lib@flatpak</pre>
Next, most important directories:
Next, most important directories:
<pre># mkdir /mnt/snapshots</pre> Stores directories containing snapshots belonging to one generation.<br>
<pre># mkdir /mnt/snapshots</pre>
<pre># mkdir /mnt/links</pre> stores generations of directories containing links to snapshot generations.<br>
Stores directories containing snapshots belonging to one generation.<br>
<pre># mkdir /mnt/links</pre>  
Stores generations of directories containing links to snapshot generations.<br>
Let's create first generation and populate it with one OS root snapshot <code>@</code>:
Let's create first generation and populate it with one OS root snapshot <code>@</code>:
<pre># NEWSNAPSHOTS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)"
<pre># NEWSNAPSHOTS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)"
Line 68: Line 70:
Link that will point to latest links generation:
Link that will point to latest links generation:
<pre># ln -s "./links/$NEWLINKS" /mnt/current</pre>
<pre># ln -s "./links/$NEWLINKS" /mnt/current</pre>
This setup allows us to just have static rEFInd config that points to <code>/current/0</code>, <code>/current/1</code>, etc. while the actual underlying boot environment will change with each upgrade.<br>
This setup allows us to just have static rEFInd config that points to to <code>/current/0/@</code>, <code>/current/1/@</code>, etc. while the actual underlying boot environment will change with each upgrade.<br>
But how will fs mounting services know which snapshot generation is currently loaded?<br>
But how will fs mounting services know which snapshot generation is currently loaded?<br>
The answer is common <code>fstab</code> in the btrfs root.<br>
The answer is common <code>fstab</code> in the btrfs root.<br>
Line 80: Line 82:
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/cache btrfs subvol=/commons/@var@cache,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/cache btrfs subvol=/commons/@var@cache,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/log btrfs subvol=/commons/@var@log,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/log btrfs subvol=/commons/@var@log,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/lib/flatpak btrfs subvol=/commons/@var@lib@flatpak,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /home btrfs subvol=/commons/@home,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /home btrfs subvol=/commons/@home,rw,noatime 0 0
# UUID=2FE6-837A /boot/efi vfat rw,noatime,discard 0 2
# UUID=2FE6-837A /boot/efi vfat rw,noatime,discard 0 2
tmpfs /tmp tmpfs mode=1777,noatime,nosuid,nodev,size=2G 0 0
tmpfs /tmp tmpfs mode=1777,noatime,nosuid,nodev,size=2G 0 0
UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 swap swap rw,noatime,discard 0 0</pre>
UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 swap swap rw,noatime,discard 0 0</pre>
<code>CURRENT_SNAPSHOTS_PATH</code> will be replaced by scripts and the result put into <code>/etc/fstab</code> of a created <code>@</code> snapshot during new generation preparations.
<code>CURRENT_SNAPSHOTS_PATH</code> will be replaced by scripts with, for example, <code>/snapshots/20210411212549sdBXyLxg</code>, and the result will be piped into <code>/etc/fstab</code> of a created <code>@</code> snapshot during new generation preparations.<br>
Root btrfs volume structure mounted on <code>/mnt</code>:
<pre>|--mnt
<pre>|--mnt
| |--commons
| |--commons
| | |--@var@tmp
| | |--@var@cache
| | |--@var@log
| | |--@var@lib@flatpak
| | |--@home
| |--current
| |--current
| |--fstab
| |--fstab
Line 99: Line 108:
| | |--20210411212549sdBXyLxg
| | |--20210411212549sdBXyLxg
| | | |--@</pre>
| | | |--@</pre>
= Base system install =
With the directory strtucture prepared we can start installation of a basic Alpine Linux system.

Revision as of 16:49, 2 May 2021

This page is a work in progress ...

This page is still being developed.

What?

This article provides a basic guide to setting up a read-only-root-based Alpine Linux system with several boot environments and atomic upgrades using rEFInd and btrfs.

Why?

Read-only root and atomic upgrades with ability to easily rollback or boot previous configurations is a concept that got some popularity recently. Distributions providing and promoting such features, for example, are Fedora Silverblue, Opensuse MicroOS, NixOS and GNU Guix. While Alpine Linux has it's killer features it lacks mentioned above ones on default setup. This is a proof of concept that it's possible to implement them in a minimal way on a minimal system.

Partitioning disks

The first step is creating partition table on target device /dev/sda:

# apk add gptfdisk
# gdisk /dev/sda
> o ↵
> y ↵
> w ↵
> y ↵

Now we can define the partitions:

# cgdisk /dev/sda

Partition creation process consists of several steps:

  1. Start sector - you can safely use default value by pressing ↵
  2. Size
  3. Type (as hex code) - EFI is ef00, Linux filesystem is 8300, Swap is 8200.

Result table:

Part.     #     Size        Partition Type            Partition Name
----------------------------------------------------------------
1               200.0 MiB   EFI System                EFI
2               200.0 GiB   Linux filesystem          ROOT
3               32.0 GiB    Linux swap                SWAP

ROOT partition name will later be used in rEFInd configuration to identify boot volume. Next step is creating filesystems:

# mkfs.vfat -F32 /dev/sda1
# mkfs.btrfs /dev/sda2
# mkswap /dev/sda3

Now we can mount our root volume:

# mount -t btrfs /dev/sda2 /mnt

File system structure

Now we should create file structure that would provide reliable atomic system upgrades.
Start with following directories:

# mkdir /mnt/next

Stores next current link.

# mkdir /mnt/commons

Stores common non-snapshotting subvolumes, is necessary due to how busybox mv does atomic link replacement.
We may populate it right away:

# btrfs subvolume create /mnt/commons/@var@tmp
# btrfs subvolume create /mnt/commons/@var@cache
# btrfs subvolume create /mnt/commons/@var@log
# btrfs subvolume create /mnt/commons/@home

If you use flatpak, you may also want to keep it's directory separate:

# btrfs subvolume create /mnt/commons/@var@lib@flatpak

Next, most important directories:

# mkdir /mnt/snapshots

Stores directories containing snapshots belonging to one generation.

# mkdir /mnt/links

Stores generations of directories containing links to snapshot generations.
Let's create first generation and populate it with one OS root snapshot @:

# NEWSNAPSHOTS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)"
# mkdir "/mnt/$NEWSNAPSHOTS"
# btrfs subvolume create /mnt/$NEWSNAPSHOTS/@

Populate links:

# NEWLINKS="$(date -u +"%Y%m%d%H%M%S")$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 8 | head -n 1)"
# mkdir "/mnt/links/$NEWLINKS"
# ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/0"
# ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/1"
# ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/2"
# ln -s "../../snapshots/$NEWSNAPSHOTS" "/mnt/links/$NEWLINKS/3"

You can have as many links as you like, just apply changes to rEFInd config and upgrade scripts described below accordingly.
Link that will point to latest links generation:

# ln -s "./links/$NEWLINKS" /mnt/current

This setup allows us to just have static rEFInd config that points to to /current/0/@, /current/1/@, etc. while the actual underlying boot environment will change with each upgrade.
But how will fs mounting services know which snapshot generation is currently loaded?
The answer is common fstab in the btrfs root.
Get UUIDs of the partitions first:

# blkid > /mnt/fstab

Now edit fstab accordingly:

# vi /mnt/fstab

Example:

UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 / btrfs subvol=CURRENT_SNAPSHOTS_PATH/@,ro,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/tmp btrfs subvol=/commons/@var@tmp,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/cache btrfs subvol=/commons/@var@cache,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/log btrfs subvol=/commons/@var@log,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /var/lib/flatpak btrfs subvol=/commons/@var@lib@flatpak,rw,noatime 0 0
UUID=b9ff5e7b-e128-4e64-861a-2fdd794a9828 /home btrfs subvol=/commons/@home,rw,noatime 0 0
# UUID=2FE6-837A /boot/efi vfat rw,noatime,discard 0 2
tmpfs /tmp tmpfs mode=1777,noatime,nosuid,nodev,size=2G 0 0
UUID=f0239163-9d46-47c1-67a4-3ee1d63d0676 swap swap rw,noatime,discard 0 0

CURRENT_SNAPSHOTS_PATH will be replaced by scripts with, for example, /snapshots/20210411212549sdBXyLxg, and the result will be piped into /etc/fstab of a created @ snapshot during new generation preparations.
Root btrfs volume structure mounted on /mnt:

|--mnt
| |--commons
| | |--@var@tmp
| | |--@var@cache
| | |--@var@log
| | |--@var@lib@flatpak
| | |--@home
| |--current
| |--fstab
| |--links
| | |--20210411213742qwrXAJBz
| | | |--0
| | | |--1
| | | |--2
| | | |--3
| |--next
| |--snapshots
| | |--20210411212549sdBXyLxg
| | | |--@

Base system install

With the directory strtucture prepared we can start installation of a basic Alpine Linux system.