別々の場所にあった開発インフラの情報を、1つのダッシュボードに集約した

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

別々の場所にあった開発インフラの情報を、1つのダッシュボードに集約した

別々の場所にあった開発インフラの情報を、1つのダッシュボードに集約した

結論

会社の開発インフラの状態は、本来ばらばらの場所にありました。継続的インテグレーションのランナー群、コードを流すたびに走るセキュリティスキャン、脆弱性の台帳、ログとメトリクスの監視、どのノードが落ちても止まらない24時間の高可用性、GPUノードで動くローカルのLLM——見る場所も見方も、それぞれ別々でした。これらの状態を、1つのダッシュボードに集約しました。

実際の Infra Portal(概要タブ)。左上のバナーが監視中の全サービスの死活を、下のカードがランナーの稼働(12/12)・busy/idle・ビルドの待ち行列を、赤い「CI」行が「いま台数不足なのか routing 不整合なのか」の判定を返します。本来ばらばらの場所にあった状態を、1つの画面に集約しています。(実画面をローカルで起動して撮影・数値はサンプルデータ)

集約先が Infra Portal というダッシュボードです。ただし、この記事でいちばん伝えたいのは画面そのものではありません。開発インフラで重要なのは「どこで実行するか」ではなく、「判断に使う情報を選び、1つのダッシュボードへ集め、その数字を実機で裏づける」仕組みだ、という考え方です。

先に要点を置きます。

  • 集約した先は、実際のプラットフォームだ — Infra Portal が映すのは表示だけで、その裏では自前のCI/CD・セキュリティスキャン・監視・高可用性・ローカルLLM・コード化されたインフラが動いています。
  • 画面が返すのは生の数値ではなく「判定」 — 「ランナーが12台中12台」ではなく「いま台数が足りない(capacity)のか、割り当てがずれている(routing)のか」を返します。人が数値を解釈する手間を、画面が肩代わりします。
  • 合格ラインは設計書ではなく実機試験 — 高可用性を「達成した」と言えるのは、実際に1ノードを落として全部が生き残ったときだけです。設計レビューやユニットテストが全部緑でも、実機で落として初めて分かるバグがありました。
  • 監視も安全のしくみも、事故のたびに後付けで増えた — 最初から完璧に設計したのではありません。障害が起きるたびに「そのとき見ていなかったもの」を一つずつ足してきた結果が、いまの基盤です。この記事では、その後付けを失敗の日付ごとにたどります。

読了は約20分です。長いので、気になる基盤(CI/セキュリティ/監視/高可用性/ローカルLLM)から読んでも構いません。

本文(読了 約20分)

ダッシュボードの裏で動いているもの

この基盤は「GitHub Actions を自宅サーバに移してコストを削った話」でも「散らばった情報を集めただけのダッシュボード」でもありません。1つのリポジトリ(infra/ops のモノレポ)に、次のものが同居しています。

  • 自前のCI/CDランナー群 — GitHub Actions を自分のサーバで実行する
  • セキュリティスキャン基盤と脆弱性台帳 — コードを流すたびに脆弱性を検査し、結果を台帳に集約する
  • 完全に自前で持つ監視 — メトリクス・ログ・トレースを、外部のSaaSに一切預けずに自分で持つ
  • 24時間365日の高可用性 — どの1ノードが落ちても、残りで全サービスが生き残る
  • ローカルのLLM — GPUノードで動くモデルを監視し、用途別に振り分け、実際の開発に使う
  • コード化されたインフラ — 2台目以降のサーバを、1コマンドでまるごと再現する

規模を数字で言うと、このリポジトリは設計判断の記録(ADR)が62本、コミットが約930、対象ノードはCIサーバ2台・GPUノード1台・Mac数台の合計7ホストです。設計判断を62回も文書に残しているのは、「なぜこうしたか」を未来の自分(と、いつか引き継ぐ誰か)に残さないと、半年で自分の作ったものが読めなくなるからです。

実際の Infra Portal(サーバタブ)。CIサーバ2台(ci-runner-1/2)・GPUノード1台(gpu-node-1)・Mac数台の稼働を1枚で見ます。各カードはCPU負荷・メモリ・ディスク使用率を色付きバーで示し、gpu-node-1 は GPU使用率91%・LLM常駐2つ・直近85 tok/s まで映します。(実画面をローカルで起動して撮影・ホスト名と数値はサンプルデータ)

Infra Portal は、この全体を1つに集約したダッシュボードです。10個のタブで26個のプロダクトを集約し、社内向けのサブドメイン(Cloudflare Access の内側・社外からは到達できません)で開きます。フレームワークもビルド工程も持たない、手書きの静的なページです。フロント側のスクリプトが約3,900行、APIサーバが約1,800行。変更はビルドではなくファイルのコピーで配ります。全体を1つの画面で把握し続けるには、これくらい単純なほうが壊れません。

実際の Infra Portal(Products タブ)。プロダクトごとに Deploy・環境・管理画面・Backup の状況を横断表示します。左のレールでプロダクトを選ぶと、右に capability(Sentry/Asana/env-sync などの導入状況)と surface×env(Web/Admin/Mobile × dev/staging/production)のマトリクスが出ます。(実画面をローカルで起動して撮影・製品名は公開済みのもの・数値はサンプルデータ)

なぜ作ったのか — 判断材料を1つに集め、CIサーバに相乗りさせた

私が経営する会社は「全業務をAIで動かす」ことを掲げています。人間は判断に専念し、実務はAIへ移す。そのためには、判断に使う材料が、最短で目の前に揃っていることが要ります。あちこちのダッシュボードにSSHでログインして数値をかき集めていたら、判断より作業に時間を取られます。だから、開発インフラの状態を1つのダッシュボードに集め、生の数値ではなく「いま何がボトルネックか」まで返す画面が要りました。集約先を新しく建てず、すでにCIが動いているローカルサーバに相乗りさせたのは、そのほうが効率的だったからです。

きっかけは地味なコスト削減でした。GitHub のホスト型ランナーの課金を減らしたくて、自宅サーバに自前のランナーを立てた。ところが、これが次から次へと運用の抜けを露呈させていきます。ランナーが暴走してサーバを食い尽くす。セキュリティのゲートが実は一度も動いていなかった。監視していたつもりの箇所が、壊れて初めて「見ていなかった」と分かる。1台が落ちるとサービスが止まる。——この記事の後半は、その抜けを一つずつ直していった記録です。コスト削減それ自体を否定はしません。むしろ、そこから運用の本質的な抜けが生まれ、それを直すうちに基盤へ育った、という経緯です。

切り分け — SSHもLAN限定のGrafanaも使わずに、原因を見分ける

Infra Portal のいちばん大事な設計は、パネルが生の数値ではなく「判定」を返すことです。

たとえばCIのビルドが詰まったとき。普通のダッシュボードは「待ち3件、稼働ランナー0台」と数値を並べて、あとは人が解釈します。Portal は違います。待ち行列(queue)と空き(idle)の2つの軸だけを見て、純粋な関数で次のように判定します。

  • 待ちがあり、空きが0 → capacity(台数が足りない。ランナーを増やせ)
  • 待ちがあり、空きはある → routing(実行できる台があるのに流れていない。ラベルの不整合を疑え)
  • 待ちがあり、稼働台数が0 → offline(実行機がいない)
  • 待ちが0 → healthy(健全)

そして画面のいちばん上に、一言「いまは台数がボトルネックです」と出します。この判定ロジックは16件のテストで回帰を固めてあり、ラベルと待ち行列の組み合わせを変えても崩れないことを確認しています。Grafana は社内LAN限定でしか見られませんが、外出先からでも、このダッシュボードだけで「ランナーを増やすべきか、設定を直すべきか」を切り分けられます。

Portal の設定タブには、もう一つ徹底したことがあります。秘密情報を一切表示しない。連携先のトークンやURLは「存在するか」「権限があるか」の診断だけを出し、値そのものは表示も入力もできません。見るのが自分だけでも画面に秘密を出さないのは、画面共有やスクリーンショットの事故を構造的に防ぐためです。

実際の Infra Portal(Runner/ビルドタブ)。CIの詰まりの原因が「台数(容量)不足」なのか「test/queue(routing)側」なのかを、label(プール)ごとに判定します。runner-docker は空き0で待ち2=台数不足、runner-light は空きがあるのに待ち1=routing 不整合、と別々の対処を指し示します。生の数値ではなく判定を返す実例です。(実画面をローカルで起動して撮影・数値はサンプルデータ)

基盤① CI/CDランナー — 無料化で開いた抜けを、失敗のたびに直す

自前のランナーは、安全のための設定の一つひとつが、過去に起きた事故への対処として付いています。

最初の事故は暴走でした。CPUの上限を設けていなかった頃、軽量ジョブ用のランナー1本が、8コアを独り占めして830%のCPU使用率まで膨れ上がったことがあります。1つのコンテナが暴走しただけで、サーバ全体が巻き込まれる。これを機に、コンテナごとにCPUとメモリの上限をカーネルレベルで強制するようにしました。上限を超えたら、そのコンテナだけが絞られ、隣は無事です。

いまのランナーは6つの役割別プール(軽量・セキュリティ・ブラウザ・Docker・コスト・バックアップ)に分かれています。Dockerのソケットを渡すのは Docker 用プールだけ。全ランナーは1ジョブごとに使い捨て(ephemeral)で、tmpfs と権限昇格の禁止で隔離します。16コアのCIサーバ上で標準13コンテナが動き、この領域だけで8本の設計判断を積み重ねてきました。

ランナーのイメージも自前でビルドしています。あるとき、GitHub のコードダウンロード基盤が障害を起こし、Actionsのチェックアウトが一斉に失敗したことがありました。そこで、よく使う依存(実行環境・パッケージ管理・クラウドCLI・ブラウザ自動化・IaCツール)をあらかじめイメージに含め、キャッシュも自前で持つようにしました。外部が落ちてもビルドが続くように、キャッシュ層は「取れなければ素通し(fail-open)」、逆にセキュリティに関わる依存ミラーは「取れなければ止める(fail-closed)」と、性質で振る舞いを変えています。

新しいリポジトリへのCI導入は、Portal からのセルフサービスにしました。ランナーの台数やCPU上限を画面で編集すると、構成管理(Ansible)のインベントリへの変更が自動でプルリクエストになります。手でサーバをいじるのではなく、必ずコードの変更として履歴に残る。運用する人数が少ないほど、この「後から履歴を辿れる」性質が効いてきます。

基盤② セキュリティ — 100%失敗し続けていたゲートの話

ここは、いちばん正直に書くべき失敗です。

ある時期まで、プルリクエストにはセキュリティチェックのステップが付いていて、緑のチェックが並んでいました。私はそれで守られているつもりでいました。ところが実態は、そのステップが存在しないサブコマンドを叩いていたのです。コマンドが存在しないので、コードの中身に関係なく、毎回エラーで失敗する。ドキュメントだけの変更でも赤くなる。しかも、導入済みの29リポジトリすべてで、同じ壊れた行が動き続けていました。

つまり「緑=安全」ではなく、ゲートが一度も成立していなかった。赤いのが当たり前になり、人は赤を手で無視してマージするようになっていました。これはゲートとして最悪の状態です。存在しないより悪い。「守られている」という誤解を与えるからです。

この失敗を設計判断として記録し、壊れたゲートを削除して、代わりに意味のあるゲートへ作り直しました。いまは3つのレーンで動いています。プルリクエスト用の「新規混入だけを止める」ゲート、mainへのpush時の再取り込み、そして日次の定期スキャンです。オープンソースのスキャナ4種(静的解析・秘密情報検出・脆弱性スキャン・依存監査)を自前のランナーで走らせ、SaaSの課金はゼロにしています。

プルリクエスト用のゲートには工夫があります。既存のリポジトリには、過去から積み上がった指摘(backlog)が必ずあります。それを全部ブロックしたら、誰もマージできません。そこで、プルリクエストの変更前後の差分を取り、その変更で新しく混入した分だけを失敗にし、既存の積み残しは非ブロックで表示するようにしました。新しい不備は止める、古い不備は見える化する、の両立です。

結果は脆弱性の台帳(DefectDojo)に集約します。この台帳は社内LAN限定・インターネットからは到達できません。ISO 27001 や SOC 2 を見据えた監査証跡のセットとして持ち、バックアップは日次のダンプ→社外ストレージ→待機系DBへの日次リストアの三重にしています。

基盤③ 監視 — 見えていなかった箇所は、壊れて初めて気づいた

監視スタックは、Grafana・Prometheus・Loki・Tempo・Alloy・各種エクスポーター・アラート管理を1つの定義でまとめて起動し、全イメージのバージョンを固定しています。加えて、リクエストIDやトレースIDで横断検索するための全文検索スタックを別に持っています。アラートのルールは24個・8グループ、ダッシュボードは5枚です。

ただ、この構成は最初から設計されたものではありません。「何を監視すべきか」は、事前には分からない。この基盤のアラートは、実際の事故のたびに「そのとき見ていなかったメトリクス」を後付けで足していった痕跡になっています。監視の博物館のようなものです。

いちばん痛かったのは、2026年8月7日の出来事です。ディスク同期複製(DRBD)の分断が18時間、誰にも気づかれず、その間にアクティブ側の役割が4回も行き来していました。標準のエクスポーターは、この同期状態を一切見ていなかったのです。そこで、DRBDの状態を自前のエクスポーターでメトリクス化しました。

監視スタックの盲点を埋める自作エクスポーターが5種類あります(ディスク同期・外形監視・フェンシング・バックアップ・ランナー枯渇)。標準のエクスポーターが見ていない箇所を埋めるためのものです(GPU/モデルサーバ用の自作エクスポーターは後述の別枠です)。そしてこれらには、一つ大事な設計を入れています。収集に失敗したら、古い値を残さずにメトリクスを出すのをやめる(fail-loud)。さらに、各エクスポーターには対になる「エクスポーター自身が古くなっていないか」のアラートを付けています。監視の死を監視する。「見ているつもりで、実は監視自体が止まっていた」を防ぐ、監視の監視です。

CIサーバは2台あり、両方のPrometheusが互いに独立して収集します。片方が落ちても、もう片方が監視を続けます。監視こそ、単一障害点にしてはいけないからです。

基盤④ 24時間止まらない仕組み — テストが全部緑でも、まだ達成していない

どのノードが落ちても止まらない仕組みは、この基盤でいちばん判断が難しい領域でした。

原則を先に書きます。合格の判定は、設計書やユニットテストではなく、実機で1ノードを落とす試験(kill-test)だ。任意の1ノードを停止して、全部のエンドポイントが残ったノードで応答し、データが欠けず、二重書き込み(split-brain)が起きず、ちゃんと復帰する。これが実機で緑になって初めて「高可用性を達成した」と呼びます。

この原則を、私は手ひどい失敗から学びました。発端は、26時間続いた split-brain 事故です。2台のノードが両方ともアクティブになり(しかも両方ともヘルスチェックは200を返していた)、2つのワーカーが同じファイルのキューを奪い合い、書き込みが途中で切り詰められて、ジョブのデータが壊れました。ヘルスチェックが緑でも、内部ではデータが壊れている。表面の200は、健全の証明にはなりません。

対策として、アクティブ側の唯一の裏付けを「単一のリース(lease)」に一本化しました。リースを持つ者だけがアクティブになれる。二重のアクティブを、設計として原理的に起こせなくする方式です。ディスクは同期複製で欠損ゼロを狙い、GPUノードを第三の投票者にして2/3の多数決で分断を判定します。エッジ側もトンネルを二重にして自動で切り替えます。

そして実機の kill-test です。2026年7月12日の試験では、アクティブ側のCIサーバをコンテナごと強制停止したところ、18秒で待機側が人手ゼロで自動的にアクティブに切り替わりました。停止と稼働確認の時刻はUTCの実タイムスタンプで証拠として残してあります。同じ試験でネットワーク分断からの自己フェンシングと引き継ぎ、データ差分の一致、切り戻しまで確認しました。

ここで正直に注意書きを一つ。この18秒は、当時(2026年7月12日)のNFSを使った構成での実測値です。その後2026年8月22日にリース基盤をDynamoDBへ移行したため、旧構成の16〜18秒という値は現在の構成にそのまま当てはまりません。数字を「当時の構成の記録」として残し、現在値として断定しないのは、この基盤で守っている作法です。

もう一つ正直に。設計とユニットテストでは検出できず、実機の kill-test でだけ現れた3つのバグがありました。猶予期間中のライブロック、ハードマウントのI/Oが無限にブロックして自己フェンシングが不発になる問題、リースを盲目的に奪って世代番号が巻き戻る問題。どれも机上では緑でした。実機で落として初めて出た。だから「実機で落とすまで、達成とは言わない」のです。

そして、いまも残る限界も書きます。2026年9月3日の再起動を伴う kill-test で、自動復帰が失敗することを実測しました。起動時の競合や、パケットロス下での「読み取りは成功するのに取得は失敗する」非対称性が原因です。これは設計判断として正直に記録し、まだ直しきれていない弱点として台帳に載せています。高可用性は「達成した/していない」の二値ではなく、弱点を証拠付きで数え、段階的に潰していく作業です。この領域だけで、実機挙動の自動テストは196ケースあります。

実機のkill-testの流れ。稼働中の1台を実際に落とし、残った台で処理を続けられるかを確認します。2026年7月12日のNFS構成では、分断の修正後に自動切替を18秒で実測しました。(画面は再構成・匿名。数値は当時の実測。現在のDynamoDB構成には旧値は適用しません)

基盤⑤ ローカルLLM — 監視される側であり、働く側でもあるGPU

GPUノードでは、ローカルのLLMを動かしています。ここには2つの顔があります。監視される側であると同時に、実際に働く側でもある。

監視のためのエクスポーターは自前で2本書きました。GPU用が約100行、モデルサーバ用が約230行。どちらもPythonの標準ライブラリだけで、追加のインストールが要りません。少人数で運用する基盤では、依存を増やさないこと自体が可用性です。

きれいな構成ではない部分にも、そのまま向き合っています。このGPUは統合メモリの構成で、標準のGPU監視コマンドがモデルのメモリ使用量を返してくれません。そこで、モデルサーバのAPIが返す値を正としました。モデルサーバにはメトリクス出力の口が無いので、ログに出る処理時間の行を正規表現で拾って、毎秒のトークン生成数を実測しています。きれいなAPIが無いなら、あるものから測る。

用途別の振り分けもしています。分類のような軽い用途は片方のエンドポイント、コードを書くような重い用途はGPUノード、と分けています。分類器の側も高可用性の kill-test を通していて、片方のMacを落とすともう片方へ透過的に切り替わることを実機で確認しました。

そして、いちばん象徴的なのはここです。このGPUで動く大きなモデルが、実際にこのインフラのリポジトリへコミットを書いています。自動化の実行バックエンドとして、AIが自分のインフラを保守する。ログには、AIが書いたと分かる prefix のコミットが5件残っています。「全業務をAIで動かす」という会社の考え方が、監視画面の光る一枚のカードと、コミット履歴の両方に、同時に現れている場所です。

全部をコードから再現し、人が代わっても続けられる

少人数で運用するものは、その人が倒れたら止まります。だから「コードから再現できる」ことを徹底しました。再現の徹底度は2段階あります。

1段階目は、2台目以降のサーバ構築です。構成管理の1コマンドで、全サービスが立ち上がります。構成の役割(Ansible が24、playbook が17、Compose のサービス層が14、Terraform のスタックが11)は、25プロダクトを一覧化した1つの正本ファイルから派生します。Portal に出るカタログも、その正本の派生物です。正本が一つだから、食い違いが起きません。

2段階目は、もっと踏み込んでいます。この基盤をまるごと、社内固有の値を残さず安全に書き出せるようにする「クリーンルーム書き出し」です。ノードが増えても(ci3、ci4…)、あるいは誰かが引き継ぐときも、同じ手順で安全に配れます。履歴をゼロにしたコピーを作り、許可リストで必要なものだけ残し、社内固有の値(ドメイン・実IP・実名・同僚名・タスク管理のID)を機械的に置換し、最後に秘密情報スキャンを通す。このスキャンで1件でも引っかかれば、1バイトも書き出さない(fail-closed)。二重のゲートを通らなければ外に出ない、一方向のパイプラインです。前の章のセキュリティの教訓と同じ思想が、ここにも通っています。

実機で数字を裏づける — 記録として残すkill-testと、コストのハードキャップ

この記事の芯にもう一度戻ります。数字は、実機で裏づけて初めて数字になる

高可用性の kill-test は、そのまま記録になります。いつ、どのノードを、どう落として、何秒で切り替わったか。UTCのタイムスタンプ付きで残す。「たぶん大丈夫」ではなく「この日、この構成で、この秒数だった」と言えるようにする。

コストも同じで、実測と構造で抑えます。ログ基盤はAWS上に全量を保存しますが、検索基盤には1クエリあたり10GB(約107億バイト)のスキャン上限をかけ、これは利用者側で解除できないように強制しています。課金は1TBあたりの従量なので、1クエリはどれだけ大きくても約0.05ドルに収束します。「貯める」「探す」「送る」を別々の仕組みとして設計し、貯めるのはS3で長期、探すのはローカルの検索基盤で短期、送るのは重要度で振り分ける、と分けています。

ここでも正直に。方針では「重要なログだけを転送する」と書いてありますが、稼働中の転送設定は、実際にはコンテナのログとシステムログを無フィルタで全量送っています。方針と現行実装にギャップがある。数字と設計を実機で突き合わせると、こういう食い違いが必ず出てきます。それを見つけて直すために、実測するのです。

情報の種類と確かめ方を更新する — 確証がなければ何もしないスクリプト

最後は、人が張り付いていなくても事故を起こさない運用の話です。

自動で走るスクリプトには、共通する原則があります。確証が持てなければ何もしないこと。破壊的な操作は既定で空実行(dry-run)にし、二段階の猶予を置き、実行には大文字の「YES」の入力を要求します。

たとえばディスクの掃除。2026年7月2日に、CIサーバのルートファイルシステムが97%まで埋まり(稼働中ランナーの書き込み層の合計が60ギガバイトを超えていました)、そこからディスクの番人スクリプトが生まれました。このスクリプトは、閾値を超えたときだけ掃除をし、実行中のジョブを持つランナーは絶対に保護します。掃除したいがために、動いているジョブを消したら本末転倒だからです。

ランナーの回収役は3つに分かれ、責務を厳密に棲み分けています。1つ目はディスクの番人で、稼働中を守る役。2つ目は、オフラインになった使い捨ての幽霊だけを消す役。3つ目は、動いているように見えて実は固まっているデッドロックを回収する役。同じ「壊れたランナー」でも、状態によって担当が違う。1つのスクリプトに全部をやらせると、判断を誤ったときの巻き添えが大きくなるので、あえて分けています。

高可用性の切り替え判定も同じ考え方です。「アクティブがどちらか」を判定できないときは、両方を停止側に倒す。無理に動かして二重書き込みになるより、止まって人を呼ぶほうが安全だからです。fail-safe——迷ったら安全側へ倒す、を徹底しています。


インフラを少人数で持つ、と言うと属人化の極みに聞こえるかもしれません。でも実際にやっているのは逆で、判断材料を1つのダッシュボードに集めて判定まで出し、合格は実機で裏づけ、全部をコードから再現できるようにし、スクリプトは迷ったら止まる——そうやって「壊れにくさ」を一つずつ足してきた結果です。その一つひとつには、暴走した日、ゲートが動いていなかった日、18時間気づかなかった日、26時間データが壊れていた日、ディスクが97%になった日、再起動で復帰しなかった日——具体的な失敗の日付が対応しています。

インフラは、設計が美しいから安全になるのではありません。落として、壊して、気づかなかった抜けを数え、証拠付きで一つずつ直したぶんだけ、安全になります。1つのダッシュボードは、その作業を続けるための土台です。

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。


EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。

EarthLink Network が自社でつくっている18のプロダクト

会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。

別々の場所にあった開発インフラの情報を、1つのダッシュボードに集約した