RHEL 実践 #3 コンテナワークロード: Podman、systemd (quadlet)

読了 8分

#1 Web サーバと #2 DB では、nginx と PostgreSQL を RHEL 上に直接インストールして systemd に登録しました。同じワークロードを今回はコンテナとして立て直します。RHEL は Docker の代わりに Podman を標準コンテナエンジンとして採用しており、Podman はデーモンなしで動作し、一般ユーザ権限でもコンテナを動かせます。ここに quadlet を加えると、コンテナを systemd サービスのように扱えるので、前の記事で身につけた運用感覚をそのまま持ち込めます。

この記事の流れは 3 段階です。まず Podman でコンテナを立てる基本を押さえ、次に一般ユーザで動かす rootless 運用を見て、最後に quadlet で systemd に統合してブート時の自動起動まで繋げます。

Podman のインストールと最初のコンテナ #

RHEL 9 以上には Podman がデフォルトリポジトリに入っています。dnf でインストールします。

# インストール
sudo dnf install -y podman

# バージョン確認
podman version

Podman のコマンド体系は Docker とほぼ同じなので、Docker を使っていた方なら違和感なく移行できます。#1 の nginx をコンテナとして立ててみます。

# イメージ取得 (Red Hat レジストリ)
podman pull registry.access.redhat.com/ubi9/nginx-124

# ホスト 8080 をコンテナ 8080 に接続して実行
podman run -d --name web -p 8080:8080 \
  registry.access.redhat.com/ubi9/nginx-124 \
  nginx -g "daemon off;"

-d はバックグラウンド実行、--name はコンテナ名、-p はポート接続です。コンテナが立ったかは podman ps で確認し、curl -I http://localhost:8080 でローカルの応答を受け取ってみます。イメージの出所としては、Red Hat が提供する UBI (Universal Base Image) 系を推奨します。RHEL と同じ基盤の上でビルドされており、互換性とセキュリティアップデートの面で運用環境に最も合います。

ボリュームと環境変数で DB を立てる #

#2 の PostgreSQL をコンテナとして立てると、コンテナの利点がはっきりします。データはホストのボリュームに残し、コンテナ自体はいつでも入れ替えられます。

# データを置くホストディレクトリ
mkdir -p ~/pgdata

# PostgreSQL コンテナ実行
podman run -d --name db \
  -p 5432:5432 \
  -e POSTGRESQL_USER=appuser \
  -e POSTGRESQL_PASSWORD=secret \
  -e POSTGRESQL_DATABASE=appdb \
  -v ~/pgdata:/var/lib/pgsql/data:Z \
  registry.redhat.io/rhel9/postgresql-16

-e で初期ユーザとデータベースを環境変数として渡し、-v でホストの ~/pgdata をコンテナのデータディレクトリに接続します。ボリュームパス末尾の :Z が RHEL では特に重要です。

SELinux ボリュームマウントと :Z #

RHEL は SELinux が enforcing なので、コンテナプロセスは基本的にホストファイルにアクセスできません。ボリュームオプションに :Z を付けると、Podman がそのディレクトリにコンテナ専用の SELinux ラベル (container_file_t) を付与してくれて、コンテナが読み書きできるようになります。

  • :Z (大文字) はそのコンテナだけが使うプライベートラベルです。単一コンテナのデータに適します。
  • :z (小文字) は複数のコンテナが共有するラベルです。同じディレクトリを複数のコンテナで一緒に使うときに使います。

:Z を抜かすとコンテナのログに権限エラーが出て DB が初期化されません。#1 で見た SELinux の問題と同じ文脈であり、コンテナではこの 1 文字で解決します。

ホストの 8080・5432 を外部に開くには、#1 と同じように firewalld でポートを開放します。コンテナだからといってファイアウォールが変わるわけではありません。

# ポートを永続開放して反映
sudo firewall-cmd --add-port=8080/tcp --permanent
sudo firewall-cmd --add-port=5432/tcp --permanent
sudo firewall-cmd --reload

DB ポート (5432) を外部に直接開けることは運用では推奨しません。#2 で扱ったとおりアクセス範囲を狭めるか、同じホストの Web コンテナだけがアクセスできるようにしておくほうが安全です。

rootless コンテナ: 一般ユーザで運用 #

Podman は root なしで一般ユーザ権限でコンテナを動かせます。これを rootless と呼び、コンテナが破られてもホスト root に波及しないので、セキュリティ上の基本推奨です。前の podman run コマンドを sudo なしで一般ユーザとして実行すると、そのまま rootless で動作します。rootless には 2 つの制約を覚えておく必要があります。

  • 1024 未満の特権ポートは一般ユーザがそのまま開けません。 そのため 80 の代わりに 8080 のように 1024 以上のポートで立てて、必要ならホストの reverse proxy や net.ipv4.ip_unprivileged_port_start の調整で 80 に公開します。
  • コンテナはそのユーザに従属します。 root が作ったコンテナと一般ユーザが作ったコンテナは互いに見えません。podman ps は現在のユーザのコンテナだけを表示します。

rootless コンテナのイメージとストレージは、システム全体ではなくユーザのホーム (~/.local/share/containers) の下に入ります。ユーザごとに隔離されるこの構造が rootless セキュリティの土台です。

quadlet で systemd に統合 #

podman run で立てたコンテナは再起動すると消えます。運用環境では、コンテナも #1 の nginx のように systemd サービスとして管理してこそ自動再起動とブート起動になります。RHEL 9.4 以上では quadlet がその標準の方法です。

quadlet は .container のような宣言型ファイルを置くと、systemd がそれを読んでサービスユニットにしてくれる構造です。ユニットファイルを自分で書かずに、コンテナ定義だけ書けば済みます。rootless で使うにはユーザディレクトリにファイルを置きます。

# ユーザ quadlet ディレクトリ作成
mkdir -p ~/.config/containers/systemd

このディレクトリに web.container ファイルを作ります。

# ~/.config/containers/systemd/web.container
[Unit]
Description=Nginx web container

[Container]
Image=registry.access.redhat.com/ubi9/nginx-124
PublishPort=8080:8080
Exec=nginx -g "daemon off;"

[Service]
Restart=always

[Install]
WantedBy=default.target

[Container] セクションが quadlet の核心です。主なキーは次のとおりです。

  • Image: 立てるイメージ。podman run のイメージ引数に相当します。
  • PublishPort: ポート接続。-p に相当します。
  • Volume: ボリュームマウント。-v に相当し、:Z もそのまま付けます。
  • Environment: 環境変数。-e に相当します。

DB コンテナも同じ方式で db.container を作ります。[Container]VolumeEnvironment を追加で書く点だけが異なります。

# ~/.config/containers/systemd/db.container
[Container]
Image=registry.redhat.io/rhel9/postgresql-16
PublishPort=5432:5432
Volume=%h/pgdata:/var/lib/pgsql/data:Z
Environment=POSTGRESQL_USER=appuser
Environment=POSTGRESQL_PASSWORD=secret
Environment=POSTGRESQL_DATABASE=appdb

[Service]
Restart=always

[Install]
WantedBy=default.target

%h はユーザのホームディレクトリを指す systemd 指定子です。ファイルを置いた後は、systemd が新しい定義を認識するように daemon-reload をしてからサービスを起動します。

# quadlet ファイルを systemd が読むように更新
systemctl --user daemon-reload

# サービス起動 (ファイル名 web.container → web サービス)
systemctl --user start web
systemctl --user start db
systemctl --user status web

quadlet は web.container ファイルから web.service という systemd サービスを自動生成します。そのため systemctl --user start web のように拡張子なしの名前で扱います。コンテナが systemd サービスになったので、enable なしでも [Install] WantedBy=default.target に従ってユーザセッション開始時に一緒に立ち上がります。

ブート時の自動起動: linger #

ここに 1 つの落とし穴があります。rootless ユーザサービスはそのユーザがログインしているときだけ動作し、ログアウトしたり再起動後にログインしなかったりするとコンテナも落ちます。ユーザがログインしなくてもサービスがブート時に立ち上がるようにするには、linger をオンにする必要があります。

# 現在のユーザの linger 有効化 (root 権限が必要)
sudo loginctl enable-linger $USER

# 確認
loginctl show-user $USER | grep Linger

enable-linger をオンにすると、そのユーザの systemd ユーザセッションがブート時点からバックグラウンドで生き続けるので、ログインの有無に関係なく quadlet コンテナが自動的に起動します。rootless でサーバを運用するときに必ず一緒に押さえておく設定です。

システム全体の quadlet #

ユーザではなくシステム全体のサービスとしてコンテナを運用するには、ファイルを /etc/containers/systemd/ に置きます。形式は同じで、管理コマンドから --user だけが抜けます。

# システム全体の quadlet ファイル配置
sudo cp web.container /etc/containers/systemd/

# システム systemd 更新・起動
sudo systemctl daemon-reload
sudo systemctl start web

システム全体の quadlet は root 権限で動作するので linger が不要で、通常の systemd サービスのようにブート時に自動起動します。ただしコンテナが root で動くことになり、rootless のセキュリティ上の利点は消えます。単一ユーザのサーバなら rootless + linger を、システムレベルのサービスとしてまとめて管理する必要があるなら、システム全体の quadlet を選ぶとよいでしょう。

ログと診断 #

コンテナが立たなかったりエラーが出たりするときに見る場所は 2 か所です。コンテナ自体のログは podman で、systemd 統合後のサービスログは journalctl で見ます。

# コンテナの標準出力ログ (リアルタイムは -f)
podman logs web
podman logs -f web

# quadlet サービスログ (rootless / 全体)
journalctl --user -u web
sudo journalctl -u web

DB ボリュームの権限問題のように SELinux が塞ぐ場合は podman logs に権限エラーとして現れ、ホスト側の audit ログ (/var/log/audit/audit.log) でも AVC denied として確認できます。このときは #1 と同じく、ボリュームに :Z が付いているかから点検すると、ほとんどの場合は解けます。

運用ポイント #

  • Podman はデーモンがありません。 Docker のように常駐デーモンに依存しないので、各コンテナが systemd サービスとして独立に管理されます。quadlet がその統合点です。
  • rootless を基本にしてください。 一般ユーザで立てるとコンテナの侵害がホスト root に波及しません。特権ポートとユーザ従属という制約だけ認識すれば済みます。
  • ボリュームには :Z を付けてください。 SELinux enforcing 環境でホストディレクトリをマウントするときはほぼ必須です。単一コンテナは :Z、共有は :z です。
  • ブート時の自動起動は linger です。 rootless quadlet は loginctl enable-linger をオンにしてこそ、ユーザログインと無関係にブート時に立ち上がります。
  • ポートは依然として firewalld です。 コンテナでも直接インストールでも、外部公開には firewalld の開放が必要です。

まとめ #

この記事で押さえたこと:

  • Podman の基本podman pullpodman run でコンテナを立て、-p-v-e でポート・ボリューム・環境変数を接続
  • rootless。一般ユーザで運用してセキュリティ隔離を得て、特権ポートとユーザ従属の制約を認識
  • SELinux ボリューム。ホストマウントには :Z (プライベート)・:z (共有) ラベルを付与
  • quadlet~/.config/containers/systemd/*.container にコンテナを宣言して systemctl --user daemon-reload の後に起動
  • ブート時の自動起動。rootless は loginctl enable-linger、全体は /etc/containers/systemd/ に配置

次回: モニタリング #

Web と DB をコンテナとして立てたので、今度はこれらのワークロードがうまく動いているかをのぞく番です。

#4 モニタリング: Cockpit、PCPでは、RHEL の Web 管理コンソールである Cockpit でシステムとコンテナを一目で見る方法、そして PCP (Performance Co-Pilot) で性能指標を収集して追跡する流れを 1 サイクルで整理します。

X