Use LVM on Flatcar Container Linux

    LVM - Logical Volume Management - allows you to create logical volumes, for example to use multiple physical disks as one volume. This allows you to make the full use of all attached disks.

    Flatcar Linux has built-in support for LVM. This guide covers creation of logical volumes using LVM and how to use them.

    Creating LVM

    There are two main ways to do this: create everything manually or use an Ignition config. We will first cover the manual way to get a better grip of what is happening, then we will cover the Ignition way.

    Manual

    You can find all volumes using the lsblk command. For example:

    
    # lsblk
    NAME    MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
    loop3     7:3    0  68.3M  1 loop
    loop4     7:4    0  39.3M  1 loop
    loop5     7:5    0     4K  1 loop
    sda       8:0    0 223.6G  0 disk
    |-sda1    8:1    0   128M  0 part
    |-sda2    8:2    0     2M  0 part
    |-sda3    8:3    0     1G  0 part
    | `-usr 254:0    0  1016M  1 crypt /usr
    |-sda4    8:4    0     1G  0 part
    |-sda6    8:6    0   128M  0 part  /oem
    |-sda7    8:7    0    64M  0 part
    `-sda9    8:9    0 221.3G  0 part  /
    sdb       8:16   0 447.1G  0 disk
    sdc       8:32   0 447.1G  0 disk
    sdd       8:48   0 223.6G  0 disk
    
    

    Now we know that we have /dev/sda, /dev/sdb, /dev/sdc, /dev/sdd available. However, we cannot use /dev/sda in this scenario, but we can work with the others.

    # pvcreate /dev/sdb /dev/sdc /dev/sdd
      WARNING: Failed to connect to lvmetad. Falling back to device scanning.
      Physical volume "/dev/sdb" successfully created.
      Physical volume "/dev/sdc" successfully created.
      Physical volume "/dev/sdd" successfully created.
    

    You can verify that everything worked with the following commands:

    # pvs
      PV         VG Fmt  Attr PSize   PFree
      /dev/sdb      lvm2 ---  447.13g 447.13g
      /dev/sdc      lvm2 ---  447.13g 447.13g
      /dev/sdd      lvm2 ---  223.57g 223.57g
      
    # pvdisplay
      "/dev/sdd" is a new physical volume of "223.57 GiB"
      --- NEW Physical volume ---
      PV Name               /dev/sdd
      VG Name
      PV Size               223.57 GiB
      Allocatable           NO
      PE Size               0
      Total PE              0
      Free PE               0
      Allocated PE          0
      PV UUID               x0N0k2-5c6j-HlHZ-vuAX-s82V-Yx8v-Pi3rWa
    
      "/dev/sdc" is a new physical volume of "447.13 GiB"
      --- NEW Physical volume ---
      PV Name               /dev/sdc
      VG Name
      PV Size               447.13 GiB
      Allocatable           NO
      PE Size               0
      Total PE              0
      Free PE               0
      Allocated PE          0
      PV UUID               dRa71o-rYk9-gJKC-bJdC-tJVV-yarW-LHTwPu
    
      "/dev/sdb" is a new physical volume of "447.13 GiB"
      --- NEW Physical volume ---
      PV Name               /dev/sdb
      VG Name
      PV Size               447.13 GiB
      Allocatable           NO
      PE Size               0
      Total PE              0
      Free PE               0
      Allocated PE          0
      PV UUID               vu98O9-4UDD-TTGK-PvsY-g4bN-FeL4-lcqLy7
    
    

    As you can see, you do not yet have a virtual group. You use the vgcreate command to create one and add your PVs.

    You need to specify the name of the group and the volumes you want to add to it like so:

    # vgcreate base-layer /dev/sdb /dev/sdc /dev/sdd
      Volume group "base-layer" successfully created
    
    

    Now you can go ahead and create a logical volume. We recommend to name it according to its purpose as in this example:

    # lvcreate -n vol_docker -l 100%FREE base-layer
     Logical volume "vol_docker" created.
    

    You can verify that everything worked well by issuing the following command:

    # lvdisplay
      --- Logical volume ---
      LV Path                /dev/base-layer/vol_docker
      LV Name                vol_docker
      VG Name                base-layer
      LV UUID                d0ne0u-zBZQ-29f5-rkd9-XnZv-0vhE-rGmLvA
      LV Write Access        read/write
      LV Creation host, time rrackow-test, 2024-09-18 06:42:09 +0000
      LV Status              available
      # open                 0
      LV Size                1.09 TiB
      Current LE             286163
      Segments               3
      Allocation             inherit
      Read ahead sectors     auto
      - currently set to     256
      Block device           254:1
    

    As you can see we now have a total size the sum of the individual disks.

    Next we need to use mkfs to create an ext4 filesystem:

    # mkfs.ext4 /dev/base-layer/vol_docker
    mke2fs 1.47.0 (5-Feb-2023)
    Discarding device blocks: done
    Creating filesystem with 293030912 4k blocks and 73261056 inodes
    Filesystem UUID: eed7e226-87f8-40e0-a49b-21eae4ef9620
    Superblock backups stored on blocks:
            32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
            4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
            102400000, 214990848
    
    Allocating group tables: done
    Writing inode tables: done
    Creating journal (262144 blocks): done
    Writing superblocks and filesystem accounting information: done
    

    Now you can for example mount the volume for use with docker, by mounting it to /var/lib/docker like so:

    # mkdir /var/lib/docker
    # mount /dev/base-layer/vol_docker /var/lib/docker
    

    Ignition

    In your Ignition config you will need two units: one to create the volume group and one to mount the volume. Additionally, you will also need a script that executes all the required commands.

    We will start with the script. It basically packages everything from the manual part into a script like so:

    #!/bin/bash
    set -euo pipefail
    
    
    # Function to find all disks
    find_volumes(){
      lsblk -d -o NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}'
    }
    
    disks=$(find_volumes)
    
    # Create Physical Volumes
    pvcreate "${disks}"
    
    # Create Volume Group
    vgcreate vg-root "${disks}"
    
    # Create Logical Volume for data
    lvcreate -n vol_root -l 100%FREE vg-root
    
    # Format the data volume with ext4 filesystem
    mkfs.ext4 /dev/vg-root/vol_root
    

    As you can see, we use a function to list all available disks. If you do not want to use all disks, you need to adjust the script accordingly. If you want to mount to a different place, e.g. /var/lib/docker instead of /, you need to adjust this bit as well.

    The next step is to create the unit file that executes the script:

    [Unit]
    Description=LVM Setup
    ConditionFirstBoot=yes
    Before=local-fs-pre.target
    [Service]
    Type=oneshot
    Restart=on-failure
    RemainAfterExit=yes
    ExecStart=/etc/systemd/multi-user.target/lvm.sh #This is the name and path of the file above
    [Install]
    WantedBy=multi-user.target
    

    However, we also need to mount the volume we created:

    [Unit]
    Description=LVM Mount
    [Mount]
    What=/dev/vg-root/vol_root
    Where=/
    Type=ext4
    Options=defaults
    [Install]
    WantedBy=local-fs.target
    

    Now we need to put it all together into a butane yaml:

    variant: flatcar
    version: 1.0.0
    systemd:
      units:
        - name: lvm-setup.service
          enabled: true
          contents: |
            [Unit]
            Description=LVM Setup
            ConditionFirstBoot=yes
            DefaultDependencies=no
            Before=local-fs-pre.target
            [Service]
            Type=oneshot
            Restart=on-failure
            RemainAfterExit=yes
            ExecStart=/opt/lvm.sh
            [Install]
            WantedBy=multi-user.target        
        - name: var-lib-docker.mount
          enabled: true
          contents: |
            [Unit]
            Description=Mount LVM to docker dir
            After=lvm-setup.service
            [Mount]
            What=/dev/vg-docker/vol_docker
            Where=/var/lib/docker
            Type=ext4
            Options=defaults
            [Install]
            WantedBy=local-fs.target        
        - name: docker.service
          dropins:
            - name: 10-wait-docker.conf
              contents: |
                [Unit]
                After=var-lib-docker.mount
                Requires=var-lib-docker.mount            
    storage:
      files:
        - path: /opt/lvm.sh
          mode: 0744
          contents:
            inline: |
              #!/bin/bash
              set -euo pipefail
    
    
              # Function to find all disks
              find_volumes(){
                lsblk -d -o NAME,TYPE | awk '$2 == "disk" {print "/dev/" $1}'
              }
    
              disks=$(find_volumes)
    
              # Create Physical Volumes
              pvcreate "${disks}"
    
              # Create Volume Group
              vgcreate vg-root "${disks}"
    
              # Create Logical Volume for data
              lvcreate -n vol_root -l 100%FREE vg-root
    
              # Format the data volume with ext4 filesystem
              mkfs.ext4 /dev/vg-root/vol_root          
    

    As mentioned before, we need to still transpile from a butane yaml to an Ignition config like so:

    $ docker run --rm -i quay.io/coreos/butane:latest < lvm.yaml > ignition.json
    

    You can verify your config with the following:

    $ cat ignition.json
    {"ignition":{"version":"3.3.0"},"storage":{"files":[{"path":"/etc/systemd/system/multi-user.target.wants/lvm.sh","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/3ySQY/TPhTE7+9TzN/Nn20lQrYrbqsiIdTdC6AVQkicKjd5bqy4doidLFW33x05rtosB47Om/mN38Sz/4qttsVW+po8B+TcO7S6ZSW1IaIZHnpbBu0sgoPStoI0BpX2jad43AzO9Hv288WRAOO3pkFeIXf4+vHL+u33n09rvEA+N7jJ7rBaQUSvwLHttA0QRcVDIZAtTzegE1HZsQy8aQc/XyAix6hVNp+GLQjovdwa3qSxEESAct0oh7bIxsE9KkcAoBXaIbGRH9IUdx/eLPGCXcct8l8Qvi9L9l71xhyQxJW4R6jZjpC/Q7PpMTHFKFQ6XtxZjrfisnZ4JY17zvDIIaJhtA9w6sw+V/sqKJtfW1lE66e0yFN98LqUBj9SLXRZMTtOAaeJJ0nx2Lm+pWF31g+7vHJlw90/nJ/dbhKWypZBkrn0ajE4szmDcoPl7e3/D9/W6yt+fFCu28u0evQj/VM861CDf4f3UNqwP/jAe9o3yr8bP47v5MIprkH0JwAA//+D2TD6wwIAAA=="},"mode":484}]},"systemd":{"units":[{"contents":"[Unit]\nDescription=LVM Setup\nConditionFirstBoot=yes\nBefore=local-fs-pre.target\n[Service]\nType=oneshot\nRestart=on-failure\nRemainAfterExit=yes\nExecStart=/etc/systemd/system/multi-user.target.wants/lvm.sh\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"lvm-setup.service"},{"contents":"[Unit]\nDescription=Mount LVM to docker dir\n[Mount]\nWhat=/dev/vg-docker/vol_docker\nWhere=/var/lib/docker\nType=ext4\nOptions=defaults\n[Install]\nAfter=lvm-setup.service\nWantedBy=local-fs.target\n","enabled":true,"name":"var-lib-docker.mount"},{"dropins":[{"contents":"[Unit]\nAfter=var-lib-docker.mount\nRequires=var-lib-docker.mount\n","name":"10-wait-docker.conf"}],"name":"docker.service"}]}}
    

    Add this Ignition config to your cloud provider of choice now as user-data and create the given instance.