12のビルド

ルートなしでコンテナイメージをビルドするCIランナー

Ryzenインスタンス上のセルフホスト型Actionsランナー。BuildahでルートレスOCIイメージをビルドし、同じマシン上のプライベートレジストリにプッシュします。

これが構築するもの

自分のフォージに接続されたCIランナー。リポジトリをチェックアウトし、デーモンもルートもなしにコンテナイメージをビルドし、その結果を同じインスタンス上で動作するプライベートレジストリにプッシュします。特権ソケットはどこにもマウントされておらず、パイプライン内のプロセスはuidゼロでは実行されません。

ほとんどのセルフホストCIセットアップは、ホストのコンテナデーモンをジョブにマウントすることでイメージビルドを解決します。それは機能し、すべてのパイプライン(誰かがプルリクエストを開いたものも含む)にマシンの完全な制御を渡します。ルートレスモードのBuildahは、それなしで同じ仕事をし、専用コアでは遅くありません。

始める前に

  • R-8。イメージビルドはほとんどのチームが実行する中で最もシングルコアに依存するものであり、Zen 5は当社が販売する中でコアあたり最速です。400ギガバイトのNVMeは非常に多くのレイヤーを保持します。
  • Actionsプロトコルを話す既に実行中のフォージと、その中にランナー登録トークンを作成する権限。
  • ホスト名: ランナーにci.example.com、レジストリにregistry.example.com

1. 名前空間範囲を持つ非特権ユーザー

apt update && apt install -y podman buildah skopeo fuse-overlayfs uidmap slirp4netns git nodejs nginx apache2-utils
useradd -m -s /bin/bash runner
echo "runner:200000:65536" >> /etc/subuid
echo "runner:200000:65536" >> /etc/subgid
loginctl enable-linger runner

これらの2つの範囲がルートレスコンテナを可能にします: runnerアカウントは65,000の従属IDを所有し、コンテナ内でルートであると信じているプロセスは、外部では非特権IDにマッピングされます。Lingeringはユーザーセッションを維持し、そのアカウント下のsystemdユニットがログアウト後も存続するようにします。

Nodeがインストールされているのは、ほとんどの再利用可能なアクションがJavaScriptであり、この構成ではランナーがそれらをホスト上で実行するためです。チェックアウトステップでそれを発見することは、よくある10分の回り道です。

2. NVMe上のルートレスストレージ

sudo -u runner mkdir -p /home/runner/.config/containers
sudo -u runner tee /home/runner/.config/containers/storage.conf <<EOF
[storage]
driver = "overlay"
graphroot = "/home/runner/.local/share/containers/storage"

[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
sudo -u runner podman info --format "{{.Store.GraphDriverName}} {{.Host.Security.Rootless}}"

最後のコマンドはoverlay trueと答えるはずです。vfsドライバーはfuse-overlayfsが欠落していることを意味し、vfsはビルドのたびにすべてのレイヤーを完全にコピーするため、90秒のパイプラインが6分になります。

3. プライベートレジストリ

apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.com

レジストリをループバックにバインドし、認証を含む外向きのすべてをnginxに任せます。/etc/docker/registry/config.ymlで:

version: 0.1
storage:
  filesystem:
    rootdirectory: /srv/registry
  delete:
    enabled: true
http:
  addr: 127.0.0.1:5000

レジストリ自体は資格情報を保持しません。プロキシを通過していないリクエストを受信しないためです。パスワードをチェックする場所が1つあることは、矛盾する可能性のある2つの場所よりも優れています。

server {
  listen 443 ssl;
  server_name registry.example.com;
  ssl_certificate     /etc/letsencrypt/live/registry.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
  client_max_body_size 0;
  chunked_transfer_encoding on;

  location /v2/ {
    auth_basic "restricted";
    auth_basic_user_file /etc/docker/registry/htpasswd;
    proxy_pass http://127.0.0.1:5000;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 900s;
  }
}

client_max_body_size 0はアップロード制限を削除します。nginxのデフォルトをそのままにしておくと、1メガバイトを超えるレイヤーはプッシュの約3分の2で413で失敗します。これは印象的な午後を過ごすことになります。

systemctl enable --now docker-registry nginx

4. ランナー

cd /usr/local/bin
wget -O act_runner https://code.forgejo.org/forgejo/runner/releases/download/v6.3.1/forgejo-runner-6.3.1-linux-amd64
chmod +x act_runner
sudo -u runner mkdir -p /home/runner/.runner-cfg
cd /home/runner/.runner-cfg && sudo -u runner /usr/local/bin/act_runner generate-config > config.yaml

生成されたファイルを編集して、ジョブがコンテナ内ではなくホスト上で実行されるようにします。Buildahがすでに分離を提供しており、ネストは複雑さを追加するだけだからです:

runner:
  capacity: 2
  timeout: 1h
  labels:
    - "debian-13:host"
host:
  workdir_parent: /home/runner/work
cache:
  enabled: true
  dir: /home/runner/cache

8コアでの容量2は意図的です: ビルドはほぼシリアルであり、4コアずつの2つの同時ジョブは、同じキャッシュをめぐって競合する4つのジョブよりも早く終了します。生成されたトークンでフォージに登録します:

cd /home/runner/.runner-cfg
sudo -u runner /usr/local/bin/act_runner register --no-interactive \
  --instance https://forge.example.com --token <registration token> \
  --name ci-ams --labels debian-13:host

次に、非特権アカウントとして実行されるユニット:

[Unit]
Description=Actions runner
After=network-online.target

[Service]
User=runner
WorkingDirectory=/home/runner/.runner-cfg
ExecStart=/usr/local/bin/act_runner daemon --config /home/runner/.runner-cfg/config.yaml
Restart=always
Environment=HOME=/home/runner
Environment=XDG_RUNTIME_DIR=/run/user/3001
NoNewPrivileges=yes

[Install]
WantedBy=multi-user.target

runnerアカウントの実際のuidをXDG_RUNTIME_DIRで置き換えます; id -u runnerがそれを表示します。ルートレスpodmanはそのディレクトリが存在することを必要とし、これはステップ1のlinger設定が保証するものです。

systemctl daemon-reload && systemctl enable --now act-runner

5. ビルドしてプッシュするワークフロー

リポジトリ内の.forgejo/workflows/image.yamlで:

on:
  push:
    branches: [main]

jobs:
  image:
    runs-on: debian-13
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: |
          buildah bud --layers --format oci -t app:${{ github.sha }} .
      - name: Push
        run: |
          buildah login -u ci -p ${{ secrets.REGISTRY_PASSWORD }} registry.example.com
          buildah push app:${{ github.sha }} docker://registry.example.com/app:${{ github.sha }}
          buildah push app:${{ github.sha }} docker://registry.example.com/app:latest

--layersはレイヤーキャッシュを有効にし、これは毎コミットでの依存関係の再ビルドと、変更時の再ビルドの違いです。

検証

ユニットが開始されてから数秒以内に、ランナーがフォージのランナーリストにオンラインとして表示されるはずです。次にコミットをプッシュし、マシンからジョブを監視します:

journalctl -fu act-runner

終了したら、イメージが単に成功を報告するのではなく、実際に到着したことを確認します:

skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/app

ダイジェスト、レイヤーリスト、両方のタグが必要です。次に、それを実行できることを証明します。手元に別のマシンがあれば:

podman run --rm registry.example.com/app:latest --version

最後に、このビルド全体が依存する主張です。ジョブが実行されている間に、プロセスを誰が所有しているかを確認します:

ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"

すべてのプロセスはrunnerに属し、セキュリティチェックはtrueと答えます。パイプライン内の何もルートを保持していないため、侵害されたビルドスクリプトは非特権アカウントと名前空間を取得し、レジストリキーとハイパーバイザーは取得しません。

その後

レジストリストレージは、古いタグを削除する何かがない限り無制限に成長するため、保持ルールを信じるようになったら、週次タイマーでregistry garbage-collectを実行します。ビルドがテストではなくボトルネックになった場合、比較ページは次のサイズアップがどのように見えるかを示します; 多くの人が期待するほどコアは役に立たず、速いコアははるかに役立ちます。

準備はできている

都市を選べ。サイズを選べ。コインで支払え。

あなたが誰かに関するフォームも、承認する人間を待つ必要も、確認のための電話もありません。請求が完了すれば、認証情報があなたの受信トレイに届きます。