🚔

Flatcar Container Linux を試してみる

2024/11/04に公開

先日 2024/10/29 の CNCF blog で Flatcar Container Linux が incubating project に採択されたとの記事がありました。

https://www.cncf.io/blog/2024/10/29/flatcar-brings-container-linux-to-the-cncf-incubator/

Flatcar は今まで存在を知らなかったのですが、面白そうだったのでどんなことができるか試してみます。

Flatcar 概要

Flatcar Container Linux (以下 Flatcar と表記) はコンテナワークロードを実行するためにカスタマイズされた軽量なオープンソースの OS です。

https://www.flatcar.org/

Flatcar の元となった CoreOS Container Linux はコンテナワークロード向けの OS として CoreOS (企業) により開発されてきましたが、2018 年に RedHat に買収され、2020 年にサポート終了となりました。後継プロジェクトとしては同じくコンテナワークロード向け OS として Fedora CoreOS が誕生し、こちらは現在でも開発が続けられています。
一方で Flatcar も上記の CoreOS から派生したプロジェクトとなっており、コンテナワークロード用に最適化された軽量・最小限のみのパッケージを含む・immutable な構成の OS として開発されています。
このようなコンセプトを持つ OS は以下のようにいくつかありますが、flatcar もそのような OS の一つとなっています。

特徴

Flatcar の特徴は概要ページにまとまっていますが、Ubuntu や RockyLinux などの汎用的な OS と比較すると特に以下の点が特徴的です。

package manager がない

Flatcar では apt や dnf などのパッケージマネージャーが含まれていないので OS 内で追加のパッケージをインストールすることができません(バイナリを直接ダウンロードしてくることは可能)。必要なパッケージは起動時の userdata で systemd-sysext を利用した独自のパッケージで追加したりカスタムイメージで対応することになるので、OS 内で色々コマンドを実行してパッケージを追加するのではなく、OS 作成前に構成を宣言的に記述する IaC に近い概念となっています。

/usr が read-only

Flatcar では /usr ディレクトリ以下が read-only となっており、システムに関連するファイル等を変更できないようにすることでよりセキュアな構成にしています。概要ページの中の Immutable filesystem がこれに対応。

使ってみる

Flatcar は下記ドキュメントの通り様々な環境で動かせるようになっています。

https://www.flatcar.org/docs/latest/

  • Cloud Providers
    • AWS, Azure, Google Cloud など主要なクラウドプロバイダーで登録済みのマシンイメージを使って仮想マシンとして起動する方法。
    • 気軽に試せる。
  • Virtualization options
    • 仮想マシン用のイメージをダウンロードしてオンプレや手元の環境で Qemu, Vagrant, VirtualBox などで仮想マシンとして起動する方法。
    • 手元で試すなら最もコストが低い。
  • Bare Metal
    • ISO イメージ等をダウンロードして物理マシンに直接インストールする方法。
    • 上記のいずれかで済むのであまり使う機会はなさそう。

自宅の環境では Openstack を使っているので、ここでは Openstack の手順に従って仮想マシンとして起動する方法を試してみます。

インストール

Flatcar は用途に合わせて stable, beta, alpha, LTS の 4 つのリリースチャンネルがあります。

https://www.flatcar.org/releases

チャンネルによって Flatcar のバージョンが異なり、containerd やカーネルのバージョン、リリーススケジュールの差異などがあります。詳細は Managing Releases を参照。
ここでは Stable チャンネルのイメージを使用します。

以下のコマンドで openstack 用のイメージをダウンロード。これにより stable チャンネルにおける最新バージョン 3975.2.2 の flatcar イメージがダウンロードされます。

$ wget https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_openstack_image.img.bz2
$ bunzip2 flatcar_production_openstack_image.img.bz2

openstack (glance) にイメージを登録

openstack image create \
  --container-format bare \
  --disk-format qcow2 \
  --public \
  --property hw_qemu_guest_agent=yes \
  --file ./flatcar_production_openstack_image.img \
  flatcar-3975.2.2-amd64

後は通常の VM と同様にイメージや flavor, ネットワークを指定して作成することで flatcar が起動します。

openstack server create \
  --image flatcar-3975.2.2-amd64 \
  --key-name kolla_ssh \
  --flavor m1.small \
  --network demo-net \
  flatcar-test

起動したマシンにはデフォルトユーザー core で SSH できます。

$ ssh -i ~/.ssh/kolla_ssh core@192.168.3.186
Last login: Thu Oct 31 13:25:14 UTC 2024 from 192.168.3.27 on pts/0
Flatcar Container Linux by Kinvolk stable 3975.2.2 for Openstack
core@test ~ $

/usr は read-only になっているので書き込みできません。

core@test /usr $ touch test
touch: cannot touch 'test': Read-only file system

systemd で以下のようなサービスが動いています。

サービス一覧
  audit-rules.service                           loaded active exited  Load Security Auditing Rules
  clean-ca-certificates.service                 loaded active exited  Clean up broken links in /etc/ssl/certs
  containerd.service                            loaded active running containerd container runtime
  coreos-metadata-sshkeys@core.service          loaded active exited  Flatcar Metadata Agent (SSH Keys)
  coreos-metadata.service                       loaded active exited  Flatcar Metadata Agent
  dbus.service                                  loaded active running D-Bus System Message Bus
  dracut-shutdown.service                       loaded active exited  Restore /run/initramfs on shutdown
  ensure-sysext.service                         loaded active exited  ensure-sysext.service
  flatcar-tmpfiles.service                      loaded active exited  Create missing system files
  getty@tty1.service                            loaded active running Getty on tty1
  kmod-static-nodes.service                     loaded active exited  Create List of Static Device Nodes
  kubelet.service                               loaded active running kubelet: The Kubernetes Node Agent
  ldconfig.service                              loaded active exited  Rebuild Dynamic Linker Cache
  lvm2-activation-early.service                 loaded active exited  Activation of LVM2 logical volumes
  lvm2-activation.service                       loaded active exited  Activation of LVM2 logical volumes
  nvidia.service                                loaded active exited  NVIDIA Configure Service
  polkit.service                                loaded active running Authorization Manager
  qemu-guest-agent.service                      loaded active running QEMU Guest Agent
  serial-getty@ttyS0.service                    loaded active running Serial Getty on ttyS0
  sshd-keygen.service                           loaded active exited  Generate sshd host keys
  sshd@0-10.0.0.254:22-10.0.0.30:47218.service  loaded active running OpenSSH per-connection server daemon (10.0.0.30:47218)
  sshkeys.service                               loaded active exited  sshkeys.service
  systemd-fsck@dev-disk-by\x2dlabel-OEM.service loaded active exited  File System Check on /dev/disk/by-label/OEM
  systemd-hwdb-update.service                   loaded active exited  Rebuild Hardware Database
  systemd-journal-catalog-update.service        loaded active exited  Rebuild Journal Catalog
  systemd-journal-flush.service                 loaded active exited  Flush Journal to Persistent Storage
  systemd-journald.service                      loaded active running Journal Service
  systemd-logind.service                        loaded active running User Login Management
  systemd-machine-id-commit.service             loaded active exited  Commit a transient machine-id on disk
  systemd-modules-load.service                  loaded active exited  Load Kernel Modules
  systemd-network-generator.service             loaded active exited  Generate network units from Kernel command line
  systemd-networkd-wait-online.service          loaded active exited  Wait for Network to be Configured
  systemd-networkd.service                      loaded active running Network Configuration
  systemd-random-seed.service                   loaded active exited  Load/Save OS Random Seed
  systemd-remount-fs.service                    loaded active exited  Remount Root and Kernel File Systems
  systemd-resolved.service                      loaded active running Network Name Resolution
  systemd-sysctl.service                        loaded active exited  Apply Kernel Variables
  systemd-sysext.service                        loaded active exited  Merge System Extension Images into /usr/ and /opt/
  systemd-sysusers.service                      loaded active exited  Create System Users
  systemd-timesyncd.service                     loaded active running Network Time Synchronization
  systemd-tmpfiles-setup-dev-early.service      loaded active exited  Create Static Device Nodes in /dev gracefully
  systemd-tmpfiles-setup-dev.service            loaded active exited  Create Static Device Nodes in /dev
  systemd-tmpfiles-setup.service                loaded active exited  Create Volatile Files and Directories
  systemd-udev-settle.service                   loaded active exited  Wait for udev To Complete Device Initialization
  systemd-udev-trigger.service                  loaded active exited  Coldplug All udev Devices
  systemd-udevd.service                         loaded active running Rule-based Manager for Device Events and Files
  systemd-update-done.service                   loaded active exited  Update is Completed
  systemd-update-utmp.service                   loaded active exited  Record System Boot/Shutdown in UTMP
  systemd-user-sessions.service                 loaded active exited  Permit User Sessions
  systemd-userdbd.service                       loaded active running User Database Manager
  systemd-vconsole-setup.service                loaded active exited  Virtual Console Setup
  update-engine.service                         loaded active running Update Engine
  update-ssh-keys-after-ignition.service        loaded active exited  Run update-ssh-keys once after Ignition
  user-runtime-dir@500.service                  loaded active exited  User Runtime Directory /run/user/500
  user@500.service                              loaded active running User Manager for UID 500

docker, containerd などコンテナ向け CLI の他、curl, wget, top, git などの基本的なコマンドはインストール済みになっています。一方で python や go などのプログラミング言語は入っていません。

core ユーザーの uid が 500 で root, core しかユーザーがいないのは珍しいかもしれません。

core@test ~ $ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
core:x:500:500:Flatcar Admin:/home/core:/bin/bash

Ignition による構成のカスタマイズ

Flatcar では OS 内でコマンドを実行して構成を変更するよりも宣言的に構成を記述する方法が推奨されているため、イメージや起動時の処理をカスタマイズする方法がいくつか提供されています。この中の 1 つである Ignition では OS 起動時にサービスの起動やファイルの作成などを記述できます。OS 起動時に実行したい処理を宣言的に記述するという点で、基本的には cloud-init と同じような感覚で利用できます。

Ignition 用の設定ファイルは json 形式で記述する必要がありますが、例えば nginx の docker コンテナを起動時に systemd で起動させるには以下のように書きます (ドキュメントのサンプルより)。

{
  "ignition": {
    "version": "3.3.0"
  },
  "systemd": {
    "units": [
      {
        "contents": "[Unit]\nDescription=NGINX example\nAfter=docker.service\nRequires=docker.service\n[Service]\nTimeoutStartSec=0\nExecStartPre=-/usr/bin/docker rm --force nginx1\nExecStart=/usr/bin/docker run --name nginx1 --pull always --net host docker.io/nginx:1\nExecStop=/usr/bin/docker stop nginx1\nRestart=always\nRestartSec=5s\n[Install]\nWantedBy=multi-user.target\n",
        "enabled": true,
        "name": "nginx.service"
      }
    ]
  }
}

見てわかるように Ignition の設定ファイルは読み書きしたり解読するには非常に分かりづらいので、人間が読み書きしやすい yml 形式で内容を記述し、これを Ignition が認識できる Json に変換するための Butane というユーティリティツールが提供されています。上記の内容を Butane の形式で書くと以下のようになります。

nginx.yml
variant: flatcar
version: 1.0.0
systemd:
  units:
    - name: nginx.service
      enabled: true
      contents: |
        [Unit]
        Description=NGINX example
        After=docker.service
        Requires=docker.service
        [Service]
        TimeoutStartSec=0
        ExecStartPre=-/usr/bin/docker rm --force nginx1
        ExecStart=/usr/bin/docker run --name nginx1 --pull always --log-driver=journald --net host docker.io/nginx:1
        ExecStop=/usr/bin/docker stop nginx1
        Restart=always
        RestartSec=5s
        [Install]
        WantedBy=multi-user.target

意味のあるまとまりごとに改行・インデントされてだいぶ見やすくなりました。

このように Ignition を使って起動時の処理をカスタマイズする場合、基本的にはまず yaml 形式で処理を記述し、Butane で Ignition の json に変換、OS 起動時に userdata として指定することで適用という流れになります。

では実際に上記の ignition 用の yaml ファイルを作成し、OS 起動時に nginx のコンテナが起動する動作を確認してみます。butane は github でバイナリが配布されていますが、コンテナイメージもあるのでここではそれを使います。

Butane では変換対象の yaml を入力として受け取り、変換後の json を出力するので、それをファイルにリダイレクトします、

cat nginx.yml | docker run --rm -i quay.io/coreos/butane:release > ignition.json

作成した json を仮想マシン作成時の userdata に適用することで、OS 起動時に json に記載された処理が実行されます。openstack では VM 作成時に --user-data にファイルを指定することで userdata として解釈されるので、以下のコマンドで新しい flatcar VM を作成します。イメージやフレーバー、ネットワークは使用している環境に合わせて指定。

openstack server create \
  --user-data ./ignition.json \
  --image flatcar-3975.2.2-amd64 \
  --key-name kolla_ssh \
  --flavor m1.small \
  --network demo-net \
  flatcar-test

起動した VM に SSH ログインすると実際に nginx サービスが稼働していることが確認できます。
systemd 配下で管理されていますが、ignition で記述したとおり実態は nginx イメージを利用した docker コンテナのプロセスが起動しています。

core@flatcar-test ~ $ sudo systemctl status nginx
● nginx.service - NGINX example
     Loaded: loaded (/etc/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Fri 2024-11-01 10:52:28 UTC; 3min 32s ago
    Process: 1574 ExecStartPre=/usr/bin/docker rm --force nginx1 (code=exited, status=0/SUCCESS)
   Main PID: 1579 (docker)
      Tasks: 6 (limit: 15356)
     Memory: 49.1M (peak: 49.3M)
        CPU: 96ms
     CGroup: /system.slice/nginx.service
             └─1579 /usr/bin/docker run --name nginx1 --pull always --log-driver=journald --net host docker.io/nginx:1

Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: /docker-entrypoint.sh: Configuration complete; ready for start up
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: using the "epoll" event method
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: nginx/1.27.2
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: OS: Linux 6.6.54-flatcar
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: start worker processes
Nov 01 10:52:52 flatcar-test.novalocal docker[1579]: 2024/11/01 10:52:52 [notice] 1#1: start worker process 29
Nov 01 10:55:53 flatcar-test.novalocal docker[1579]: ::1 - - [01/Nov/2024:10:55:53 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.7.1" "-"

OS 起動時のログを見て Ignition の処理が実際に実行されていることも見ておきます。
/var/log/ 配下のログは以下のようになっており、ubuntu 等の通常の OS と比較するとすっきりしている印象を受けます。

core@flatcar-test ~ $ ls -l /var/log/
total 24
drwxr-x---. 2 root root              4096 Nov  1 10:52 audit
-rw-rw----. 1 root utmp                 0 Nov  1 10:52 btmp
-rw-r--r--. 1 root root                 0 Nov  1 10:52 faillog
drwxr-sr-x. 4 root systemd-journal   4096 Nov  1 10:52 journal
-rw-rw-r--. 1 root utmp            146292 Nov  1 10:55 lastlog
drwx------. 2 root root              4096 Nov  1 10:52 private
drwx------. 2 root root              4096 Nov  1 10:52 sssd
-rw-rw-r--. 1 root utmp              3456 Nov  1 10:55 wtmp

システム関連のログは syslog や messages ではなく systemd-journald によって journal に出力されます。sudo journalctl > journal.log でファイルに書き出すと Ignition がいろいろなセットアップを行っていることが確認できます。

Ignition の出力
core@flatcar-test ~ $ cat journal.log  | grep ignition
Nov 01 10:52:13 localhost systemd[1]: Starting ignition-setup-pre.service - Ignition env setup...
Nov 01 10:52:13 localhost systemd[1]: Finished ignition-setup-pre.service - Ignition env setup.
Nov 01 10:52:15 localhost systemd[1]: Starting ignition-setup.service - Ignition (setup)...
Nov 01 10:52:15 localhost systemd[1]: Finished ignition-setup.service - Ignition (setup).
Nov 01 10:52:15 localhost systemd[1]: Starting ignition-fetch-offline.service - Ignition (fetch-offline)...
Nov 01 10:52:15 localhost ignition[723]: Ignition 2.18.0
Nov 01 10:52:15 localhost ignition[723]: Stage: fetch-offline
Nov 01 10:52:15 localhost systemd[1]: Finished ignition-fetch-offline.service - Ignition (fetch-offline).
Nov 01 10:52:15 localhost ignition[723]: no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:15 localhost ignition[723]: no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:15 localhost ignition[723]: parsed url from cmdline: ""
Nov 01 10:52:15 localhost ignition[723]: no config URL provided
Nov 01 10:52:15 localhost ignition[723]: reading system config file "/usr/lib/ignition/user.ign"
Nov 01 10:52:15 localhost ignition[723]: no config at "/usr/lib/ignition/user.ign"
Nov 01 10:52:15 localhost ignition[723]: failed to fetch config: resource requires networking
Nov 01 10:52:15 localhost ignition[723]: Ignition finished successfully
Nov 01 10:52:16 localhost systemd[1]: Starting ignition-fetch.service - Ignition (fetch)...
Nov 01 10:52:16 localhost ignition[739]: Ignition 2.18.0
Nov 01 10:52:16 localhost ignition[739]: Stage: fetch
Nov 01 10:52:16 localhost ignition[739]: no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:16 localhost ignition[739]: no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:16 localhost ignition[739]: parsed url from cmdline: ""
Nov 01 10:52:16 localhost ignition[739]: no config URL provided
Nov 01 10:52:16 localhost ignition[739]: reading system config file "/usr/lib/ignition/user.ign"
Nov 01 10:52:16 localhost ignition[739]: no config at "/usr/lib/ignition/user.ign"
Nov 01 10:52:16 localhost ignition[739]: config drive ("/dev/disk/by-label/config-2") not found. Waiting...
Nov 01 10:52:16 localhost ignition[739]: config drive ("/dev/disk/by-label/CONFIG-2") not found. Waiting...
Nov 01 10:52:16 localhost ignition[739]: GET http://169.254.169.254/openstack/latest/user_data: attempt #1
Nov 01 10:52:16 localhost ignition[739]: GET result: OK
Nov 01 10:52:16 localhost ignition[739]: parsing config with SHA512: 2b8506e6288fefe7b8410bf75504c5f38e7139d3fbe90e8da6d0918e3993ef2c864e76f5d2caebbdc65687a2711076ade6a150a4a10784cdf4bd9365f02ab2e5
Nov 01 10:52:16 localhost ignition[739]: fetched base config from "system"
Nov 01 10:52:16 localhost ignition[739]: fetch: fetch complete
Nov 01 10:52:16 localhost ignition[739]: fetched base config from "system"
Nov 01 10:52:16 localhost ignition[739]: fetch: fetch passed
Nov 01 10:52:16 localhost ignition[739]: fetched user config from "openstack"
Nov 01 10:52:16 localhost ignition[739]: Ignition finished successfully
Nov 01 10:52:16 localhost systemd[1]: Finished ignition-fetch.service - Ignition (fetch).
Nov 01 10:52:16 localhost systemd[1]: Starting ignition-kargs.service - Ignition (kargs)...
Nov 01 10:52:16 localhost ignition[744]: Ignition 2.18.0
Nov 01 10:52:16 localhost ignition[744]: Stage: kargs
Nov 01 10:52:16 localhost ignition[744]: no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:16 localhost ignition[744]: no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:16 localhost ignition[744]: kargs: kargs passed
Nov 01 10:52:16 localhost systemd[1]: Finished ignition-kargs.service - Ignition (kargs).
Nov 01 10:52:16 localhost ignition[744]: Ignition finished successfully
Nov 01 10:52:16 localhost systemd[1]: Starting ignition-disks.service - Ignition (disks)...
Nov 01 10:52:16 localhost ignition[749]: Ignition 2.18.0
Nov 01 10:52:16 localhost ignition[749]: Stage: disks
Nov 01 10:52:16 localhost ignition[749]: no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:16 localhost ignition[749]: no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:16 localhost ignition[749]: disks: disks passed
Nov 01 10:52:16 localhost ignition[749]: Ignition finished successfully
Nov 01 10:52:16 localhost systemd[1]: Finished ignition-disks.service - Ignition (disks).
Nov 01 10:52:16 localhost systemd[1]: ignition-remount-sysroot.service - Remount /sysroot read-write for Ignition was skipped because of an unmet condition check (ConditionPathIsReadWrite=!/sysroot).
Nov 01 10:52:16 localhost systemd[1]: Reached target ignition-diskful.target - Ignition Boot Disk Setup.
Nov 01 10:52:16 localhost systemd[1]: Starting ignition-mount.service - Ignition (mount)...
Nov 01 10:52:16 localhost ignition[881]: INFO     : Ignition 2.18.0
Nov 01 10:52:16 localhost ignition[881]: INFO     : Stage: mount
Nov 01 10:52:16 localhost ignition[881]: INFO     : no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:16 localhost ignition[881]: INFO     : no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:16 localhost ignition[881]: INFO     : mount: mount passed
Nov 01 10:52:16 localhost ignition[881]: INFO     : Ignition finished successfully
Nov 01 10:52:16 localhost systemd[1]: Finished ignition-mount.service - Ignition (mount).
Nov 01 10:52:23 localhost systemd[1]: Starting ignition-files.service - Ignition (files)...
Nov 01 10:52:23 localhost ignition[913]: INFO     : Ignition 2.18.0
Nov 01 10:52:23 localhost ignition[913]: INFO     : Stage: files
Nov 01 10:52:23 localhost ignition[913]: INFO     : no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:23 localhost ignition[913]: INFO     : no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:23 localhost ignition[913]: DEBUG    : files: compiled without relabeling support, skipping
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(1): [started]  processing unit "nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(1): op(2): [started]  writing unit "nginx.service" at "/sysroot/etc/systemd/system/nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(1): op(2): [finished] writing unit "nginx.service" at "/sysroot/etc/systemd/system/nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(1): [finished] processing unit "nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(3): [started]  setting preset to enabled for "nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: op(3): [finished] setting preset to enabled for "nginx.service"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: createResultFile: createFiles: op(4): [started]  writing file "/sysroot/etc/.ignition-result.json"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: createResultFile: createFiles: op(4): [finished] writing file "/sysroot/etc/.ignition-result.json"
Nov 01 10:52:23 localhost ignition[913]: INFO     : files: files passed
Nov 01 10:52:23 localhost ignition[913]: INFO     : Ignition finished successfully
Nov 01 10:52:23 localhost systemd[1]: Finished ignition-files.service - Ignition (files).
Nov 01 10:52:23 localhost systemd[1]: Starting ignition-quench.service - Ignition (record completion)...
Nov 01 10:52:23 localhost systemd[1]: Starting initrd-setup-root-after-ignition.service - Root filesystem completion...
Nov 01 10:52:23 localhost systemd[1]: ignition-quench.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Finished ignition-quench.service - Ignition (record completion).
Nov 01 10:52:23 localhost initrd-setup-root-after-ignition[933]: grep: /sysroot/etc/flatcar/enabled-sysext.conf: No such file or directory
Nov 01 10:52:23 localhost initrd-setup-root-after-ignition[933]: grep: /sysroot/usr/share/flatcar/enabled-sysext.conf: No such file or directory
Nov 01 10:52:23 localhost initrd-setup-root-after-ignition[937]: grep: /sysroot/etc/flatcar/enabled-sysext.conf: No such file or directory
Nov 01 10:52:23 localhost systemd[1]: Finished initrd-setup-root-after-ignition.service - Root filesystem completion.
Nov 01 10:52:23 localhost systemd[1]: Reached target ignition-complete.target - Ignition Complete.
Nov 01 10:52:23 localhost systemd[1]: Stopped target ignition-complete.target - Ignition Complete.
Nov 01 10:52:23 localhost systemd[1]: Stopped target ignition-diskful.target - Ignition Boot Disk Setup.
Nov 01 10:52:23 localhost systemd[1]: initrd-setup-root-after-ignition.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped initrd-setup-root-after-ignition.service - Root filesystem completion.
Nov 01 10:52:23 localhost systemd[1]: ignition-files.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-files.service - Ignition (files).
Nov 01 10:52:23 localhost systemd[1]: Stopping ignition-mount.service - Ignition (mount)...
Nov 01 10:52:23 localhost ignition[957]: INFO     : Ignition 2.18.0
Nov 01 10:52:23 localhost ignition[957]: INFO     : Stage: umount
Nov 01 10:52:23 localhost ignition[957]: INFO     : no configs at "/usr/lib/ignition/base.d"
Nov 01 10:52:23 localhost ignition[957]: INFO     : no config dir at "/usr/lib/ignition/base.platform.d/openstack"
Nov 01 10:52:23 localhost ignition[957]: INFO     : umount: umount passed
Nov 01 10:52:23 localhost ignition[957]: INFO     : Ignition finished successfully
Nov 01 10:52:23 localhost systemd[1]: ignition-mount.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-mount.service - Ignition (mount).
Nov 01 10:52:23 localhost systemd[1]: ignition-disks.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-disks.service - Ignition (disks).
Nov 01 10:52:23 localhost systemd[1]: ignition-kargs.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-kargs.service - Ignition (kargs).
Nov 01 10:52:23 localhost systemd[1]: ignition-fetch.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-fetch.service - Ignition (fetch).
Nov 01 10:52:23 localhost systemd[1]: ignition-fetch-offline.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-fetch-offline.service - Ignition (fetch-offline).
Nov 01 10:52:23 localhost systemd[1]: ignition-setup.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-setup.service - Ignition (setup).
Nov 01 10:52:23 localhost systemd[1]: ignition-setup-pre.service: Deactivated successfully.
Nov 01 10:52:23 localhost systemd[1]: Stopped ignition-setup-pre.service - Ignition env setup.
Nov 01 10:52:24 flatcar-test.novalocal systemd[1]: ignition-delete-config.service - Ignition (delete config) was skipped because no trigger condition checks were met.
Nov 01 10:52:25 flatcar-test.novalocal systemd[1]: ignition-delete-config.service - Ignition (delete config) was skipped because no trigger condition checks were met.
Nov 01 10:52:25 flatcar-test.novalocal systemd[1]: ignition-delete-config.service - Ignition (delete config) was skipped because no trigger condition checks were met.
Nov 01 10:52:25 flatcar-test.novalocal systemd[1]: ignition-delete-config.service - Ignition (delete config) was skipped because no trigger condition checks were met.
Nov 01 10:52:25 flatcar-test.novalocal systemd[1]: ignition-delete-config.service - Ignition (delete config) was skipped because no trigger condition checks were met.
Nov 01 10:52:26 flatcar-test.novalocal systemd[1]: Starting update-ssh-keys-after-ignition.service - Run update-ssh-keys once after Ignition...
Nov 01 10:52:26 flatcar-test.novalocal systemd[1]: Finished update-ssh-keys-after-ignition.service - Run update-ssh-keys once after Ignition.

Ignition を使うことで OS 起動時にサービスの追加やファイルの追加・更新、ユーザー追加などを行えます。サポートされているフィールドは以下を参照。

上記で見たように systemd のサービスも追加できますが、package manager がないので一般的な OS でよくある必要なパッケージを userdata 内でインストールするといった使い方はできません。何らかのアプリケーションを起動時に実行するような場合は事前にコンテナ化しておいて docker コンテナとして起動する、もしくは後述の sysext を利用するなどの工夫が必要になります。

Kubernetes で使う

Flatcar はコンテナワークロードを実行するために最適化された OS なのでもちろん k8s クラスタのノードとして使用できます。k8s 用のセットアップについては以下にドキュメントがあるので、こちらを参考に k8s クラスタに組み込んでみます。

https://www.flatcar.org/docs/latest/container-runtimes/getting-started-with-kubernetes/

worker node として追加する

Flatcar は control plane 用のノード、worker ノードのどちらでも使えます。ここでは ubuntu ノードで構成された既存の k8s クラスタに worker ノードとして参加させる動作を試してみます。

前述のように Flatcar では package manager がないので、kubectl や kubeadm といった k8s 運用に必要なツールは Ignition でインストールするように構成する必要があります。ドキュメントでは systemd-sysext で kubectl, kubeadm をインストールする方法と、バイナリをダウンロードして /opt/bin に配置する方法が記載されています。systemd-sysext を使う方法では systemd-sysupdate と組み合わせることでパッケージマネージャーのように機能し、r新しいバージョンが提供された際にノード再起動時に自動で適用されるメリットがあるそうです。

ここではどちらの方法でも特に差はないので systemd-sysext を使う方法を試します。まずドキュメントの通りに Ignition 用の yaml ファイルを用意します。
この Ignition 構成では、起動時に sysext で提供される kubernetes のダウンロードや kubeadm で worker node としてクラスタに参加する処理を実行します。ドキュメントでは k8s バージョンが 1.27 となっていますが 1.30 に書き換えました。提供されているバージョンは https://github.com/flatcar/sysext-bakery の Github Release から確認できます。

k8s.yml
---
version: 1.0.0
variant: flatcar
storage:
  links:
    - target: /opt/extensions/kubernetes/kubernetes-v1.30.0-x86-64.raw
      path: /etc/extensions/kubernetes.raw
      hard: false
  files:
    - path: /etc/sysupdate.kubernetes.d/kubernetes-v1.30.conf
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-v1.30.conf
    - path: /etc/sysupdate.d/noop.conf
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/noop.conf
    - path: /opt/extensions/kubernetes/kubernetes-v1.30.0-x86-64.raw
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-v1.30.0-x86-64.raw
systemd:
  units:
    - name: systemd-sysupdate.timer
      enabled: true
    - name: systemd-sysupdate.service
      dropins:
        - name: kubernetes.conf
          contents: |
            [Service]
            ExecStartPre=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/kubernetes.raw > /tmp/kubernetes"
            ExecStartPre=/usr/lib/systemd/systemd-sysupdate -C kubernetes update
            ExecStartPost=/usr/bin/sh -c "readlink --canonicalize /etc/extensions/kubernetes.raw > /tmp/kubernetes-new"
            ExecStartPost=/usr/bin/sh -c "if ! cmp --silent /tmp/kubernetes /tmp/kubernetes-new; then touch /run/reboot-required; fi"
    - name: locksmithd.service
      # NOTE: To coordinate the node reboot in this context, we recommend to use Kured.
      mask: true
    - name: kubeadm.service
      enabled: true
      contents: |
        [Unit]
        Description=Kubeadm service
        Requires=containerd.service
        After=containerd.service
        ConditionPathExists=!/etc/kubernetes/kubelet.conf
        [Service]
        ExecStart=/usr/bin/kubeadm join $(output from 'kubeadm token create --print-join-command')
        [Install]
        WantedBy=multi-user.target

既存の k8s クラスタでは kubeadm を使ってクラスタを管理しているので、control plane 用のノードで以下のコマンドを実行して worker node を追加するための token を発行します。

$ sudo kubeadm token create --print-join-command
kubeadm join 10.0.0.40:6443 --token di8j2u.ym0uxb5ibhy8xx42 --discovery-token-ca-cert-hash sha256:53909cf7fb4b9a26f0b5b6a36f30c1b43e34d76c02f5b7ce5f3d2a4d1d6c413c

k8s.yml のプレースホルダーの部分を出力されたコマンドで置き換えます。

k8s.yml
        [Service]
-        ExecStart=/usr/bin/kubeadm join $(output from 'kubeadm token create --print-join-command')
+        ExecStart=/usr/bin/kubeadm join 10.0.0.40:6443 --token di8j2u.ym0uxb5ibhy8xx42 --discovery-token-ca-cert-hash sha256:53909cf7fb4b9a26f0b5b6a36f30c1b43e34d76c02f5b7ce5f3d2a4d1d6c413c

butane で json に変換。

cat k8s.yml | docker run --rm -i quay.io/coreos/butane:release > ignition.json

この ignition json を userdata に指定して flatcar VM を作成。

openstack server create \
  --user-data ./ignition.json \
  --image flatcar-3975.2.2-amd64 \
  --key-name kolla_ssh \
  --flavor m1.medium \
  --network demo-net \
  flatcar-test

正常に起動すると、作成した flatcar VM (flatcar-test.novalocal) が既存のクラスタに worker node として追加されていることが確認できます。

NAME                     STATUS   ROLES           AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                                             KERNEL-VERSION       CONTAINER-RUNTIME
flatcar-test.novalocal   Ready    <none>          2m28s   v1.30.0   10.0.0.254    <none>        Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)   6.6.54-flatcar       containerd://1.7.17
kata-m1                  Ready    control-plane   34d     v1.30.0   10.0.0.30     <none>        Ubuntu 22.04.5 LTS                                   5.15.0-124-generic   containerd://1.7.22
kata-w1                  Ready    <none>          34d     v1.30.0   10.0.0.31     <none>        Ubuntu 22.04.5 LTS                                   5.15.0-124-generic   containerd://1.7.22

status が ready になるには flatcar node 上に CNI が展開されてクラスタと通信できる必要があります。現時点でテスト済みの CNI は以下のとのこと。

  • Cilium
  • Flannel
  • Calico

あとは通常の OS のノードと同様に pod の配置などが行えます。

通常ノードとの比較

ところで flatcar はコンテナワークロードに適した OS となっていますが、ubuntu などの通常の OS と比較して性能に優劣があるのか気になります。
そこで、以下のような非常に簡単な検証を行って、負荷をかけた際の CPU 使用率などに差があるのかを比較しました。

  • nginx pod を 20 台起動する。
  • k6s で負荷をかけたときの CPU 使用率の変化を測定する。

検証用ノードは上記の k8s クラスタの worker node を使います。

  • Flatcar 3975.2.2
  • ubuntu 22.04 LTS
  • ノードの VM スペックはいずれも メモリ 8 GB, vCPU 4

各測定における CPU 使用率などは kube-state-metrics の prometheus で収集するメトリクスから計算します。
nginx pod 配置用のマニフェストは以下。始めに ubuntu ノードに配置するため nodeSelector で指定。

nginx-deploy.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 20
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
      nodeSelector:
        kubernetes.io/hostname: kata-w1

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

まず ubuntu ノードに 20 台の pod を配置してメモリ、CPU の変化を見ます。
デフォルトの状態では平均して CPU 使用率 ~ 2.3 %、メモリ使用量 ~ 1.1 GB 。
20 個の nginx pod を起動すると、CPU 使用率 ~ 3.3 %、メモリ使用量 ~ 1.2 GB に増加。

次に k6 で負荷をかけるために、単に http リクエストを送信するための js ファイルを用意。

k6.js
import http from 'k6/http';
import { sleep, check } from 'k6';

export default function () {
    const url = 'http://10.98.101.228';
    const res = http.get(url);

    sleep(0.1);
}

k6 は control plane ノードから実行し、service を経由して svc -> pod でリクエストを送信、virtual user 100 で 3 分間負荷をかけます。

k6 run k6.js --duration 3m --vus 100

実行すると、負荷をかけている間の CPU 使用率 ~ 27 %、メモリ使用量 ~ 1.25 GB となりました。
ちなみに k6 の実行結果は以下。

$ k6 run k6.js --duration 3m --vus 100

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: k6.js
        output: -

     scenarios: (100.00%) 1 scenario, 100 max VUs, 3m30s max duration (incl. graceful stop):
              * default: 100 looping VUs for 3m0s (gracefulStop: 30s)


     data_received..................: 149 MB 830 kB/s
     data_sent......................: 14 MB  78 kB/s
     http_req_blocked...............: avg=19.73µs  min=1.72µs   med=5.17µs   max=33.83ms  p(90)=8.55µs   p(95)=12.53µs
     http_req_connecting............: avg=10.64µs  min=0s       med=0s       max=33.75ms  p(90)=0s       p(95)=0s
     http_req_duration..............: avg=1.81ms   min=344.97µs med=1.32ms   max=34.42ms  p(90)=3.34ms   p(95)=4.58ms
       { expected_response:true }...: avg=1.81ms   min=344.97µs med=1.32ms   max=34.42ms  p(90)=3.34ms   p(95)=4.58ms
     http_req_failed................: 0.00%  0 out of 175183
     http_req_receiving.............: avg=141.34µs min=13.24µs  med=49.01µs  max=33.16ms  p(90)=281.5µs  p(95)=575.52µs
     http_req_sending...............: avg=163.04µs min=5.51µs   med=17.21µs  max=22.65ms  p(90)=401.95µs p(95)=913.55µs
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=1.51ms   min=266.99µs med=1.1ms    max=33.88ms  p(90)=2.74ms   p(95)=3.85ms
     http_reqs......................: 175183 972.691039/s
     iteration_duration.............: avg=102.73ms min=100.43ms med=102.14ms max=149.73ms p(90)=104.87ms p(95)=106.51ms
     iterations.....................: 175183 972.691039/s
     vus............................: 100    min=100         max=100
     vus_max........................: 100    min=100         max=100


running (3m00.1s), 000/100 VUs, 175183 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs  3m0s

次に flatcar node に対しても同様の検証を行うため、deployment の node selector を変更。

nginx-deploy.yml
      nodeSelector:
-        kubernetes.io/hostname: kata-w1
+        kubernetes.io/hostname: flatcar-test.novalocal

デフォルトの状態では平均して CPU 使用率 ~ 2.9 %、メモリ使用量 ~ 1.7 GB でした。
20 個の nginx pod を起動すると、CPU 使用率 ~ 3.0 %、メモリ使用量 ~ 2.0 GB に増加。
負荷をかけた際は、CPU 使用率 ~ 20.7 % まで増加。
k6 の実行結果は以下。

         /\      Grafana   /‾‾/
    /\  /  \     |\  __   /  /
   /  \/    \    | |/ /  /   ‾‾\
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/

     execution: local
        script: k6.js
        output: -

     scenarios: (100.00%) 1 scenario, 100 max VUs, 3m30s max duration (incl. graceful stop):
              * default: 100 looping VUs for 3m0s (gracefulStop: 30s)


     data_received..................: 150 MB 830 kB/s
     data_sent......................: 14 MB  78 kB/s
     http_req_blocked...............: avg=23.08µs  min=1.78µs   med=5.15µs   max=46.54ms  p(90)=8.47µs   p(95)=12.21µs
     http_req_connecting............: avg=13.94µs  min=0s       med=0s       max=44.41ms  p(90)=0s       p(95)=0s
     http_req_duration..............: avg=1.72ms   min=293.83µs med=1.27ms   max=53.96ms  p(90)=3.05ms   p(95)=4.16ms
       { expected_response:true }...: avg=1.72ms   min=293.83µs med=1.27ms   max=53.96ms  p(90)=3.05ms   p(95)=4.16ms
     http_req_failed................: 0.00%  0 out of 175297
     http_req_receiving.............: avg=125.19µs min=13.39µs  med=44.75µs  max=38.35ms  p(90)=222.53µs p(95)=465.59µs
     http_req_sending...............: avg=159.96µs min=5.4µs    med=16.85µs  max=25.32ms  p(90)=377.13µs p(95)=856.17µs
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
     http_req_waiting...............: avg=1.44ms   min=260.46µs med=1.07ms   max=39.03ms  p(90)=2.52ms   p(95)=3.47ms
     http_reqs......................: 175297 973.335568/s
     iteration_duration.............: avg=102.66ms min=100.44ms med=102.11ms max=164.93ms p(90)=104.59ms p(95)=106.06ms
     iterations.....................: 175297 973.335568/s
     vus............................: 100    min=100         max=100
     vus_max........................: 100    min=100         max=100


running (3m00.1s), 000/100 VUs, 175297 complete and 0 interrupted iterations
default ✓ [======================================] 100 VUs  3m0s

k6 の実行結果を比較すると、いずれも時間あたりのリクエストは ~ 970 req/sec で失敗したリクエストは 0 となっています。特にレスポンスが返ってくるまでの時間は以下のようになりました。
単位が μs なので正直そこまで差はないのですが flatcar のほうが若干早くなっています。

項目 ubuntu 22.04 flatcar
http_req_receiving avg (μs) 141.34 125.19
http_req_receiving p95 (μs) 575.52 465.59

POD 起動時、負荷時のメモリ、 CPU 使用率の増加量は以下のようになりました。

項目 ubuntu 22.04 flatcar
pod 起動時のメモリ増加 (GB) 0.1 0.3
pod 起動時の CPU 使用率の増加量 (%) 1 0.1
負荷時の CPU 使用率の増加量 (%) 25 17

今回の検証の範囲では Flatcar の方が負荷時の CPU 使用率の上昇が低く、リクエストの処理も若干早いという結果になりました。非常に単純な検証なのでこれだけで Flactar のほうが優れているとは言えませんが、少なくともパフォーマンスが顕著に劣っているわけではないことが確認できました。

Cluster API で flatcar クラスタを作成する

Ignition で k8s クラスタにノードを参加させる方法では事前にクラスタ上で登録用の token を発行して構成ファイルに記載しなければならないので、クラスタ作成や worker node のスケールアップを自動化するのは一工夫必要になっています。
そのため、Cluster API を利用して k8s クラスタを構成する方法にもサポートしています。こちらの方法では Cluster API を利用してクラスタ作成や node の追加を行うことができるので、一度セットアップを行えば比較的容易に Flatcar クラスタを管理できるようになっています。

Cluster API とは

Cluster API は k8s クラスタを各クラウド上に容易にプロビジョニング、管理することを目的とした kubernetes のサブプロジェクトです。

https://cluster-api.sigs.k8s.io/introduction

各クラウド は provider と呼ばれる形式で提供され、aws, azure, openstack, vcluster など様々なクラウドがサポートされています。cluster API でクラスタを作成すると provider が各クラウド上の操作を実行し、VM やネットワークなどクラスタの作成・運用に必要なリソースをプロビジョニングする動作となっています。例えば AWS の provider は以下で管理されています。
https://github.com/kubernetes-sigs/cluster-api-provider-aws

似たようなツールでは、AWS の EKS 上に CLI からクラスタを作成できる eksctl が有名ですが、 provider によって様々なクラウド上に展開できるように拡張したようなものをイメージすればわかりやすいかと思います。

使ってみる

Cluster API は openstack 上へのクラスタ作成もサポートしているので、実際に Cluster API を使って Flatcar クラスタを作成してみます。

image の作成

事前準備として、Cluster API で Flatcar を使用するには Cluster API 用にカスタマイズした Flatcar のマシンイメージを作成する必要があります。

https://cluster-api-openstack.sigs.k8s.io/clusteropenstack/configuration#ignition-based-images

イメージは Image-builder で作成できるようになっているので、こちらを元にビルドします。
手順は https://image-builder.sigs.k8s.io/capi/providers/openstack を参照。ビルド用に適当な ubuntu VM を用意して以下を実行していきます。

必要なパッケージのインストール

sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients virtinst cpu-checker libguestfs-tools libosinfo-bin

sudo usermod -a -G kvm ubuntu
sudo chown root:kvm /dev/kvm
sudo reboot now

image-builder リポジトリを clone して依存ツールをインストール

git clone --depth 1 https://github.com/kubernetes-sigs/image-builder.git
cd image-builder/images/capi
make deps-qemu

flatcar イメージのビルド

make OEM_ID=openstack build-qemu-flatcar

ビルドはだいぶ時間がかかりますが、完了すると output 以下に qcow2 形式のイメージが作成されます。

file output/flatcar-stable-3975.2.2-kube-v1.30.5/flatcar-stable-3975.2.2-kube-v1.30.5
output/flatcar-stable-3975.2.2-kube-v1.30.5/flatcar-stable-3975.2.2-kube-v1.30.5: QEMU QCOW2 Image (v3), 21474836480 bytes

作成したイメージを openstack に登録。

openstack image create \
  --container-format bare \
  --disk-format qcow2 \
  --public \
  --property hw_qemu_guest_agent=yes \
  --file ./flatcar-stable-3975.2.2-kube-v1.30.5 \
  flatcar-3975.2.2-k8s-1.30.5-amd64\n

これでイメージ側の準備は完了です。

Cluster API のセットアップ

Cluster API で openstack 上にクラスタを作成するには以下の手順に従います。

https://cluster-api-openstack.sigs.k8s.io/getting-started

まずはじめに clusterctl CLI をインストール

curl -L https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.4/clusterctl-linux-amd64 -o clusterctl
sudo chmod +x clusterctl
sudo mv clusterctl /usr/local/bin

Cluster API を使うには management Cluster と呼ばれる k8s クラスタが必要になります。普通に作成した k8s クラスタに対して clusterctl init を実行することで management cluster にすることができるので、既存のクラスタ上でコマンドを実行します。
この際、--infrastructure openstack を指定することで openstack 用にセットアップされます。
また、flatcar のように userdata に Ignition 形式を使用する場合は環境変数 EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION=true を設定する必要があります。

management cluster の作成
$ export EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION=true
$ clusterctl init --infrastructure openstack
Fetching providers
Installing cert-manager version="v1.16.0"
Waiting for cert-manager to be available...
Installing provider="cluster-api" version="v1.8.4" targetNamespace="capi-system"
Installing provider="bootstrap-kubeadm" version="v1.8.4" targetNamespace="capi-kubeadm-bootstrap-system"
Installing provider="control-plane-kubeadm" version="v1.8.4" targetNamespace="capi-kubeadm-control-plane-system"
Installing provider="infrastructure-openstack" version="v0.11.0" targetNamespace="capo-system"

Your management cluster has been initialized successfully!

You can now create your first workload cluster by running the following:

  clusterctl generate cluster [name] --kubernetes-version [version] | kubectl apply -f -

clusterctl generate cluster を実行することでクラスタ作成用のマニフェストが作成されますが、openstack の場合はいくつか設定が必要になります。
まず env.rc を使って clouds.yaml から openstack 認証情報を読み取ります。

source ./env.rc clouds.yaml kolla-admin

読み取ると openstack 認証情報の環境変数が設定されます。内容は base64 encode されるので decode することで内容を確認可能。

$ env

CAPO_CLOUD=openstack
OPENSTACK_CLOUD=openstack
OPENSTACK_CLOUD_YAML_B64=...
OPENSTACK_CLOUD_PROVIDER_CONF_B64=...
OPENSTACK_CLOUD_CACERT_B64=Cg==

clusterctl generate cluster では cloud 上にクラスタを作成するためのマニフェストを templates から作成しますが、その際にプロビジョン先のネットワークやマシンイメージを環境変数かコマンド実行時の引数から読み取って入力するので事前に設定しておく必要があります。

設定項目は必須と任意のものがありますが、基本的に以下は必須項目です。各パラメータの詳細や他の項目は https://cluster-api-openstack.sigs.k8s.io/clusteropenstack/configuration を参照。

name Description
OPENSTACK_CONTROL_PLANE_MACHINE_FLAVOR control plane 用の node に使用される VM の flavor 名
OPENSTACK_NODE_MACHINE_FLAVOR worker node に使用される VM の flavor 名
OPENSTACK_DNS_NAMESERVERS VM で使用する DNS
OPENSTACK_FAILURE_DOMAIN openstack の available zone 名
OPENSTACK_EXTERNAL_NETWORK_ID openstack の外部 network の ID
OPENSTACK_IMAGE_NAME VM に使用されるマシンイメージ名
OPENSTACK_SSH_KEY_NAME 作成した VM に設定する ssh keypair
OPENSTACK_FLATCAR_IMAGE_NAME openstack 上に登録した flatcar image 名

使っている環境に合わせて設定。

export OPENSTACK_CONTROL_PLANE_MACHINE_FLAVOR=m1.medium
export OPENSTACK_NODE_MACHINE_FLAVOR=m1.medium
export OPENSTACK_DNS_NAMESERVERS=8.8.8.8
export OPENSTACK_FAILURE_DOMAIN=nova
export OPENSTACK_EXTERNAL_NETWORK_ID=bac31e0e-0804-46cf-a944-5daff4073501
export OPENSTACK_IMAGE_NAME=flatcar-3975.2.2-k8s-1.30.5-amd64
export OPENSTACK_SSH_KEY_NAME=kolla_ssh
export OPENSTACK_FLATCAR_IMAGE_NAME=flatcar-3975.2.2-k8s-1.30.5-amd64

これで準備が整ったのでマニフェストを作成。

clusterctl generate cluster test-op3 --kubernetes-version v1.30.5 --flavor flatcar > template.yml

適用

kubectl apply -f template.yml

これにより Cluster API のカスタムリソースがいろいろ作成されますが、裏では openstack 側にロードバランサーやネットワーク、 control plane 用の VM が作成されます。
kubeadmcontrolplanes リソースを見て replicas が 1 になれば ok.

$ k get kubeadmcontrolplanes.controlplane.cluster.x-k8s.io
NAME                     CLUSTER    INITIALIZED   API SERVER AVAILABLE   REPLICAS   READY   UPDATED   UNAVAILABLE   AGE    VERSION
test-op3-control-plane   test-op3   true                                 1                  1         1             108s   v1.30.5

この時点で flatcar クラスタの control plane が作成されます。 cluster get kubeconfig [cluster_name] で control plane に通信するための kubeconfig が取得できます。

clusterctl get kubeconfig test-op3 > test-op3.kubeconfig

kubeconfig を指定することで作成した control plane に対して kubectl を実行できます。

$ kubectl --kubeconfig ./test-op3.kubeconfig get node
NAME                           STATUS     ROLES           AGE   VERSION
test-op3-control-plane-kpwb5   NotReady   control-plane   93s   v1.30.5

CNI をインストールしないと NotReady のままなので calico をインストール

kubectl --kubeconfig=./test-op3.kubeconfig \
  apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.26.1/manifests/calico.yaml

インストールが完了すると status が Ready になり、flatcar ノードから構成される control plane が完成します。

NAME                           STATUS   ROLES           AGE    VERSION   INTERNAL-IP   EXTERNAL-IP    OS-IMAGE                                             KERNEL-VERSION   CONTAINER-RUNTIME
test-op3-control-plane-kpwb5   Ready    control-plane   7m7s   v1.30.5   10.6.0.102    192.168.3.58   Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)   6.6.54-flatcar   containerd://1.7.20

このクラスターで openstack 側にリソースを作成するには、openstack 用の cloud provider である openstack-cloud-controller-manager をインストールする必要があります。
上記を元に openstack 認証情報を設定した cloud.conf を作成し、作成した control plane 側に secret を作成します。

kubectl --kubeconfig=./test-op3.kubeconfig -n kube-system create secret generic cloud-config --from-file=cloud.conf

cloud provider 本体をインストール

kubectl apply --kubeconfig=./test-op3.kubeconfig -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/cloud-controller-manager-roles.yaml
kubectl apply --kubeconfig=./test-op3.kubeconfig -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/cloud-controller-manager-role-bindings.yaml
kubectl apply --kubeconfig=./test-op3.kubeconfig -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/openstack-cloud-controller-manager-ds.yaml

インストールが正常に完了すれば openstack 側とも通信できるようになり、k8s クラスタ側のリソース作成に伴い openstack 側でも対応するリソースが作成されるようになります。

上記で作成したクラスタはまだ worker node がありませんが、cluster API の ノードの Scaling を実行することでノードを追加できます。
machinedeployment の replicas を 1 に設定することで worker node を 1 つ追加してみます。

$ kubectl scale machinedeployment test-op3-md-0  --replicas=1
machinedeployment.cluster.x-k8s.io/test-op3-md-0 scaled

少し待つと、Flatcar の worker node がクラスタに追加されていることが確認できます。

NAME                           STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP    OS-IMAGE                                             KERNEL-VERSION   CONTAINER-RUNTIME
test-op3-control-plane-kpwb5   Ready    control-plane   13m   v1.30.5   10.6.0.102    192.168.3.58   Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)   6.6.54-flatcar   containerd://1.7.20
test-op3-md-0-9xskz-kjqc2      Ready    <none>          73s   v1.30.5   10.6.0.230    <none>         Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)   6.6.54-flatcar   containerd://1.7.20

openstack 側でも上記のノードに対応する VM が自動的に作成されます。

$ openstack server list
+--------------------------------------+----------------------------------------------+---------+---------------------------------------------------------------------+-----------------------------------+-----------------------+
| ID                                   | Name                                         | Status  | Networks                                                            | Image                             | Flavor                |
+--------------------------------------+----------------------------------------------+---------+---------------------------------------------------------------------+-----------------------------------+-----------------------+
| 3ca31781-2fe1-492d-ba9c-2be5ed4d1cd2 | test-op3-md-0-9xskz-kjqc2                    | ACTIVE  | k8s-clusterapi-cluster-default-test-op3=10.6.0.230                  | flatcar-3975.2.2-k8s-1.30.5-amd64 | m1.medium             |
| fb7ceac2-c1bf-4464-a4bb-66e5451ed4ea | test-op3-control-plane-kpwb5                 | ACTIVE  | k8s-clusterapi-cluster-default-test-op3=10.6.0.102, 192.168.3.58    | flatcar-3975.2.2-k8s-1.30.5-amd64 | m1.medium             |

ignition 構成で node を追加する方法と比較するとノード追加時の token 処理等が自動で行われるので、cluster API はより大規模な環境でのノード管理を容易に行えるメリットがあります。

sysext

systemd-sysext はシステム起動時に /usr 等に動的にファイルを追加する機能です。これ自体は一般的な systemd の機能として使われているようですが、flatcar でも起動時に追加のパッケージをインストールするための拡張機能として使用されます。

flatcar ではパッケージマネージャーがなく OS 起動後に構成を変更する余地がほとんどないため、以前では torcx と呼ばれる拡張機能でカスタマイズに対応してきました。sysext は torcx に代わる新しい拡張機能で、overlayfs を利用して追加のパッケージや独自パッケージを含むディレクトリをマージしてカスタマイズするようになっています。以下の blog にあるイメージ図がわかりやすいです。

https://www.flatcar.org/blog/2023/12/extending-flatcar-say-goodbye-to-torcx-and-hello-to-systemd-sysext/

ignition 構成で sysext イメージを利用するには、ダウンロードした sysext イメージ (通常は .raw の拡張子を持つ) を /opt/extensions/[package]/[image] に配置します。
k8s クラスタにノードを追加する際に使った ignition 構成では、sysext-bakery リポジトリの kubernetes sysext イメージをダウンロードして /opt/extensions/kubernetes/ に配置することで有効化しています(サービスとして起動するには systemd unit の設定も必要)。

storage:
  links:
    - target: /opt/extensions/kubernetes/kubernetes-v1.30.0-x86-64.raw
      path: /etc/extensions/kubernetes.raw
      hard: false
  files:
    - path: /etc/sysupdate.kubernetes.d/kubernetes-v1.30.conf
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-v1.30.conf
    - path: /etc/sysupdate.d/noop.conf
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/noop.conf
    - path: /opt/extensions/kubernetes/kubernetes-v1.30.0-x86-64.raw
      contents:
        source: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-v1.30.0-x86-64.raw

コミュニティによってサポートされている sysext イメージは sysext-bakery で管理されており、現時点では以下のパッケージが sysext でインストールできるようになっています。

Extension Availability
kubernetes released
docker released (includes containerd)
docker_compose released
nvidia-runtime released
wasmtime released
wasmcloud released
tailscale released
crio released
k3s released
rke2 released
keepalived build script
ollama released

sysext イメージの作り方

独自の sysext イメージを作成する方法はドキュメントには明示的に書いてなさそうですが、sysext-bakery では flatcar 管理でない wasmcloud や k3s の sysext イメージを作成するスクリプトがあるので、これらを参考にすることで他のパッケージにも応用できます。
例えば docker-compose をインストールするための sysext raw ファイルを作成する create_docker_compose_sysext.sh の中身を見ると、docker compose のリポジトリからバイナリをダウンロードして bake.sh で作成していることがわかります。

https://github.com/flatcar/sysext-bakery/blob/main/create_docker_compose_sysext.sh

実際にリポジトリを clone してスクリプトを実行すると docker-compose.raw が作成できます。

$ ./create_docker_compose_sysext.sh 2.30.1 docker-compose
Parallel mksquashfs: Using 16 processors
Creating 4.0 filesystem on docker-compose.raw, block size 131072.
[=============================================================================================================================================================================================================================/] 490/490 100%

Exportable Squashfs 4.0 filesystem, gzip compressed, data block size 131072
       compressed data, compressed metadata, compressed fragments,
       compressed xattrs, compressed ids
       duplicates are removed
Filesystem size 18498.03 Kbytes (18.06 Mbytes)
       29.58% of uncompressed filesystem size (62541.87 Kbytes)
Inode table size 1503 bytes (1.47 Kbytes)
       65.98% of uncompressed inode table size (2278 bytes)
Directory table size 150 bytes (0.15 Kbytes)
       56.39% of uncompressed directory table size (266 bytes)
Number of duplicate files found 0
Number of inodes 10
Number of files 2
Number of fragments 1
Number of symbolic links 0
Number of device nodes 0
Number of fifo nodes 0
Number of socket nodes 0
Number of directories 8
Number of hard-links 0
Number of ids (unique uids + gids) 1
Number of uids 1
       root (0)
Number of gids 1
       root (0)
Created docker-compose.raw

sysextname ディレクトリをスクリプト内でクリーンアップされないように変更すると、イメージの元となるディレクトリ構造は以下のようになっていることが確認できます。この内 /usr/lib 以下は bake.sh の実行時に自動で作成されます。

docker-compose
└── usr
    ├── lib
    │   └── extension-release.d
    │       └── extension-release.docker-compose
    └── local
        └── lib
            └── docker
                └── cli-plugins
                    └── docker-compose

sysext は overlayfs でディレクトリをマージするので、独自のパッケージを sysext イメージにしたい場合はマージ先のディレクトリ構造を作って実行ファイルを配置すれば良いことがわかります。

このやり方で独自パッケージが適用できるか確認するため、ここでは例としてモダンな top コマンドの代替品である bottom をインストールするための sysext イメージを作ってみます。bottom のバイナリを /usr/local/bin に配置したいので、実行ファイル btm を github からダウンロードして以下のディレクトリ構造に配置します。sysext-bakery リポジトリにある back.sh も同ディレクトリに配置。

$ tree
.
├── bake.sh
└── bottom
    └── usr
        └── local
            └── bin
                └── btm

bake.sh にディレクトリ名を渡して実行。

$ ./bake.sh bottom
Parallel mksquashfs: Using 16 processors
Creating 4.0 filesystem on bottom.raw, block size 131072.
[=================================================================================================================================================================================================================================|] 1/1 100%

Exportable Squashfs 4.0 filesystem, gzip compressed, data block size 131072
 compressed data, compressed metadata, compressed fragments,
 compressed xattrs, compressed ids
 duplicates are removed
Filesystem size 0.31 Kbytes (0.00 Mbytes)
 66.53% of uncompressed filesystem size (0.47 Kbytes)
Inode table size 65 bytes (0.06 Kbytes)
 40.12% of uncompressed inode table size (162 bytes)
Directory table size 81 bytes (0.08 Kbytes)
 61.83% of uncompressed directory table size (131 bytes)
Number of duplicate files found 0
Number of inodes 5
Number of files 1
Number of fragments 1
Number of symbolic links 0
Number of device nodes 0
Number of fifo nodes 0
Number of socket nodes 0
Number of directories 4
Number of hard-links 0
Number of ids (unique uids + gids) 1
Number of uids 1
 root (0)
Number of gids 1
 root (0)
Created bottom.raw

これで bottom をインストールするための sysext イメージ bottom.raw ができました。

flatcar 起動時には source で指定した URL から sysext イメージをダウンロードする動作となっているため、作成したイメージを Github Release などダウンロード可能な場所に配置する必要があります。ここでは簡単のため raw イメージのあるディレクトリで python http server を使って対応します。

python -m http.server 9999

curl 等で上記サーバーの ip にアクセスしてファイルがダウンロードできれば ok.
ignition 構成ファイルでは raw イメージを取得して /opt 以下に配置する処理と /etc 以下にシンボリックリンクを作成する処理を記述します。

custom-sysext.yml
# butane < config.yaml > config.json
# ./flatcar_production_qemu.sh -i ./config.json
variant: flatcar
version: 1.0.0
storage:
  files:
    - path: /opt/extensions/bottom/bottom.raw
      contents:
        source: http://192.168.3.30:9999/bottom.raw # 上記 python server が起動している ip, port と botom.raw のパスを指定
  links:
    - target: /opt/extensions/bottom/bottom.raw
      path: /etc/extensions/bottom.raw
      hard: false

変換して VM を作成。

cat custom-sysext.yml | docker run --rm -i quay.io/coreos/butane:release > ignition.json
openstack server create \
  --user-data ./ignition.json \
  --image flatcar-3975.2.2-amd64 \
  --key-name kolla_ssh \
  --flavor m1.small \
  --network demo-net \
  flatcar-test-custom

SSH でログインすると実際に /usr/local/bin に btm のバイナリが配置されて実行できるようになっています。

$ ssh -i ./kolla_ssh core@10.0.0.71
Last login: Sat Nov  2 18:35:31 UTC 2024 from 10.0.0.30 on pts/0
Flatcar Container Linux by Kinvolk stable 3975.2.2 for Openstack

core@flatcar-test-custom ~ $ which btm
/usr/local/bin/btm

core@flatcar-test-custom ~ $ btm -h
bottom 0.10.2
Clement Tsang <cjhtsang@uwaterloo.ca>

A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.

Usage: btm [OPTIONS]
...


btm を実行している様子

やや手間はかかりますが、独自のパッケージやコマンドを flatcar にインストールしたい場合は上記のように独自のカスタム sysext イメージを作成することで対応できます。

Nebraska で OS 自動アップデートを管理する

Flatcar の特徴の 1 つ Automated Update では、常に安定したセキュアなアップデート方法で最新のバージョンを適用できるとあります。パッケージマネージャがないのにどのようにアップデートを適用するかというと、1 つは Flatcar のバージョン毎にマシンイメージを作成し、新しいバージョンがリリースされたら都度 VM を作成し直すという方法があります。これは原始的な方法ではありますが、マシンイメージ側と実際の OS 側で差分が発生しないので terraform のような IaC と相性の良いアプローチとなっています。
もう一つは flatcar 内にインストール済みのコマンドを利用して Flactar 側のリリース済みバージョンを取得し、新しいバージョンが利用可能になったら更新を適用する方法です。こちらは nebraska と組み合わせることで更新作業を自動化・管理できるのでより大規模な環境に適しています。

nebraska は Flatcar のバージョン更新やロールアウトを管理する update manager となっています。nebraska は公式の Flactar からリリースチャンネル毎の最新バージョンなどを自動で取得できます。一方で各 Flatcar VMは nebraska に接続し、新しいバージョンが利用可能になった際に自動で更新を適用したり、グループ毎に一括で更新を適用するといったことができます。


イメージ図

https://www.flatcar.org/docs/latest/nebraska/authorization/

nebraska は helm でインストールできるので既存の k8s クラスタに入れてみます。

helm repo add nebraska https://flatcar.github.io/nebraska
helm show values nebraska/nebraska > values.yml
helm install -n nebraska nebraska nebraska/nebraska --values values.yml  --create-namespace

これにより nebraska 本体とデータ管理用の postgresql が起動します。

NAME                            READY   STATUS    RESTARTS      AGE
pod/nebraska-776b7654bf-l74cr   1/1     Running   3 (18h ago)   18h
pod/nebraska-postgresql-0       1/1     Running   0             18h

NAME                             TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
service/nebraska                 ClusterIP   10.96.25.36   <none>        80/TCP     18h
service/nebraska-postgresql      ClusterIP   10.101.4.13   <none>        5432/TCP   18h
service/nebraska-postgresql-hl   ClusterIP   None          <none>        5432/TCP   18h

nebraska の svc にブラウザでアクセスすると、リリースチャンネル毎のバージョン情報や nebraska がバージョンを管理している Flactar インスタンスの個数、詳細などが確認できます。


nebraska の webUI

実際に nebraska を使って flatcar のバージョンを上げてみます。
例えば今まで使ってきた stable channel の flatcar ではバージョン 3975.2.2 なので os-release の中身が以下のようになっています。

core@flatcar-test ~ $ cat /etc/os-release
NAME="Flatcar Container Linux by Kinvolk"
ID=flatcar
ID_LIKE=coreos
VERSION=3975.2.2
VERSION_ID=3975.2.2
BUILD_ID=2024-10-08-1845
SYSEXT_LEVEL=1.0
PRETTY_NAME="Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)"
ANSI_COLOR="38;5;75"
HOME_URL="https://flatcar.org/"
BUG_REPORT_URL="https://issues.flatcar.org"
FLATCAR_BOARD="amd64-usr"
CPE_NAME="cpe:2.3:o:flatcar-linux:flatcar_linux:3975.2.2:*:*:*:*:*:*:*"

nebraska ではリリースチャンネルを切り替えることも可能なので、alpha チャンネルの最新バージョンを適用してみます。flactar VM を nebraska に接続するには、ignition 構成ファイルで /etc/flatcar/update.conf に接続先 nebraska の情報を記載します。

  • SERVER: [nebraska の ip アドレス]:[port]/v1/update を指定
  • GROUP: この Flatcar VM が所属するグループを指定。4 つのリリースチャンネルの他、独自のグループを指定することも可能。
nebraska.yml
variant: flatcar
version: 1.0.0
storage:
  files:
    - path: /etc/flatcar/update.conf
      overwrite: true
      mode: 0644
      contents:
        inline: |
          GROUP=alpha
          SERVER=http://nebraska.ops.com:32323/v1/update/
    - path: /etc/hosts
      overwrite: false
      mode: 0644
      append:
        - inline: |
            10.0.0.31  nebraska.ops.com

上記の ignition ファイルを使って Flactar VM を作成。起動直後ではまだ更新は実行されず 3975.2.2 のままです。

$ ssh -i ./kolla_ssh core@10.0.0.92

Last login: Sat Nov  2 14:18:58 UTC 2024 on tty1
Flatcar Container Linux by Kinvolk alpha 3975.2.2 for Openstack

core@flatcar-test-alpha ~ $ cat /etc/os-release
NAME="Flatcar Container Linux by Kinvolk"
ID=flatcar
ID_LIKE=coreos
VERSION=3975.2.2
VERSION_ID=3975.2.2
BUILD_ID=2024-10-08-1845
SYSEXT_LEVEL=1.0
PRETTY_NAME="Flatcar Container Linux by Kinvolk 3975.2.2 (Oklo)"
ANSI_COLOR="38;5;75"
HOME_URL="https://flatcar.org/"
BUG_REPORT_URL="https://issues.flatcar.org"
FLATCAR_BOARD="amd64-usr"
CPE_NAME="cpe:2.3:o:flatcar-linux:flatcar_linux:3975.2.2:*:*:*:*:*:*:*"

nebraska からのバージョン取得は定期的に実行されるようですが、update_engine_client -update を実行することで任意のタイミングでサーバーと接続できます。
以下では alpha チャンネルの最新バージョン 4116.0.0 が取得されていることが確認できます。
取得が完了したタイミングで 5 分後に reboot がスケジュールされ、reboot 後に取得したバージョンに更新されます。

core@flatcar-test-alpha ~ $ update_engine_client -update
I1102 14:22:11.959977  1551 update_engine_client.cc:251] Initiating update check and install.
I1102 14:22:12.016368  1551 update_engine_client.cc:256] Waiting for update to complete.
LAST_CHECKED_TIME=1730557285
PROGRESS=0.270325
CURRENT_OP=UPDATE_STATUS_DOWNLOADING
NEW_VERSION=4116.0.0
NEW_SIZE=505686594
LAST_CHECKED_TIME=1730557285
PROGRESS=0.320383

...

CURRENT_OP=UPDATE_STATUS_DOWNLOADING
NEW_VERSION=4116.0.0
NEW_SIZE=505686594
LAST_CHECKED_TIME=1730557285
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_FINALIZING
NEW_VERSION=4116.0.0
NEW_SIZE=505686594

Broadcast message from locksmithd at 2024-11-02 14:23:39.762143696 +0000 UTC m=
System reboot in 5 minutes!

LAST_CHECKED_TIME=1730557285
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT
NEW_VERSION=4116.0.0
NEW_SIZE=505686594
I1102 14:23:42.049386  1551 update_engine_client.cc:198] Update succeeded -- reboot needed.

再起動後に確認するとバージョンが 4116.0.0 に更新されています。

core@flatcar-test-alpha ~ $ cat /etc/os-release
NAME="Flatcar Container Linux by Kinvolk"
ID=flatcar
ID_LIKE=coreos
VERSION=4116.0.0
VERSION_ID=4116.0.0
BUILD_ID=2024-10-09-0001
SYSEXT_LEVEL=1.0
PRETTY_NAME="Flatcar Container Linux by Kinvolk 4116.0.0 (Oklo)"
ANSI_COLOR="38;5;75"
HOME_URL="https://flatcar.org/"
BUG_REPORT_URL="https://issues.flatcar.org"
FLATCAR_BOARD="amd64-usr"
CPE_NAME="cpe:2.3:o:flatcar-linux:flatcar_linux:4116.0.0:*:*:*:*:*:*:*"

nebraska 側でもバージョンアップデートを実行した Flatcar の情報が確認できます。


Instances 1 となっているので上記の Flatcar VM が更新された

このように nebraska では管理下の Flatcar VM のバージョン更新作業を自動で行うことができます。Flatcar では Automated Updates として常に最新のセキュリティパッチが適用されたバージョンの使用を推奨しているため、nebraska を使うことでこの作業を自動で管理できます。
一方でマシンイメージの情報等も合わせて更新されるわけではないので、例えばイメージ名や terraform 等の構成側で宣言的に flatcar のバージョン等を管理している場合、そのバージョンと OS 内の実際のバージョンが異なるドリフトが発生します。その場合は宣言的に記述する際はバージョン情報を含まないようにする、nebraska ではなく terraform 等を使って常にバージョン更新を行うなどの工夫が必要になります。

その他

Debug

Flatcar 内で何らかの追加コマンドをインストールしてデバッグを行いたい場合にはデフォルトでインストール済みの toolbox が利用できます。

https://www.flatcar.org/docs/latest/setup/debug/install-debugging-tools/

toolbox を実行すると Fedora の docker コンテナが起動します。このコンテナ内では dnf が使えるので dnf でダウンロード可能なパッケージを追加でインストールできます。toolbox 内の環境は /usr read-only の制限を受けないのでデバッグ作業に使えます。

おわりに

最近 CNCF incubating project に採択された Flatcar Container Linux を試しました。コンテナワークロード向けに最適化された OS だけあって必要最小限のパッケージのみが含まれる、ファイルシステムが readonly になっているなど、よりセキュアな環境でコンテナを実行できます。便利な CLI やユーティリティを追加するのが結構大変なので検証しながら使うのは不向きですが、一方で ubuntu 等の汎用的な OS と比較すると以下のようなメリットがあります。

  • インストール済みパッケージが少ないため軽量かつ依存パッケージの脆弱性対応が最小限で済む。
  • 基本的に OS 内で構成の変更ができないので、宣言的に記述した内容との差分(ドリフト)が発生しにくい。IaC のコンセプトに従う。
  • 軽量なので OS アップグレードも容易。nebraska など組み合わせると自動アップデートも可能。

上記の特徴から、本番環境でのコンテナワークロード運用に適した OS という立ち位置になっています。

Discussion