Buildah:Docker DaemonなしでOCIコンテナイメージをビルド — CI/CDのためのrootlessソリューション

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

午前2時。チームのGitLab CIパイプラインが赤くなった。ずっと前から嫌いだったあのお馴染みのログが表示された:

Cannot connect to the Docker daemon at unix:///var/run/docker.sock.
Is the docker daemon running?

実は、このエラーに遭遇したのは初めてではなかった。ECサービスのマイクロサービスをデプロイしていたとき、ホストからDockerソケットをマウントできず、ジョブがisolatedコンテナ内で実行されるたびにイメージビルドパイプラインが失敗していた。当時は応急処置として/var/run/docker.sockをCIコンテナにマウントしたら動くようになったが、後になってセキュリティ面でその月最悪の決断だったと気づくことになる。

CI/CD環境でDocker Daemonが問題になる理由

パイプラインでdocker buildを使う場合、2つの根本的な問題がある:

  • DockerはrootでDaemonを実行する必要がある — KubernetesやGitLab shared runnersのようなコンテナ化された環境では、ホスト上でDocker daemonが常に利用できるとは限らない。
  • Dockerソケットのマウントは深刻なセキュリティリスクだ/var/run/docker.sockにアクセスできるプロセスは、ホストのrootへ権限昇格が可能になる。

一般的な解決策はDocker-in-Docker(DinD)だ — CIコンテナ内に別のDocker daemonを起動する方法だ。聞こえはいいが、DinDは--privilegedモードを必要とし、コンテナのセキュリティ機能のほとんどを無効にしてしまう。本番環境では絶対に避けたいことだ。

別のツールが必要だった。

Buildahとは何か、そしてなぜこの問題を解決できるのか

BuildahはRed Hatが開発したOCI(Open Container Initiative)イメージビルドツールで、Buildah + Podman + Skopeoからなるdaemonレスコンテナツールセットの一部だ。重要なポイント:

  • Docker daemonが不要 — 完全にスタンドアロン
  • rootlessビルドをサポート — 一般ユーザーで実行でき、sudoは不要
  • 標準OCIイメージを出力 — Docker、Kubernetes、Podmanと完全互換
  • DockerfileContainerfileをそのまま読み込める

LinuxへのBuildahインストール

RHEL / CentOS / Fedora

sudo dnf install -y buildah

Ubuntu / Debian

sudo apt-get update
sudo apt-get install -y buildah

インストールの確認:

buildah version
# buildah version 1.35.0 (image-spec 1.1.0, runtime-spec 1.2.0)

rootlessビルド(sudoなし)を使うには、ユーザーネームスペースマッピングを設定する必要がある:

# "youruser"を実際のユーザー名に置き換えてください
echo "youruser:100000:65536" | sudo tee -a /etc/subuid
echo "youruser:100000:65536" | sudo tee -a /etc/subgid

Buildahを使ったイメージビルド方法

方法1:Containerfileを使う(シンプルでなじみやすい)

Containerfileを作成する — 構文はDockerfileと完全に同じ:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

ビルド:

# カレントディレクトリのContainerfileからビルド
buildah build -t my-node-app:latest .

# またはファイルを明示的に指定する
buildah build -f Containerfile -t my-node-app:latest .

以上。daemonもrootも不要だ。

方法2:シェルスクリプトで段階的にビルドする(scripted build)

これはBuildahの独自の強みだ。純粋なシェルスクリプトで各レイヤーを細かく制御できる:

#!/bin/bash
set -e

# ベースイメージからコンテナを作成
CONTAINER=$(buildah from node:20-alpine)

# コンテナのファイルシステムをホストにマウント
MOUNTPOINT=$(buildah mount $CONTAINER)

# ファイルをコンテナにコピー — ホストのcp/rsyncを使用、RUNレイヤー不要
mkdir -p $MOUNTPOINT/app
cp -r ./src $MOUNTPOINT/app/
cp package*.json $MOUNTPOINT/app/

# コンテナ内でコマンドを実行
buildah run $CONTAINER -- sh -c "cd /app && npm ci --only=production"

# イメージのメタデータを設定
buildah config --cmd '["node", "/app/server.js"]' $CONTAINER
buildah config --port 3000 $CONTAINER
buildah config --label version=1.0.0 $CONTAINER
buildah config --env NODE_ENV=production $CONTAINER

# アンマウントして最終イメージにコミット
buildah unmount $CONTAINER
buildah commit $CONTAINER my-node-app:v1.0.0

# 一時コンテナを削除
buildah rm $CONTAINER

echo "ビルド完了: my-node-app:v1.0.0"

この方法では、ホストのあらゆるツール(rsync、sed、jq…)を使ってファイルをパッケージング前に処理でき、余分なRUNレイヤーも増えない — 結果としてイメージサイズが小さくなり、ビルドも速くなる。

Rootless Build — rootなしで実行する

これが自分のCI/CD全体をBuildahに移行した主な理由だ。一般ユーザーで実行する:

# sudoは不要
whoami       # 出力: ci-runner
buildah build -t myapp:latest .
# ビルド成功 — root権限ゼロ

パイプラインが侵害されても、攻撃者はci-runnerユーザーの権限しか持てず、ホストのrootへの権限昇格はできない。これはDinDが決して保証できないことだ。

BuildahをCI/CDパイプラインに統合する

GitLab CI

stages:
  - build
  - push

build-image:
  stage: build
  image: quay.io/buildah/stable:latest
  variables:
    STORAGE_DRIVER: vfs        # ネストされたコンテナ環境ではvfsを使用
    BUILDAH_FORMAT: docker     # Docker registry互換の出力フォーマット
  script:
    - buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - buildah build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    - buildah push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

--privilegedなし、Dockerソケットマウントなし、DinDなし。ジョブはRed Hat公式のBuildahイメージでクリーンに実行される。

GitHub Actions

name: Build with Buildah

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Buildah
        run: sudo apt-get install -y buildah

      - name: Build image
        run: |
          buildah build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .

      - name: Push image
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | buildah login ghcr.io -u ${{ github.actor }} --password-stdin
          buildah push ghcr.io/${{ github.repository }}:${{ github.sha }}

ベストプラクティス:Buildah + Podmanで完全なワークフローを構築

チームのCI/CDをDocker-in-DockerからBuildahに切り替えた後、最も安定したワークフローとしてまとめたのは:

  1. イメージのビルド:Buildah(rootless、daemonレス)
  2. コンテナのテスト:Podmanでコンテナを実行して確認(docker runの代わりにpodman run — こちらもdaemon不要)
  3. レジストリへのプッシュ:buildah pushまたはskopeo copy
# ステップ1:ビルド
buildah build -t myapp:test .

# ステップ2:テスト(Podmanもrootless、daemon不要)
podman run --rm myapp:test npm test

# ステップ3:タグ付けしてproductionレジストリにプッシュ
buildah tag myapp:test registry.example.com/myapp:latest
buildah push registry.example.com/myapp:latest

# ビルド済みイメージの一覧を確認
buildah images

# 古いイメージを削除
buildah rmi myapp:test

Buildah + Podman + Skopeoの三位一体は、バックグラウンドで動くdaemonもroot権限も必要なく、サーバー環境でDockerを完全に代替できる。そして最も重要なこと — 悪用される/var/run/docker.sockが存在しないのだ。

ECサービスでコンテナ内のメモリリークをデバッグしていたあの夜を振り返ると、失われた時間のほとんどはDinDが引き起こす大量のレイヤーキャッシュコンフリクトによる不安定なビルド環境が原因だった。Buildahに切り替えてから、パイプラインはよりクリーンになり、再現性も上がり、セキュリティチームからスプリントレビューのたびに指摘を受けることもなくなった。

KubernetesやGitLab shared runners、あるいはDocker daemonが問題となるあらゆる環境でCI/CDを運用しているなら — 今使える最も実践的な解決策はBuildahだ。

Share: