0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Woodpecker CI のススメ — SaaS クレジット枯渇から自前ホストへ

0
Last updated at Posted at 2026-07-24

ある朝、PR を出しても Checks が緑にならない——正確には、そもそも走り始めない。GitHub Actions や CircleCI のクレジットが尽きたのだ。INTERESTIC の公式サイトや Web Tool Lab を育てるなかで何度か味わったこの朝が、自前ホストの Woodpecker CI に移るきっかけになった。いまも手元のマシンで lint と test を回せている。その経緯と、いまの設計を書く。

きっかけは「クレジットが尽きた」朝

SaaS CI のクレジット残量が尽き、チェックが止まった様子の挿絵

個人開発や小規模チームだと、SaaS の CI は最初こそ快適だ。リポジトリを繋ぎ、YAML を置き、PR を出せば lint と test が走る。マージゲートにもしやすい。問題は、無料枠や月次クレジットが想定より早く減ることだった。

INTERESTIC のコードベースは、Laravel と React / Vite、複数の Web ツール、Worker まで含む。PR のたびに composer と npm、静的解析、ビルド、テストが積み重なる。依存の解決も時間を食う。クレジットは「あるうちは気にしない」が、「切れた瞬間に全部止まる」タイプの痛みだった。障害というより、静かに手を止める。

SaaS CI の便利さと、上限の現実

GitHub Actions も CircleCI も、プロダクトとしては優秀だ。ホスト管理をしないで済む、エコシステムが厚い、ドキュメントも多い。チームが大きく課金を前提にできるなら、いまも第一候補でよいと思う。クラウド側がランナーを用意してくれる安心感は、本当に大きい。

一方で個人・少数運営だと、上限は設計そのものになる。クレジットは「今月あと何回フルパイプラインを回せるか」の枠であり、それを超えると復旧まで待つしかない。無料枠の復活はカレンダーに縛られることが多い。開発のリズムは、そう都合よく合わせられない。

便利さの裏側に、時間軸を他人の課金モデルに預けるという依存がある——枯渇の朝に、はっきり見えた。上限があること自体は悪い話ではない。ただ、その上限が自分の作業量より先に来るなら、設計を見直す合図になる。

INTERESTIC が当時感じていた差を、定性で並べると次のとおりだ(数値ベンチは意図的に置かない)。

観点 SaaS CI(Actions / CircleCI など) 自前 Woodpecker
クレジット上限 無料枠・月次枠に縛られる ホスト資源(CPU / ディスク / 稼働時間)が上限
ホスト運用 ほぼ不要 Docker・Tunnel・スリープ対策が自分の仕事
PR 以外の実行 設定次第で push ごとにも走りやすい .woodpecker/ci.yamlpull_request のみに絞る
デプロイ責務 CI に載せやすい 本番はローカルの make deploy。CI は lint / test ゲートに限定
Checks の見え方 クラウド側が返す 手元エージェントの結果を GitHub Checks に返す

復活待ちのあいだ、PR は止まる

クレジットが戻るまで、PR は「レビューはできるが、マージの根拠がない」状態になる。ローカルで同じコマンドを叩けば代替はできる。だがローカル確認は、意図せず環境差を許しやすい。CI をマージゲートにしているほど、止まっている時間の損失は大きい。

「次の機能」より「CI が戻るのを待つ」がタスクになる日があった。レビューへの返信も、Checks が動かないあいだは半端に感じる。SaaS を捨てたくて捨てたのではなく、止まらない経路がほしかった。

次の図は、クレジット枯渇から自前ホストへ移るまでの流れだ。

なぜ自前ホストの CI を選んだか

選択肢はいくつかあった。SaaS の有料プランを上げる、ジョブを極端に削る、ホスト型に移す。INTERESTIC では、手元のマシンで CI を回す方向を選んだ。理由は単純で、クレジット上限という外部制約を外したかったからだ。

もちろん「無料で無限」ではない。マシンの電気とディスク、Docker のメンテ、webhook 用の公開 URL、スリープ中は止まる——そういった運用コストとの交換だ。個人開発のスケールでは、月次の残クレジットを気にするより、ホストが起きているかを気にするほうが予測しやすかった。

候補として見たのが Woodpecker CI だ。Docker ベースでパイプラインを定義でき、GitHub と連携して Checks に結果を返せる。構成をホスト側とアプリ側で分けやすい点も、再現性の面で魅力だった。

Woodpecker CI とは何か

Woodpecker CI は、自前でサーバとエージェントを動かせる CI システムだ。パイプラインはリポジトリ内の YAML で定義し、各ステップはコンテナ上でコマンドを実行する。感覚は他の CI と近く、学習コストを抑えつつホストを自分の側に置ける。

INTERESTIC では、ホスト構成をアプリ本体とは別の置き場にまとめている。Docker Compose で server / agent を起動し、GitHub からの webhook 用には Cloudflare Tunnel を使う。日常の起動は make up-tunnel だ。マシンを替えても、同じ手順で戻せる——そこが SaaS 以外で欲しかった「再現できる運用」だった。

役割の置き場は、ざっくり次の二層だ。

置き場 役割
ホスト 手元の Compose / Tunnel 構成 server / agent、公開 URL、CI イメージビルド、クリーンアップ
アプリ プロダクト本体 .woodpecker/ci.yaml と lint / test スクリプト

公式ドキュメントと、自前の compose・Tunnel 手順を両方持っておくと、トラブル時の切り分けが楽になる。UI でリポジトリを Activate し、アプリ側に .woodpecker/ を置く流れ自体は素直だ。秘密情報はテンプレートと生成手順に寄せ、リポジトリ本文には載せない。

INTERESTIC での移行と運用の形

Woodpecker のパイプラインが PR イベントだけ走る様子の挿絵

アプリ側の設定は .woodpecker/ci.yaml だ。ポイントは、PR イベントのときだけ実行すること。feature ブランチへの単なる push では走らせない。SaaS 時代に痛感した「無駄な実行=クレジット相当の消費」を、自前でも抑えるための設計だ。ホスト資源にも限りがある以上、走らせるタイミングは意図して絞る。

# .woodpecker/ci.yaml(抜粋・イメージ名は伏せる)
when:
  - event: pull_request

steps:
  - name: lint
    image: <ci-image>
    commands:
      - bash scripts/woodpecker-lint.sh

  - name: test
    image: <ci-image>
    commands:
      - bash scripts/woodpecker-test.sh

実行イメージは、用途に絞った自前の軽量イメージに置き換えた。SaaS 時代の汎用イメージより依存の土台が軽いと、待ち時間もホスト負荷も下がる。イメージ自体はホスト側でビルドする。

本番デプロイは CI に含めない。本番反映はローカルからの make deploy だ。GitHub Actions は labeler など補助用途に残し、lint / test の本体ゲートは Woodpecker に寄せている。役割を分けると、「何がマージを止めているか」が分かりやすい。

最初に Checks が手元のエージェントから緑に戻ったとき、安心感は単純だった。課金枠の残量ではなく、ホストが起きているかどうか——制約の置き場所が、自分の側に移った。失敗したときも、ログは同じマシン上にある。原因を追う導線が手元にあるだけで、待ちの質が変わる。

いま回しているジョブの中身

パイプラインは 2 ステップだけだ。YAML は薄い入口にし、実体はシェルに寄せている。

ステップ やること
lint 依存監査・静的解析・フロントの lint / typecheck
test 変更スコープに応じて build / Vitest / PHPUnit(または skip)

lint の入口は、だいたい次のようなイメージだ(抜粋・サンプル)。

#!/usr/bin/env bash
# scripts/woodpecker-lint.sh(抜粋)
set -euo pipefail

# 後段 test 用に full / light / skip を決める
bash scripts/ci-resolve-test-scope.sh

composer install --prefer-dist --no-interaction
composer validate --strict --no-check-publish
composer audit # severity の低いものは無視

# PHP
find app config database routes scripts tests -name '*.php' -print0 \
  | xargs -0 -n1 php -l
vendor/bin/pint --test
vendor/bin/phpstan analyse --memory-limit=512M

# Front
npm ci
npm audit --audit-level=high
npm run lint
npm run typecheck

依存の脆弱性ゲートも、ここでの composer audit / npm audit に載せている。別サービスに切り出すより、同じ PR の失敗理由として見えるほうが運用しやすい、という判断だ。PHP 側とフロント側を一つの lint ステップにまとめると、失敗の入口が散らばりにくい。

test 側は、lint が書き出したスコープを読んで分岐する(抜粋・サンプル)。

#!/usr/bin/env bash
# scripts/woodpecker-test.sh(抜粋)
set -euo pipefail

source ci-scope.env
echo "scope: mode=${CI_TEST_MODE}"

if [ "$CI_TEST_MODE" = "skip" ]; then
  echo "Skipping tests (docs/ci-config only)"
  exit 0
fi

if [ "$CI_TEST_MODE" = "light" ]; then
  npm run build
  npx vitest run --config vite.config.js # 必要ならパスを絞る
  exit 0
fi

# full: migrate + Vitest + PHPUnit(SQLite)
npm run build
php artisan migrate --force
npx vitest run --config vite.config.js
php artisan test

全部を毎回フルにするのは、自前ホストでも無駄だ。次節のスコープ解決が、ここで効いてくる。

テストを「全部フル」にしない工夫

スコープ解決は、移行後いちばん効いている工夫のひとつだ。変更ファイルから full / light / skip を決めるスクリプト(例: scripts/ci-resolve-test-scope.sh)の概要は次のとおりである。

モード ざっくりの条件 test でやること
full アプリ本体・設定・依存ロックなど広い変更 npm run build、migrate、Vitest、PHPUnit(SQLite)
light ツール単体フロントなど狭い変更 ビルドとスコープ付き Vitest。PHPUnit は走らない
skip docs / CI 設定だけの差分など test ステップを実質スキップ

次の図は、PR から lint・スコープ判定・test までの分岐だ。

ツール単体のフロント変更なら light で足りることが多く、アプリ本体や設定の広い変更では full に寄せる。判断をスクリプトに閉じることで、「今回は短縮でいいか」の属人判断を減らせる。PR のたびに人間がモードを選ぶ運用は、続かない。

一方で、まだ整えたい穴もある。たとえば .woodpecker/* だけの変更はいま skip になり得る。CI 定義そのものの変更は、軽く見えて影響が大きい。ここを force-full へ寄せるパスは、候補として残している。曖昧にすると、いつか「通ったつもり」が生まれる。

また現状の test は SQLite 前提だ。本番に近い MySQL での migrate 検証は、別途足す価値がある。速さのために軽くした部分と、本番相当で確かめたい部分は、意図して分けておく必要がある。「何を保証し、何をまだ保証していないか」を明示するほうが、ベンチマーク数値で語るより誠実だ。

自前ホストのトレードオフ

ノート PC のスリープと Tunnel・メンテのバランスを示す挿絵

公平に書くと、自前 CI は万能ではない。Mac がスリープしたり Docker が止まっているあいだは、webhook を受けてもジョブは進まない。外出中に PR を出しても、帰宅してホストを起こすまで Checks は動かない——これは SaaS にはない制約だ。常時起動のマシンを別に用意できるなら話は変わるが、個人開発ではノート PC 兼用になりやすい。

次の図は、ホストの起き/眠りと CI 可否のトレードオフだ。

ディスクも溜まる。エージェントはパイプラインごとにコンテナを起動するため、イメージとキャッシュの掃除は運用の一部になる。ホスト側ではクリーンアップ用の make ターゲットを用意している。置いただけでは、数週間後にホストが苦しむ。

ドキュメントの鮮度も負債になりうる。README や手順書がまだ CircleCI 表記のまま、といったズレは、移行直後ほど起きやすい。パイプラインが動いていても、オンボーディング文書が古いと、未来の自分が迷う。コードより文章のほうが、移行の取り残しとして残りやすい。

それでも INTERESTIC では、クレジット復活待ちよりこのトレードオフのほうが扱いきれた。止まらないことと、止め方を自分で決められることが、個人開発の速度に直結した。スリープで止まるなら、起こしてから出す。その判断を自分でできることが、当時ほしかった「止まらない経路」の実質だった。

これから足したいものと、読者への提案

現状で足りているのは「PR ごとの lint / test ゲート」だ。これから足すとよさそうな候補は、次のようなものだ。

候補 ねらい
OCR(Open Code Review)× ローカル LLM / Ollama 任意ジョブとしてのレビュー実験
MySQL での migrate 検証 いまの SQLite テストを補う
Storybook build の CI 化 UI / トークン変更の回帰をゲートに載せる
.woodpecker/* 変更時の force-full CI 定義変更が skip になる穴を塞ぐ
ドキュメントの CircleCI 表記整理 オンボーディングを Woodpecker 前提へ揃える

読者への提案は単純だ。SaaS CI のクレジット上限に何度も当たっている個人・小規模チームなら、Woodpecker のような自前ホストは現実的な選択肢になる。ただし「課金から逃げる魔法」ではなく、マシン・Tunnel・メンテとの交換だ。公式の Woodpecker CI を起点に、手元ホストとアプリ側の .woodpecker/ を分ける構成を想像すると、再現のイメージが掴みやすい。

クレジットが尽きて朝が止まる経験をした人ほど、「手元で回す」意味が具体的になる。INTERESTIC では、その選択のあとも CI は手元で動き続けている。完璧な未来図ではない。それでも、開発のリズムを自分の側に戻すには足りている。


正本・関連

この記事の正本(INTERESTIC ブログ):

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?