Featured image of post Quadletイン・アクション

Quadletイン・アクション

Table of contents

Header Photo by Amy Asher on Unsplash

🦭 はじめに

Red Hat Advent Calendar 2023 17日目の記事🎅です。 ちなみにすべて 個人の見解 です。

🦭 podman generate systemd の非推奨化と Quadlet

エッジ等のユースケースで、systemdのサービスユニットとしてPodmanコンテナやPodを制御したい場合、従来は podman generate systemd というコマンドを使って、既存のコンテナやPodからユニットファイルを生成していました。2023年9月に発売された書籍Podmanイン・アクション(Podman in ActionをFedora 38/Podman v4.5.1を前提に日本語訳したもの) の「7章 systemdとの統合」でも、この方法を紹介しています。

Podman v4.7.0 で、この `podman generate systemd はdeprecated(非推奨)となりました。systemdのもとでPodmanコンテナやPodを実行したい場合は、Podman v4.7.0 のリリースノートに記載がある通り、Quadlet を使うことが推奨されています。以下の記載があります。

The podman generate systemd command is deprecated. Use Quadlet for running containers and pods under systemd.

🦭 Quadletを完全に理解する

QuadletとPodman

Quadletは、PodmanコンテナやPodをsystemdのもとでいい感じに実行できるようにするためのツールであり、もともとPodmanとは別のリポジトリ containers/quadlet で開発されていました。その後Podman v4.4で containers/podman 内の pkg/systemd の下にマージされています。Podman v4.4.0 のリリースノートには以下の記載があります。

Introduce Quadlet, a new systemd-generator that easily writes and maintains systemd services using Podman.

Quadletはsystemdのgenerator

そもそもQuadletとは何なんでしょうか。Quadletの作者であるAlexander LarssonさんのブログQuadlet, an easier way to run system containersには、以下のように書かれています。また、podman-systemd.unit のドキュメントもあわせて参照してださい。

Quadlet is a systemd generator that takes a container description and automatically generates a systemd service file from it.

つまるところ、Quadletはsystemdのgeneratorの1つです。generatorについては、systemd.generator(7)のman pageに詳しく説明されています。ChatGPTくんに頼んで要約してもらいましょう。

(ChatGPTくん🤖): ジェネレータは、systemdの一部で、システムの起動時や設定再読み込み時に実行され、ユニットファイルがロードされる前に動的にユニットファイルやシンボリックリンクを生成する役割を持つ。ジェネレータは、特定のディレクトリに配置され、システム設定を拡張・上書きし、ユニットファイルの階層を拡張する。優先度の違うディレクトリに出力し、systemctl daemon-reloadで前の設定を消去し、ジェネレータを再実行してsystemdがユニットを再読み込む。

わかった気になれたところで、man pageのExamplesを見てみましょう。ここでは最も馴染み深いであろう Example 1. systemd-fstab-generator を取り上げます。Quadletと同じくgeneratorの1つである systemd-fstab-generator が、/etc/fstab の内容をもとにユニットファイルを生成してくれています。

Example 1. systemd-fstab-generator

systemd-fstab-generator(8) converts /etc/fstab into native mount units. It uses argv[1] as location to place the generated unit files in order to allow the user to override /etc/fstab with their own native unit files, but also to ensure that /etc/fstab overrides any vendor default from /usr/.

After editing /etc/fstab, the user should invoke systemctl daemon-reload. This will re-run all generators and cause systemd to reload units from disk. To actually mount new directories added to fstab, systemctl start /path/to/mountpoint or systemctl start local-fs.target may be used.

Quadletに話を戻しましょう。手元のFedora CoreOS 39な環境で、ユーザー用のgeneratorが置かれるディレクトリ /usr/lib/systemd/user-generators/ を覗いてみると、/usr/lib/systemd/user-generators/podman-user-generator というgeneratorがあります。これは /usr/libexec/podman/quadlet へのシンボリックリンクになっています。

core@fedora-39:~$ cat /etc/redhat-release 
Fedora release 39 (Thirty Nine)
core@fedora-39:~$ podman version
Client:       Podman Engine
Version:      4.7.2
API Version:  4.7.2
Go Version:   go1.21.1
Built:        Tue Oct 31 23:30:33 2023
OS/Arch:      linux/arm64
ore@fedora-39:~$ ls -l /usr/lib/systemd/user-generators/podman-user-generator 
lrwxrwxrwx. 5 root root 31 Nov 21 04:19 /usr/lib/systemd/user-generators/podman-user-generator -> ../../../libexec/podman/quadlet

/usr/libexec/podman/quadlet は実行可能なバイナリで、podman パッケージに含まれています。

core@fedora-39:~$ file /usr/libexec/podman/quadlet 
/usr/libexec/podman/quadlet: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=59f30e7e5606e8c538765862854095113e30a9bb, for GNU/Linux 3.7.0, stripped
core@fedora-39:~$ /usr/libexec/podman/quadlet -h
Usage of /usr/libexec/podman/quadlet:
  -dryrun
        Run in dryrun mode printing debug information
  -no-kmsg-log
        Don't log to kmsg
  -user
        Run as systemd user
  -v    Print debug information
  -version
        Print version information and exit
core@fedora-39:~$ rpm -qf /usr/libexec/podman/quadlet
podman-4.7.2-1.fc39.aarch64

Quadlet ファイルの形式

Quadletがユニットファイルを生成するための元ファイル(以下、Quadletファイルと呼んでみます)の形式については、podman-systemd.unit(5)に書いてあります。

Quadletファイルの置き場所は、rootユーザーとrootlessユーザーで異なります。以下のディレクトリのいずれかにおけばいいようです。

  • rootユーザーの場合:
    • /usr/share/containers/systemd/
    • /etc/containers/systemd/
  • rootlessユーザーの場合:
    • $HOME/.config/containers/systemd/
    • $XDG_CONFIG_HOME/containers/systemd/
    • /etc/containers/systemd/users/$(UID)
    • /etc/containers/systemd/users/

例えば、Podmanコンテナを定義するためには、以下のような内容で.containerという拡張子のファイルを作成すればいいようです。systemdのユニットファイルに似た形式ですね。実際、[Service][Install] という部分はユニットファイルと同じように書けるようです。一方、[Container] はQuadlet特有ですね。他の例はpodman-systemd.unit(5)のExamplesを参照ください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Unit]
Description=A minimal container

[Container]
# Use the centos image
Image=quay.io/centos/centos:latest

# Use volume and network defined below
Volume=test.volume:/data
Network=test.network

# In the container we just run sleep
Exec=sleep 60

[Service]
# Restart service when sleep finishes
Restart=always
# Extend Timeout to allow time to pull the image
TimeoutStartSec=900
# ExecStartPre flag and other systemd commands can go here, see systemd.unit(5) man page.
ExecStartPre=/usr/share/mincontainer/setup.sh

[Install]
# Start by default on boot
WantedBy=multi-user.target default.target

Quadletでは [Container] 以外にも、[Kube][Volume][Network][Image] というセクションが定義できるようです。特にKubernetesのYAMLファイルを指定できる [Kube] は便利そうです。podman kube play のおかげですね。ちなみに、KubernetesとPodmanの連携についても、Podmanイン・アクションの「8章 Kubernetesとの連携」で紹介しています(宣伝)。

🦭 Quadlet を使ってみる

この章で使うQuadletファイルおよびKubernetes YAMLファイルは、このブログのGitHubリポジトリに置いています。

.containerなQuadletファイルの使用

Podmanコンテナを実行するために、以下のQuadletファイル podman-hello.container を作成しました。oneshotpodman_hello_world を実行するだけです。rootlessユーザーを使うので、$HOME/.config/containers/systemd/ に置いています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=Hello World from Podman

[Container]
Image=quay.io/podman/hello:latest
Exec=/usr/local/bin/podman_hello_world

[Service]
Restart=no
Type=oneshot
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target default.target

このQuadletファイルからどのようなユニットファイルが生成されるかは、quadlet -dryrun -user で確認できます。

core@fedora-39:~$ /usr/libexec/podman/quadlet -dryrun -user
quadlet-generator[21202]: Loading source unit file /var/home/core/.config/containers/systemd/podman-hello.container
---podman-hello.service---
[Unit]
Description=Hello World from Podman
SourcePath=/var/home/core/.config/containers/systemd/podman-hello.container
RequiresMountsFor=%t/containers

[X-Container]
Image=quay.io/podman/hello:latest
Exec=/usr/local/bin/podman_hello_world

[Service]
Restart=no
Type=oneshot
TimeoutStartSec=300
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -f -i --cidfile=%t/%N.cid
ExecStopPost=-/usr/bin/podman rm -f -i --cidfile=%t/%N.cid
Delegate=yes
SyslogIdentifier=%N
ExecStart=/usr/bin/podman run --name=systemd-%N --cidfile=%t/%N.cid --replace --rm --cgroups=split quay.io/podman/hello:latest /usr/local/bin/podman_hello_world

[Install]
WantedBy=multi-user.target default.target

.container なQuadletファイルからユニットファイルを生成するロジックはこの辺に実装されています。とても長いですが、Quadletファイルの [Container] の下に書かれたパラメータをひたすら変換して、ExecStart=/usr/bin/podman run の引数に追加していってるのがわかるかと思います。

さて、Quadletファイルを書いた後 systemctl --user daemon-reload すると、podman-hello.service というユニットファイルを生成されます。内容は先程 quadlet -dryrun -user で確認したものと同じです。

core@fedora-39:~$ systemctl --user daemon-reload
core@fedora-39:~$ file /run/user/$UID/systemd/generator/podman-hello.service
/run/user/501/systemd/generator/podman-hello.service: ASCII text
core@fedora-39:~$ file /run/user/$UID/systemd/generator/*/podman-hello.service
/run/user/501/systemd/generator/default.target.wants/podman-hello.service:    symbolic link to ../podman-hello.service
/run/user/501/systemd/generator/multi-user.target.wants/podman-hello.service: symbolic link to ../podman-hello.service

このsystemdサービスを起動すると、🦭が挨拶だけしてすぐ帰っていくはずです。

core@fedora-39:~$ systemctl --user start podman-hello.service
core@fedora-39:~$ journalctl --user -u podman-hello.service
Dec 11 19:06:34 fedora-39 podman[22326]: 2023-12-11 19:06:34.164871911 +0900 JST m=+0.116574772 container start 3c1197296f89b6f70afb20a6ac01fa53f41f7bca1875578bffb965749984ffac (image=quay.io/podman/hello:latest, name=syst>
Dec 11 19:06:34 fedora-39 podman[22326]: 2023-12-11 19:06:34.165512076 +0900 JST m=+0.117214937 container attach 3c1197296f89b6f70afb20a6ac01fa53f41f7bca1875578bffb965749984ffac (image=quay.io/podman/hello:latest, name=sys>
Dec 11 19:06:34 fedora-39 podman-hello[22326]: !... Hello Podman World ...!
Dec 11 19:06:34 fedora-39 podman-hello[22326]:          .--"--.
Dec 11 19:06:34 fedora-39 podman-hello[22326]:        / -     - \
Dec 11 19:06:34 fedora-39 podman-hello[22326]:       / (O)   (O) \
Dec 11 19:06:34 fedora-39 podman-hello[22326]:    ~~~| -=(,Y,)=- |
Dec 11 19:06:34 fedora-39 podman-hello[22326]:     .---. /`  \   |~~
Dec 11 19:06:34 fedora-39 podman-hello[22326]:  ~/  o  o \~~~~.----. ~~
Dec 11 19:06:34 fedora-39 podman-hello[22326]:   | =(X)= |~  / (O (O) \
Dec 11 19:06:34 fedora-39 podman-hello[22326]:    ~~~~~~~  ~| =(Y_)=-  |
Dec 11 19:06:34 fedora-39 podman-hello[22326]:   ~~~~    ~~~|   U      |~~
Dec 11 19:06:34 fedora-39 podman-hello[22326]: Project:   https://github.com/containers/podman
Dec 11 19:06:34 fedora-39 podman-hello[22326]: Website:   https://podman.io
Dec 11 19:06:34 fedora-39 podman-hello[22326]: Documents: https://docs.podman.io
Dec 11 19:06:34 fedora-39 podman-hello[22326]: Twitter:   @Podman_io
Dec 11 19:06:34 fedora-39 podman[22326]: 2023-12-11 19:06:34.1658957 +0900 JST m=+0.117598561 container died 3c1197296f89b6f70afb20a6ac01fa53f41f7bca1875578bffb965749984ffac (image=quay.io/podman/hello:latest, name=systemd>
Dec 11 19:06:34 fedora-39 podman[22399]: 2023-12-11 19:06:34.23270892 +0900 JST m=+0.060999942 container remove 3c1197296f89b6f70afb20a6ac01fa53f41f7bca1875578bffb965749984ffac (image=quay.io/podman/hello:latest, name=syst>
Dec 11 19:06:34 fedora-39 systemd[985]: Finished podman-hello.service - Hello World from Podman.

.kubeなQuadletファイルの使用

他のQuadletファイルも試してみましょう。.kube という拡張子のQuadletファイルに [Kube] セクションを設定すると、podman kube play を実行するユニットファイルが生成されます。

podman kube play では、KubernetesなYAMLファイルからPodmanコンテナやPod、Volumeなどを作成することができます。執筆時点で最新のPodman v4.8.0では、以下のKubernetesリソースがサポートされています。

  • Pod
  • Deployment
  • PersistentVolumeClaim
  • ConfigMap
  • Secret
  • DaemonSet

ということで、まずはKubernetes YAMLの準備です。今回は PodConfigMap を使いたいので、Kubernetesのドキュメント Configure a Pod to Use a ConfigMap に載っている例を拝借します。

ConfigMapのマニフェストはこれで、

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
  name: special-config
  namespace: default
data:
  special.how: very
  special.type: charm

Podはこれです。very charmなPodができそうな予感。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
  - name: test-container
    image: registry.k8s.io/busybox
    command: [ "/bin/sh", "-c", "env" ]
    envFrom:
    - configMapRef:
        name: special-config
    restartPolicy: Never

準備ができたところで、.kubeなQuadletファイルを書きましょう。[Kube]のところは、↑のYAMLのパスをそのまま指定すればいいだけでとても楽です。ConfigMapが複数ある場合には、ConfigMap=の行を必要な分だけ足していけばいいらしいです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Unit]
Description=A Service of very charm Pod

[Kube]
Yaml=/tmp/quadlet-in-action/pod-configmap-envFrom.yaml
ConfigMap=/tmp/quadlet-in-action/special-config.yaml

[Service]
Restart=no
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target default.target

こんなので動くのか不安なので quadlet -dryrun -user してみると、ちゃんとユニットファイル変換されました。.container のときと違って、ExecStart には podman kube play が指定されています。

core@fedora-39:~$ /usr/libexec/podman/quadlet -dryrun -user ~/.config/containers/systemd/special-pod.kube
...snip...
quadlet-generator[42819]: Loading source unit file /var/home/core/.config/containers/systemd/special-pod.kube
...snip...
---special-pod.service---
[Unit]
Description=A Service of very charm Pod
SourcePath=/var/home/core/.config/containers/systemd/special-pod.kube
RequiresMountsFor=%t/containers

[X-Kube]
Yaml=/tmp/quadlet-in-action/pod-configmap-envFrom.yaml
ConfigMap=/tmp/quadlet-in-action/special-config.yaml

[Service]
Restart=no
Type=notify
TimeoutStartSec=300
KillMode=mixed
Environment=PODMAN_SYSTEMD_UNIT=%n
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/usr/bin/podman kube play --replace --service-container=true --configmap /tmp/quadlet-in-action/special-config.yaml /tmp/quadlet-in-action/pod-configmap-envFrom.yaml
ExecStopPost=/usr/bin/podman kube down /tmp/quadlet-in-action/pod-configmap-envFrom.yaml

[Install]
WantedBy=multi-user.target default.target

忘れずに systemctl --user daemon-reload した後、special-pod.service を起動してみましょう。very charmなコンテナがsystemdサービスとして実行されたことがわかります。

core@fedora-39:~$ systemctl --user daemon-reload
core@fedora-39:~$ systemctl --user start special-pod.service
core@fedora-39:~$ journalctl --user -u special-pod.service --grep "special\."
Dec 12 01:59:06 fedora-39 dapi-test-pod-test-container[327907]: special.type=charm
Dec 12 01:59:06 fedora-39 dapi-test-pod-test-container[327907]: special.how=very

🦭 Quadlet は podman-generate-systemd の代替になるのか?

個人的な意見として、podman-generate-systemd に慣れ親しんだユーザーは、Quadlet が代替と言われるとモヤると思います。というのも、podman-generate-systemd では既存のコンテナやPodからユニットファイルを生成することができました。一方、Quadlet ファイルは自分で書く必要があり、podman-generate-systemd のように既存のコンテナやPodからQuadlet ファイルを自動生成する機能はありません。

この辺りは、containers/podman の Discussion#20218の通りコミュニティでも議論されています。How to replace the "podman generate systemd" command since its deprecated という質問に対して、PodmanのMaintainerが投稿した以下のコメントがベストアンサーに選ばれています。

I do not want to add that functionality to Podman as the ultimately goal is to move away from the workflow of generating systemd units based on existing containers. With Quadlet we aim at a Compose and K8s-like declarative workflow. Adding podman generate quadlet conflicts with that goal and it would imply that new Quadlet features had to be added there as well. The current strategy is to deprecate podman generate systemd. That means that no new features will be added to generate systemd but only bug fixes.

このコメントから分かるように、Podmanは最終的には既存のコンテナからユニットファイルを生成する既存のワークフローから脱却して、KubernetesやComposeのような宣言的なワークフローを目指しており、これがPodmanにQuadletをマージした目的とのこと。このゴールと衝突するため、Quadletなファイルを生成する機能をPodmanに追加する予定はないようです。

確かにQuadletはコンテナやPodだけでなく、NetworkやVolume、Imageも宣言的に書くことができ再配布しやすいですね。Kubernetes YAMLを使ったPodmanコンテナやPodの起動をsystemdと連携できるのも魅力です。

ちなみに、podman run コマンドなどからQuadletファイルを生成する podlet というツールは開発されているようです。

🦭 おわりに

以上で、Podmanとsystemdを連携させるQuadletを完全に理解できました。Podmanユーザーの方はもちろん、Dockerユーザーの方も興味あればぜひ触ってみてください。WindowsやMacをお使いでも、Podman Desktopで簡単に試せると思います。Quadletで遊びたい場合は、systemdがinitプロセスである必要があるので、podman machine init して Fedora CoreOS のVMを作成した後に、podman machine sshして試すのが簡単です。

[nishipy-MBP] $ podman machine list
NAME        VM TYPE     CREATED     LAST UP            CPUS        MEMORY      DISK SIZE
fedoracos*  qemu        4 days ago  Currently running  1           2GiB        100GiB
[nishipy-MBP] $ podman machine ssh fedoracos 
Connecting to vm fedoracos. To close connection, use `~.` or `exit`
Fedora CoreOS 39.20231119.2.0
Tracker: https://github.com/coreos/fedora-coreos-tracker
Discuss: https://discussion.fedoraproject.org/tag/coreos

Last login: Fri Dec 11 20:12:59 2023 from 192.168.127.1
core@fedora-39:~$ 

以上です。この記事のタイトルが怒られないことを祈りつつ…🎅

🦭 (余談)containers/ansible-podman-collections でも Quadlet をサポートしたい?

最近Pythonを書きたくなった時にPR送っている Ansible Collection containers/ansible-podman-collections でも、Quadletをサポートするモジュール/オプションを追加したいな〜と思い、Issue作って提案してみました。Quadletファイル詳しい人は、ansible.builtin.template module使う気もしますが…

https://github.com/containers/ansible-podman-collections/issues/671

🦭 References

Built with Hugo
Theme Stack designed by Jimmy