Os Immutable: Flatcar Linux
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.