RHEL 中級 #7 コンテナ入門 — Podman/Buildah/Skopeo

読了 10分

この記事で RHEL 中級シリーズを締めくくります。最後のテーマはコンテナ、より正確には RHEL 9 が標準として採択した Podman です。Docker とコマンドは似ていますが内部構造はかなり異なります。デーモンがなく、一般ユーザー権限でコンテナを立ち上げ、systemd とも自然に統合されます。この違いを理解せずに入ると Docker 式の習慣をそのまま持ってきてミスしやすいです。この記事ではその違いを運用観点から整理します。

RHEL 中級 シリーズでこの記事の位置:

Docker 自体の入門は別シリーズで扱いました — Docker 基礎。この記事は Docker を 1 度でも使った読者を想定し、同じ作業を RHEL 9 の標準ツールでどうやるかに焦点を当てます。

なぜ RHEL 9 は Docker ではないのか #

CentOS/RHEL 8 から Red Hat は Docker パッケージをデフォルトリポジトリから外しました。その空いた役割を Podman が埋めました。理由はシンプルではありませんが運用観点で見ると明確な 1 行に要約されます。

Docker は root デーモンにすべてのコンテナ権限が集中する構造Podman はデーモンなしでユーザー権限で直接実行される構造

比較DockerPodman
デーモンdockerd 常時実行なし
コマンド → コンテナクライアントがデーモンに RPCfork/exec 直接実行
デフォルト権限root デーモンユーザー権限 (rootless デフォルト)
攻撃表面デーモン 1 か所に集中コンテナ別に隔離
systemd 統合別途 wrapper が必要quadlet で 1 級市民
composedocker-composepodman compose / quadlet
コマンドdocker pspodman ps (alias で docker も可)

コマンドが同じなので学習コストはほぼ 0 ですが、rootless がデフォルトという点 は運用観点から大きな違いを生みます。

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

インストール + 確認
$ sudo dnf install -y podman
$ podman --version
podman version 4.9.x

Docker ユーザーに馴染みのフローそのまま:

最初のコンテナ
$ podman run --rm -it docker.io/library/alpine sh
/ # apk add curl
/ # exit

# nginx を立ち上げる
$ podman run -d --name web -p 8080:80 docker.io/library/nginx:1.27
$ curl http://localhost:8080

$ podman ps
$ podman logs web
$ podman stop web && podman rm web

最初に見たらどこが違うかわからないほど。Docker と異なる点が露わになる箇所は「これを root なしでやった」 という事実です。

rootless コンテナ — 何が変わるか #

ユーザー名前空間 #

rootless Podman は ユーザー名前空間 (user namespace) の上で動作します。コンテナ内の root (uid 0) はホストでは一般ユーザーの uid にマッピングされます。コンテナ内では権限があるように見えてもホストリソースに対しては一般ユーザー分の権限しか持ちません。

マッピング確認
$ id
uid=1000(curtis) gid=1000(curtis)

$ podman unshare cat /proc/self/uid_map
         0       1000          1
         1     100000      65536

読み方: コンテナ内 uid 0 = ホスト uid 1000 (私)、コンテナ内 uid 1〜65536 = ホスト uid 100000〜165535 (subuid)。

このマッピングが可能になるには /etc/subuid/etc/subgid にユーザー別領域が設定されていなければなりません。RHEL 9 はユーザー作成時に自動で掴みます。

確認
$ cat /etc/subuid
curtis:100000:65536

rootless 制約 #

権限が弱い分、できないこともあります。

  • ポート 1024 未満のバインディング ✗ — 80、443 は sysctl で解放するか Caddy/HAProxy のような reverse proxy の後ろに置く
  • NFS マウント ✗ — ユーザー名前空間で NFS は通常ブロックされます
  • AF_NETLINK 制限 — ホストネットワークの奥深くに入るツールは動作しない可能性
  • MTU/IP forwarding 一部オプション制限

このような限界に当たれば rootful モード (sudo podman ...) に逃げることができます。両者は完全に分離されたストレージを使います。

ストレージ位置
# rootless
~/.local/share/containers/storage/

# rootful
/var/lib/containers/storage/

同じマシンに同じイメージを 2 度ダウンロードすることになるので、運用では片側に統一 するのが標準です。セキュリティが重要なら rootless、ホストネットワーク統合が絶対的なら rootful。

ポート 1024 未満のバインディング #

80 番ポートを解放
# 永続適用
$ sudo sh -c 'echo "net.ipv4.ip_unprivileged_port_start=80" > /etc/sysctl.d/99-podman-rootless.conf'
$ sudo sysctl --system

# その後
$ podman run -d -p 80:80 nginx

イメージ — レジストリと OCI #

Podman は OCI (Open Container Initiative) 標準に従います。Docker イメージと互換です。ただしレジストリ名を 明示的に 書く点が異なります。

レジストリ名明示
$ podman pull nginx:1.27
?: Please select an image:
   ▸ registry.access.redhat.com/nginx:1.27
     registry.redhat.io/nginx:1.27
     docker.io/library/nginx:1.27

Docker は問わずに docker.io を仮定しますが、Podman は /etc/containers/registries.conf に書かれた候補のうちどこから受けるか 1 度確認します。自動化スクリプトなら常にフルパス (docker.io/library/nginx:1.27) で書くのが安全。

/etc/containers/registries.conf 要部分
unqualified-search-registries = ["registry.access.redhat.com", "registry.redhat.io", "docker.io"]

Red Hat レジストリ #

RHEL ユーザーがよく出会う 2 か所:

  • registry.access.redhat.com — 認証なしで受けられる公開イメージ (UBI など)
  • registry.redhat.io — サブスクリプション認証が必要。podman login registry.redhat.io でログイン後に使用

UBI (Universal Base Image) — Red Hat が RHEL ベースイメージを無料で配布するライン。運用で RHEL の上に RHEL 互換コンテナを回す標準です。

UBI 使用
$ podman pull registry.access.redhat.com/ubi9/ubi:latest
$ podman run --rm -it registry.access.redhat.com/ubi9/ubi cat /etc/redhat-release
Red Hat Enterprise Linux release 9.x ...

Buildah — Dockerfile なしでもビルド #

Podman は podman build で Dockerfile ビルドをサポートします (内部的に Buildah が働きます)。Buildah を直接使えばより細かく制御できます。

Dockerfile ビルド #

Containerfile (Dockerfile と同じ)
FROM registry.access.redhat.com/ubi9/ubi:latest
RUN dnf install -y python3 && dnf clean all
COPY app.py /opt/app.py
CMD ["python3", "/opt/app.py"]
ビルド
$ podman build -t myapp:1.0 .
$ buildah bud -t myapp:1.0 .  # 同じ意味

Containerfile があればそれを使い、なければ Dockerfile を使います。両方 OCI 標準形式。

Buildah スクリプトビルド #

Dockerfile は layer ごとに commit が起きてイメージが大きくなりやすい構造です。Buildah は buildah from → コンテナ開始 → 変更 → buildah commit の明示的フローを提供して layer 数を直接制御できます。

Buildah スクリプトビルド — 単一 layer
#!/bin/bash
ctr=$(buildah from registry.access.redhat.com/ubi9/ubi)
buildah run "$ctr" -- dnf install -y python3
buildah run "$ctr" -- dnf clean all
buildah copy "$ctr" app.py /opt/app.py
buildah config --cmd '["python3","/opt/app.py"]' "$ctr"
buildah commit "$ctr" myapp:1.0
buildah rm "$ctr"

dnf install + dnf clean all を 2 つの RUN に分けても結果イメージには 1 layer として入ります。運用でイメージサイズを減らさなければならないときに有用な技法です。

Dockerfile でよく見る RHEL パターン #

UBI + マルチステージ
FROM registry.access.redhat.com/ubi9/go-toolset:latest AS build
WORKDIR /src
COPY . .
RUN go build -o /tmp/app ./cmd/app

FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
COPY --from=build /tmp/app /usr/local/bin/app
USER 1001
CMD ["/usr/local/bin/app"]

ubi9/ubi-minimal は約 100MB レベルのスリムなベース。USER 1001 のように non-root ユーザーに落とすのがコンテナセキュリティの基本。

Skopeo — レジストリの間を移すツール #

skopeo はイメージを 解凍せずに レジストリの間で移したり検査できるツール。バックアップ、ミラーリング、エアギャップ (air-gapped) 環境に必須。

インストール
$ sudo dnf install -y skopeo

よく使うフロー #

イメージ検査 — 受けずに manifest を見る
$ skopeo inspect docker://registry.access.redhat.com/ubi9/ubi:latest
{
  "Name": "registry.access.redhat.com/ubi9/ubi",
  "Digest": "sha256:abc...",
  "Architecture": "amd64",
  ...
}
レジストリ間のコピー
$ skopeo copy \
  docker://docker.io/library/nginx:1.27 \
  docker://registry.example.com/mirror/nginx:1.27
エアギャップ — ディスクに落とす
# インターネット接続されたところで
$ skopeo copy \
  docker://docker.io/library/nginx:1.27 \
  dir:/tmp/nginx-1.27

# tar で束ねて移した後
$ tar czf nginx-1.27.tar.gz -C /tmp nginx-1.27

# 隔離されたところで
$ tar xzf nginx-1.27.tar.gz -C /tmp
$ skopeo copy \
  dir:/tmp/nginx-1.27 \
  docker://registry.internal/mirror/nginx:1.27
タグ一覧
$ skopeo list-tags docker://registry.access.redhat.com/ubi9/ubi

docker pull + docker save + docker load の組み合わせを 1 コマンドで終えると見ればよいです。デーモンが必要ないので CI/CD パイプラインでも軽く回ります。

Podman + systemd — quadlet #

運用でコンテナを立ち上げる標準的な方法は systemd unit として管理することです。RHEL 9 の Podman 4.4+ から quadlet が導入されて systemd フレンドリーな unit ファイルでコンテナを定義できます。

/etc/containers/systemd/web.container
[Unit]
Description=Nginx web container
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/nginx:1.27
PublishPort=8080:80
Volume=/srv/web:/usr/share/nginx/html:ro,Z
AutoUpdate=registry

[Service]
Restart=always

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

systemd に登録して開始:

quadlet アクティベーション
$ sudo systemctl daemon-reload
$ sudo systemctl start web.service
$ sudo systemctl status web.service
$ journalctl -u web.service -f

quadlet は .container ファイルを systemd が起動時点で自動で .service unit に変換してくれます。systemd がコンテナライフサイクルの 1 級管理者 になる構造です。

ユーザー単位でも同じく可能です — ~/.config/containers/systemd/web.container に置いて systemctl --user で管理。

Volume の :Z — SELinux ラベル #

quadlet 例の Volume=/srv/web:/usr/share/nginx/html:ro,ZZ はホストディレクトリに コンテナ専用 SELinux ラベル を自動で付けます。

オプション意味
:z共有ラベル (複数のコンテナがアクセス可能)
:Z専用ラベル (このコンテナのみアクセス)
オプションなしラベルが付かない → SELinux Enforcing 環境でコンテナが拒否される

#1 SELinux 入門 で扱ったラベル概念がコンテナボリュームでもそのまま生きているという点。このオプションを抜かしてコンテナが読み取り拒否される事故が最もよくあります。

Podman compose — docker-compose 互換 #

既存の docker-compose.yml をそのまま使いたいなら:

podman-compose
$ sudo dnf install -y podman-compose
$ podman-compose -f docker-compose.yml up -d

ただし運用では quadlet に移すフローを推奨します — systemd 統合と起動時自動開始を一度に把握できます。

Auto Update — イメージ自動更新 #

quadlet や systemd unit に AutoUpdate=registry を書いておけば、podman-auto-update.timer (RHEL 9 デフォルト timer の 1 つ) が毎日 1 度新しいイメージを確認して自動でダウンロードしてコンテナを再起動します。

確認
$ systemctl list-timers podman-auto-update.timer
$ podman auto-update --dry-run

運用推奨:

  • 開発/ステージング: AutoUpdate=registry で自動更新
  • プロダクション: 自動更新を切って明示的なデプロイ手順で (意図しないマイナーバージョン変更を遮断)

デバッグ — コンテナが立ち上がらないとき #

チェックリスト
# 1. イメージが実際にダウンロードされているか
$ podman images
$ podman pull <image>

# 2. コンテナ試行時の正確なエラー
$ podman run --rm -it <image> sh
# (バックグラウンド -d で立ち上げて「Exited」だけ見て終わることが多い)

# 3. 生きているコンテナのログ
$ podman logs <name>
$ podman logs --tail 100 -f <name>

# 4. SELinux 拒否 — Volume に :Z を抜かしたとき
$ sudo ausearch -m AVC -ts recent
$ sudo journalctl -t setroubleshoot --since "10 min ago"

# 5. ポート衝突
$ sudo ss -tlnp | grep :8080

# 6. quadlet — 変換結果確認
$ /usr/libexec/podman/quadlet -dryrun

-d で立ち上げた途端コンテナが死んで消えるなら、--rm オプションを外して podman logs <name> または podman run --rm -it <image> sh でインタラクティブに入ってみるのが最速の診断です。

よくある落とし穴 #

  • Docker 式の習慣docker.io/library/ 接頭を抜かすと unqualified-search 候補のうちどこから受けるか問われたり別のイメージを受けることになります。自動化では常にフルパスを使ってください。
  • rootless で 80 ポートを試行bind: permission denied。sysctl で解放するか reverse proxy の後ろへ。
  • Volume :Z 漏れ — SELinux Enforcing 環境 (RHEL 9 デフォルト) でコンテナがホストディレクトリを読めません。
  • rootless ↔ rootful ストレージ分離sudo podman pull で受けたイメージは一般 podman コマンドからは見えません。片側に統一。
  • コンテナ内 root = ホスト root ではない — rootless は user namespace でマッピングされる。セキュリティ前提が異なることを忘れないでください。
  • イメージキャッシュの蓄積podman system prune -a -f で周期的整理。quadlet AutoUpdate=registry だけ使うと古いイメージが積み重なり続けます。

覚えておくコマンド #

作業コマンド
イメージ受け / 確認podman pull <img> / podman images
コンテナ実行podman run -d --name <n> -p 8080:80 <img>
ログを見るpodman logs -f <n>
シェル進入podman exec -it <n> bash
イメージビルドpodman build -t <tag> .
レジストリコピーskopeo copy docker://A docker://B
イメージ検査skopeo inspect docker://<img>
systemd 統合quadlet *.container + systemctl daemon-reload
整理podman system prune -a

まとめ #

  • Podman = Docker コマンド互換 + デーモンなし + rootless デフォルト。RHEL 9 のコンテナ標準。
  • Buildah — イメージビルドツール。podman build が内部で使用。Dockerfile/Containerfile + スクリプトビルド両方サポート。
  • Skopeo — レジストリの間を移すツール。ミラーリング、エアギャップ、CI/CD パイプラインに必須。
  • quadlet — Podman + systemd の 1 級統合。.container ファイルを systemd が自動で service に変換。
  • 運用の要となる落とし穴: フルレジストリパス、Volume の :Z、rootless ↔ rootful 分離、コンテナ root とホスト root の違い。

シリーズ締めくくり #

これで RHEL 中級シリーズ 7 編を終えます。SELinux、LVM、ストレージ、ネットワーキング、ログ、スケジューリング、コンテナまで、運用の日常でよく出会う 7 領域を一巡しました。

次は RHEL 上級 シリーズです。クラスタリング (Pacemaker)、高可用性、チューニング、セキュリティ強化、Ansible 自動化のような 1 マシンを越えて複数マシンを一緒に運用する領域に移ります。

ここまで読んでいただきありがとうございました。

X