Aller au contenu principal

Os Immutable: Flatcar Linux

· 9 minutes de lecture
Stéphane ROBERT
Consultant DevOps

FlatCar Linux est un fork de CoreOS Container Linux développé par la société Kinvolk. Ce fork a été initié pour assurer la pérennité de CoreOS à la suite de l'annonce de Redhat d'acquérir CoreOS.

Concepts de FlatCar Linux

FlatCar Linux ne contient que ce qui est nécessaires pour déployer des applications dans des conteneurs, ce qui permet de limiter sa surface d'attaque.

Il est considéré comme immuable, car sa partition principale (/usr) est montée en lecture seule et il n'intègre pas de gestionnaire de packages. Pour les mises à jour, FlatCar reprend le système de mise à jour de ChromeOS. Il existe en fait deux partitions où est installé des versions différentes du système d'exploitation, mais une seule est active. Lors de la publication d'une nouvelle version, la nouvelle image est chargée sur la partition inactive. Au prochain reboot, c'est cette nouvelle version qui deviendra active. En cas de problème de démarrage, le système revient automatiquement à l'ancienne version.

La configuration des instances est fait comme pour CoreOS via un fichier de configuration au format ignition (proche de cloud-init). Via ce fichier, il est possible de :

  • Ajouter des utilisateurs et groupes
  • Créer et gérer des devices, système de fichiers, des fichiers et définir la swap.
  • De customiser les mises à jour et reboots automatiques
  • De créer des configurations réseaux
  • De définir les services à démarrer

Installation de FlatCar Linux

FlatCar Linux peut être installé sur toutes les plateformes cloud suivantes :

  • AWS EC2
  • Equinix Metal
  • Microsoft Azure
  • VMware
  • Google Compute Engine
  • DigitalOcean
  • Hetzner

Mais il peut être aussi installé sur :

  • Libvirt
  • Qemu
  • VirtualBox

On peut le tester avec Vagrant via les boxes mise à disposition. C'est ce que nous allons voir, dans un premier temps et nous verrons par la suite comment l'installer sur du libvirt avec Terraform.

Test de FlatCar Linux avec Vagrant et VirtualBox

Il faut installer au préalable VirtualBox et Vagrant :

sudo apt install virtualbox
# Pour un debian/ubuntu
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt update && sudo apt install vagrant

Maintenant on peut passer au test :

wget https://alpha.release.flatcar-linux.net/amd64-usr/current/flatcar_production_vagrant.box
vagrant box add flatcar-alpha flatcar_production_vagrant.box
vagrant init flatcar-alpha
vagrant up
vagrant ssh

On vérifie que /usr est bien en read only :

mount | grep usr
/dev/mapper/usr on /usr type ext4 (ro,relatime,seclabel)
/dev/vda6 on /usr/share/oem type btrfs (rw,nodev,relatime,seclabel,space_cache=v2,subvolid=5,subvol=/)

Installation de FlatCar avec Terraform

En m'appuyant sur la documentation du site (qui est excellente), je vais monter une VM dans ma VM de développement avec du Terraform.

Ce qui est sympathique avec Terraform c'est que la transpilation du fichier iginition est assuré par le provider posseidon/ct. Il suffit donc de créer un fichier de configuration via le système de template de Terraform.

Si vous voulez instancier sur d'autres plateformes le site du projet regorge d'exemples

On commence par écrire le fichier main.tf :

terraform {
  required_version = ">= 1.1.7"
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.6.14"
    }
    ct = {
      source  = "poseidon/ct"
      version = "0.10.0"
    }
    template = {
      source  = "hashicorp/template"
      version = "~> 2.2.0"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_pool" "volumetmp" {
  name = "${var.cluster_name}-pool"
  type = "dir"
  path = "/var/tmp/${var.cluster_name}-pool"
}

resource "libvirt_volume" "base" {
  name   = "flatcar-base"
  source = var.base_image
  pool   = libvirt_pool.volumetmp.name
  format = "qcow2"
}

resource "libvirt_volume" "vm-disk" {
  for_each = toset(var.machines)
  # workaround: depend on libvirt_ignition.ignition[each.key], otherwise the VM will use the old disk when the user-data changes
  name           = "${var.cluster_name}-${each.key}-${md5(libvirt_ignition.ignition[each.key].id)}.qcow2"
  base_volume_id = libvirt_volume.base.id
  pool           = libvirt_pool.volumetmp.name
  format         = "qcow2"
}

resource "libvirt_ignition" "ignition" {
  for_each = toset(var.machines)
  name     = "${var.cluster_name}-${each.key}-ignition"
  pool     = libvirt_pool.volumetmp.name
  content  = data.ct_config.vm-ignitions[each.key].rendered
}

resource "libvirt_domain" "machine" {
  for_each = toset(var.machines)
  name     = "${var.cluster_name}-${each.key}"
  vcpu     = var.virtual_cpus
  memory   = var.virtual_memory

  fw_cfg_name     = "opt/org.flatcar-linux/config"
  coreos_ignition = libvirt_ignition.ignition[each.key].id

  disk {
    volume_id = libvirt_volume.vm-disk[each.key].id
  }

  graphics {
    listen_type = "address"
  }

  # dynamic IP assignment on the bridge, NAT for Internet access
  network_interface {
    network_name   = "default"
    wait_for_lease = true
  }
}

data "ct_config" "vm-ignitions" {
  for_each = toset(var.machines)
  content  = data.template_file.vm-configs[each.key].rendered
}

data "template_file" "vm-configs" {
  for_each = toset(var.machines)
  template = file("${path.module}/machine-${each.key}.yaml.tmpl")

  vars = {
    ssh_keys = jsonencode(var.ssh_keys)
    name     = each.key
  }
}

output "ip-addresses" {
  value = {
    for key in var.machines :
    "${var.cluster_name}-${key}" => libvirt_domain.machine[key].network_interface.0.addresses.*
  }
}

Le fichier de variables.tf :

variable "machines" {
  type        = list(string)
  description : "Machine names, corresponding to machine-NAME.yaml.tmpl files"
}

variable "cluster_name" {
  type        = string
  description : "Cluster name used as prefix for the machine names"
}

variable "ssh_keys" {
  type        = list(string)
  description : "SSH public keys for user 'core'"
}

variable "base_image" {
  type        = string
  description : "Path to unpacked Flatcar Container Linux image flatcar_production_qemu_image.img (probably after a qemu-img resize IMG +5G)"
}

variable "virtual_memory" {
  type        = number
  default     = 2048
  description : "Virtual RAM in MB"
}

variable "virtual_cpus" {
  type        = number
  default     = 1
  description : "Number of virtual CPUs"
}

Un fichier terraform.tfvars (remplacer la clé ssh avec la vôtre) :

base_image     = file("${path.module}/flatcar_production_qemu_image-libvirt-import.img")
cluster_name   = "mycluster"
machines       = ["mynode"]
virtual_memory = 1024
ssh_keys       = ["ssh-ed25519 AA... me@mail.net"]

Nous devons récupérer l'image de base, celle définie par la variable base_image :

cd ~/Downloads
wget https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_qemu_image.img.bz2
bunzip2 flatcar_production_qemu_image.img.bz2
# optional, increase the image by 5 GB:
qemu-img resize flatcar_production_qemu_image.img +5G

Et pour finir, nous allons créer le fichier de template pour la configuration Ignition :

---
passwd:
  users:
    - name: core
      ssh_authorized_keys: ${ssh_keys}
storage:
  files:
    - path: /home/core/works
      filesystem: root
      mode: 0755
      contents:
        inline: |
          #!/bin/bash
          set -euo pipefail
          hostname="$(hostname)"
          echo My name is ${name} and the hostname is $${hostname}

On applique :

terraform init
terraform apply --auto-approve

Outputs:

ip-addresses = {
  "mycluster-mynode" = tolist([
    "192.168.122.45",
  ])
}

Et on lance le script créé par la configuration ignition :

ssh core@192.168.122.45
core@mycluster ~ $ ./works
My name is mynode and the hostname is mycluster

Docker est opérationnel ?

core@mycluster ~ $ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Mais comment ajouter des packages à FlatCar Linux ?

Customisation de FlatCar Linux

Il est possible de modifier la configuration Ignition et tout est très bien documenté sur le site officiel

Ajouter des packages ?

Il est possible de customiser FlatCar Linux en lui ajoutant des packages par exemple. Pour cela il faut ajouter des packages Gentoo via le SDK de FlatCar Linux. Tiré de la documentation du site

Pourquoi pas lui ajouter zsh (pour faire simple) ?

Il faut dans un premier temps récupérer le SDK de FlatCar Linux :

git clone --recurse-submodules https://github.com/flatcar-linux/scripts.git
cd scripts

Nous sommes sur la branche main du projet et donc la dernière version nigthly. Pour changer de version, il suffit de regarder dans les branches et de basculer sur celle désirée :

git tag -l | grep -E 'stable-[0-9.]+$'
...
stable-3033.2.0
stable-3033.2.1
stable-3033.2.2
stable-3033.2.3
stable-3033.2.4
stable-3139.2.0
...

git checkout stable-3139.2.0

Pour lancer le SDK, qui tourne dans container, on a à disposition un script. Ce script se charge de récupérer l'image du sdk correspondant à la branche dans laquelle vous avez basculé :

./run_sdk_container -t
###### Writing versionfile 'sdk_container/.repo/manifests/version.txt' to SDK '3200.0.0', OS '3200.0.0+nightly-20220408-0155'. ######

###### Creating a new container 'flatcar-sdk-all-3200.0.0_os-alpha-3200.0.0-nightly-20220408-0155' ######
3200.0.0: Pulling from flatcar-linux/flatcar-sdk-all
7d387896cfc0: Extracting [==================>                                ]  1.315GB/3.547GB
49afac011620: Download complete
7a0d64bb993d: Download complete
64465755da82: Download complete
9b0499ad5de5: Download complete
Emaint: fix world          100% [============================================>]
INFO    setup_board: Elapsed time (setup_board): 0m5s
INFO    setup_board: The SYSROOT is: /build/arm64-usr

Il faut ensuite récupérer le projet Gentoo :

git clone --depth 5 https://github.com/gentoo/gentoo.git

Avant tout ajout, il faut builder tous les packages (c'est assez long !) :

./build_packages

On recherche zsh dans le projet Gentoo :

sdk@flatcar-sdk-all-3139_0_0_os-stable-3139_2_0 ~/trunk/src/scripts $ find gentoo -name zsh

Il faut copier ce répertoire dans celui de FlatCar Linux et lancer sa compilation :

mkdir -p ../third_party/portage-stable/app-shells
cp -R gentoo/app-shells/zsh ../third_party/portage-stable/app-shells
emerge-amd64-usr --newuse app-shells/zsh

Il faudra répéter l'opération pour chaque dépendance manquante !

Il faut ensuite ajouter au fichier de description de FlatCar zsh :

vim ../third_party/coreos-overlay/coreos-base/coreos/coreos-0.0.1.ebuild
RDEPEND="${RDEPEND}
...
        app-misc/jq
        app-misc/pax-utils
        app-shells/bash
        app-shells/bash
        coreos-base/afterburn
        coreos-base/coreos-cloudinit
...

Ensuite on lance le build :

emerge-amd64-usr coreos-base/coreos
./build_image
./image_to_vm.sh --from=../build/images/amd64-usr/latest --format qemu

On sort de la session docker en cours, on copie la nouvelle image à la place de celle utilisée dans la configuration Terraform.

cp flatcar_production_qemu_image.img /home/vagrant/Projets/homelab/test-flatcar/flatcar_production_qemu_image-libvirt-import.img
terraform apply

Outputs:

ip-addresses = {
  "mycluster-mynode" = tolist([
    "192.168.122.45",
  ])
}

On vérifie que zsh est disponible :

ssh core@192.168.122.45
Last login: Fri Apr  8 06:59:28 UTC 2022 from 192.168.122.1 on pts/0
Flatcar Container Linux by Kinvolk developer 3202.0.0+nightly-20220407-0155 for QEMU
core@mycluster ~ $ zsh
mycluster%

Voilà on a réussi, j'ai testé avec docker-compose et ça fonctionne également, mais il faut copier pas mal de dépendances de dev-python. La liste :

pycparser, urllib3, six, idna, charset-normalizer, cffi, certifi, websocket-client, requests, pynacl, cryptography, bcrypt, pyrsistent, paramiko, docker, attrs, texttable, PyYAML, python-dotenv, jsonschema, docopt, dockerpty, distro, docker-compose

Dans un contexte de production, il y aura du travail pour créer un CI permettant de builder en automatique ce genre d'image et de les stocker.

Plus loin

C'est franchement une belle découverte. J'imagine déjà pas mal de cas d'utilisation. Vu que ssh et python fonctionne pourquoi pas utiliser Ansible pour installer des produits du type rundeck. Je pense que cette distribution a pas mal de potentiels, pas étonnant qu'on la retrouve chez tous les clouders.