Mười hai bài xây dựng

Một CI runner xây dựng container image không cần root

Một Actions runner tự lưu trữ trên instance Ryzen, xây dựng OCI image rootless bằng Buildah và đẩy chúng lên registry riêng trên cùng máy.

Nội dung bài này tạo ra

Một runner CI gắn với mã nguồn của riêng bạn, nó check out kho lưu trữ, xây dựng image container mà không cần daemon và không cần root, và đẩy kết quả vào registry riêng tư chạy trên cùng instance. Không có socket đặc quyền nào được mount ở bất kỳ đâu, và không có gì trong pipeline chạy với uid bằng 0.

Hầu hết các thiết lập CI tự quản lý giải quyết việc xây dựng image bằng cách mount daemon container của máy chủ vào job. Điều đó hoạt động, nhưng nó trao cho mọi pipeline, kể cả pipeline mà ai đó mở pull request vào, quyền kiểm soát hoàn toàn máy. Buildah ở chế độ không root làm công việc tương tự mà không có vấn đề đó, và trên các lõi chuyên dụng, nó không chậm hơn.

Trước khi bắt đầu

  • Một R-8. Xây dựng image là điều bị ràng buộc nhẹ nhất bởi đơn lõi mà hầu hết các nhóm chạy, và Zen 5 là nhanh nhất mỗi lõi mà chúng tôi bán. Bốn trăm gigabyte NVMe giữ được rất nhiều layer.
  • Một mã nguồn bạn đã chạy hỗ trợ giao thức Actions, và quyền tạo token đăng ký runner trong đó.
  • Tên máy chủ: ci.example.com cho runner và registry.example.com cho registry.

1. Một tài khoản không đặc quyền với dải namespace

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

Hai dải đó là những gì làm cho container không root trở nên khả thi: tài khoản runner sở hữu sáu mươi lăm nghìn id phụ, vì vậy một process tin rằng nó là root bên trong container được ánh xạ tới một id không đặc quyền bên ngoài nó. Lingering giữ phiên người dùng sống để các đơn vị systemd dưới tài khoản đó tồn tại sau đăng xuất.

Node được cài đặt vì hầu hết các hành động tái sử dụng là JavaScript và runner thực thi chúng trên máy chủ trong cấu hình này. Phát hiện điều đó ở bước checkout là một vòng lặp thường tốn mười phút.

2. Bộ nhớ không root trên 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}}"

Lệnh cuối đó nên trả lời overlay true. Một driver vfs thay vào đó có nghĩa là fuse-overlayfs bị thiếu, và vfs sao chép mọi layer đầy đủ trong mỗi lần build, điều này biến một pipeline chín mươi giây thành một cái sáu phút.

3. Một registry riêng tư

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

Gắn registry vào loopback và để nginx sở hữu mọi thứ hướng ra ngoài, bao gồm cả xác thực. Trong /etc/docker/registry/config.yml:

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

Registry tự nó không mang thông tin đăng nhập vì nó không bao giờ nhận một yêu cầu chưa qua proxy. Một nơi để kiểm tra mật khẩu tốt hơn hai nơi có thể không khớp.

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 bỏ giới hạn tải lên. Để mặc định nginx và mọi layer trên một megabyte sẽ thất bại với 413 khoảng hai phần ba đường đẩy, đó là một buổi chiều đáng nhớ.

systemctl enable --now docker-registry nginx

4. Runner

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

Sửa file được sinh ra để các job chạy trên máy chủ thay vì bên trong container, vì Buildah đã cung cấp sự cô lập và việc lồng thêm không mang lại gì ngoài sự phức tạp:

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

Công suất hai trên tám lõi là chủ ý: các bản build phần lớn là tuần tự, và hai job đồng thời mỗi job bốn lõi hoàn thành sớm hơn bốn job tranh nhau cùng cache. Đăng ký với mã nguồn của bạn bằng token mà nó sinh ra:

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

Sau đó một đơn vị, chạy dưới tài khoản không đặc quyền:

[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

Thay uid thực của tài khoản runner trong XDG_RUNTIME_DIR; id -u runner in nó ra. Podman không root cần thư mục đó tồn tại, đó là những gì cài đặt linger ở bước một đảm bảo.

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

5. Một workflow xây dựng và đẩy

Trong kho lưu trữ, tại .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 bật cache layer, đó là sự khác biệt giữa việc xây lại dependencies của bạn trên mỗi commit và xây lại chúng khi chúng thay đổi.

Xác minh nó

Runner nên xuất hiện là online trong danh sách runner của mã nguồn trong vòng vài giây sau khi đơn vị bắt đầu. Sau đó đẩy một commit và xem job từ máy:

journalctl -fu act-runner

Khi nó hoàn thành, xác nhận image thực sự đến chứ không chỉ báo cáo thành công:

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

Bạn muốn digest, danh sách layer và cả hai tag. Bây giờ chứng minh nó chạy, trên một máy khác nếu bạn có trong tay:

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

Cuối cùng, khẳng định mà toàn bộ build này dựa vào. Trong khi một job đang chạy, hãy xem ai sở hữu các process:

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

Mọi process thuộc về runner, và kiểm tra bảo mật trả lời true. Không gì trong pipeline giữ quyền root, điều đó có nghĩa là một script build bị xâm phạm nhận một tài khoản không đặc quyền và một namespace, không phải khóa registry và hypervisor của bạn.

Sau đó

Bộ nhớ registry phát triển không giới hạn trừ khi có gì đó xóa các tag cũ, vì vậy hãy chạy registry garbage-collect trên một bộ hẹn giờ hàng tuần một khi bạn có một quy tắc giữ lại mà bạn tin. Nếu các bản build trở thành nút thắt thay vì các bài kiểm tra, trang so sánh cho thấy kích thước tiếp theo trông như thế nào; thêm lõi giúp ít hơn nhiều so với những gì hầu hết mọi người mong đợi, và lõi nhanh hơn giúp nhiều hơn nhiều.

Sẵn sàng khi bạn cần

Chọn thành phố. Chọn kích thước. Thanh toán bằng coin.

Không có biểu mẫu về bạn là ai, không chờ đợi con người phê duyệt, không gọi điện thoại để xác minh bất cứ điều gì. Hóa đơn được thanh toán và thông tin đăng nhập sẽ vào hộp thư của bạn.