ブログ
Web 制作・システム開発に関する記事を公開しています。
障害検知のトリアージから対応まで AI エージェントに任せる設計手法例
監視アラートの一次判定から修正の PR まで AI エージェントに任せ、判定に使う道具を読み取り専用に絞った設計。13週間で起票14件、人の本番操作0件。
戻せない D1 マイグレーションは本番のダンプで先に流す
EmDash は merge した瞬間に本番の D1 スキーマを自動で書き換える。down が空で戻せない 2本を含む 20本のアップグレードを、本番のダンプに先に流して検証してから通した設計を記す。
サンドボックスに鍵を置かずに EmDash の CLI を使う
CMS への記事投入を内蔵 MCP 経由で行うと本文がモデルの入出力を通り、トークン消費も文字化けの危険も増えます。サンドボックスの環境変数にプレースホルダーを置き、ホストのプロキシが差し替える経路に切り替えた設計を実例で示します。
X 公式 MCP が Docker サンドボックスで認可できない
X 公式 MCP を Docker サンドボックスから直接使おうとすると認可サーバーの制約と噛み合わない。接続して分かったツールの中身と読み取りの単価を示し、認証をホスト側のブリッジに置く設計を記す。
AI エージェントに X の鍵を渡さず投稿させる権限設計
X 公式 MCP に投稿ツールが無い前提で、鍵を neta-bot だけに残したまま AI エージェントに X へ投稿させる権限設計を実例で示します。承認は会話に一本化し、投稿口は 1 つに閉じて台帳に必ず載せます。
AI エージェントの記憶を git だけに置き環境は捨てる「秘書戦略」
サンドボックスは作り直すと中身がすべて消えます。事業の判断・手順・議事録を責務ごとに git の文書へ分けて置き、規約の書き換えだけを PR 必須にする、消える環境を前提にした記憶の設計を実例で示します。
GitHub Actions の OIDC で AWS の鍵を手元から無くした
Claude Code の Docker Sandbox に AWS の認証情報を渡すスクリプトを 3 本書きましたが、docker sandbox exec が別のコンテナに書き込んでいて一度も機能していませんでした。渡すこと自体をやめ、Terraform の bootstrap 層(Actions が引き受ける IAM ロールと OIDC)まで GitHub Actions の workflow_dispatch で plan / apply する構成にしました。ロールが自分自身を書き換える権限の線引きと、信頼ポリシーのリポジトリ名の既定値が古いままで自分を締め出しかけた話も残します。
changesets を AI エージェントに書かせると変更履歴が読める物になる
8 サイトのモノレポの変更をほぼ AI エージェントの PR で回していると、git log では「あのサイトはいつ何が変わったか」を追えなくなりました。changesets を入れ、PR を作るその瞬間にエージェントへ変更記録を書かせ、Release PR の merge でサイトごとの CHANGELOG に集約する形にしました。誤った記録を次の記録が訂正して並ぶ実例と、別リポジトリで踏んだ罠(タグの系統が 2 つになる、changeset status は commit 済みしか数えない、Release PR の merge がデプロイになる)を書きます。
.github リポジトリの PR テンプレートに「ハマったこと」欄を置く理由
AI エージェントに gh pr create --body で PR を作らせると、リポジトリの PR テンプレートは使われません。適用されるはずだと思い込んでいて、しばらく気づきませんでした。組織の .github 公開リポジトリにテンプレートを 1 つ置いて全リポジトリの既定にし、エージェントには「テンプレートを先に読んで本文を組む」手順を渡しました。テンプレート末尾の「ハマったこと・試行錯誤」欄は、commit には残らない物語を作業直後に残す場所で、別の bot が毎朝拾って素材にします。拾う側の本文上限が 300 文字で、一番下にあるこの欄ごと落ちていた失敗も書きます。
自動投稿 bot の設計を「ネタ→投稿」から「背景→投稿」に変えた
LLM で X に自動投稿する bot にテキスト投稿を足そうとして、「ネタの粒度」で機能を分ける設計を始め、書く前に破綻しました。bot を通す理由は「投稿を書くこと」ではなく「そのままでは投稿にならない背景を変換すること」で、境界線を「変換が要るか」の 1 本にしました。毎朝 GitHub の活動と PR 本文の「ハマったこと」欄を集めて Slack に提示する push 型に反転させ、返信がどのネタに届くか分からなくなった混線を 1 ネタ 1 スレッドで、状態機械の使い回しで生まれた競合を条件付き更新で塞いだ記録です。
Slack bot が 3 秒で切られるので Lambda を受付と処理に分けた
Bolt for JavaScript と Lambda の Function URL で Slack bot を動かすと、レスポンスは全リスナーの完了後にしか返らず、Slack の 3 秒制限を毎回超えていました。当初は http_timeout の再送を無視する分岐で凌いでいましたが、初回の処理が失敗するとイベントが静かに消える構造でした。Lambda を「受付」と「処理」に分け、重い処理は自分自身を非同期 invoke して委譲する形に直しました。lazy リスナーは Bolt for Python にしか無い、という前提の取り違えから始まった記録です。
X API への自動投稿が二重になる経路を入口と出口で塞ぐ
Slack から X に 4 コマを自動投稿する bot に、同じ投稿が 2 回出る経路が 2 つありました。入口は Slack の再送(3 秒で切られ最大 3 回届く)とボタンの連打で、イベント ID の条件付き書き込みと status 遷移のロックで塞ぎました。出口は X API の応答が返らないまま再試行することで、失敗を「確実に失敗・結果不明・重複」に分け、結果不明は自動で再試行しないようにしました。実際に二重投稿は起きておらず、コードを読み直して先に塞いだ記録です。
Cloudflare D1 の無料枠を守るためだけに作らなかった 4 つの仕組み
D1 の読み取りを 1 日 2,110 万行(無料枠の 422%)から 190 万行前後まで下げる過程で、「次はこれを作ればさらに減る」という案が 4 つ出ました。公開時にキャッシュ済みの 404 を消すミドルウェア、404 ページを D1 を読まない静的ページにする案、リダイレクト表を Bulk Redirects へ同期する仕組み、スキャナをエッジで落とす WAF ルール。どれも半日で作れて効果も数字で見えていましたが、4 つとも作りませんでした。守る対象が今は圧迫されていないこと、持ち続ける費用が作る費用より高いこと、圧迫されたときの第一手は課金の方が確実なこと。判断の材料と、再訪の条件をまとめます。
Bot Fight Mode を無料プランで使うと GitHub Actions が 403 になる
Cloudflare の Bot Fight Mode を有効にしていた 2 サイトだけ、GitHub Actions からの死活チェックが 403 になりました。切って再実行すると全サイト 200。無料プランの Bot Fight Mode は Ruleset Engine の外で動くため WAF カスタムルールの Skip / Allow が効かず、例外を作るには Pro の Super Bot Fight Mode が要ります。監視・CLI・Webhook 受信・確認の curl など自分の非ブラウザ通信が全部判定の対象になる一方、守りたい Workers の起動枠(34%)も D1(30%)も圧迫されていなかったので、8 月に再検討して入れないと決めた経緯と、再訪の条件をまとめます。
Cloudflare Workers の Cron で曜日 0 は拒否され、7 は土曜になる
監視用の Worker に週 1 回の Cron を「30 23 * * 0」で入れたら、wrangler deploy がスクリプトのアップロードだけ成功して Cron の登録に失敗しました(invalid cron string)。0 を 7 に変えて登録は通りましたが、Cloudflare の曜日は 1 = 日曜〜7 = 土曜で、7 は土曜でした。公式ドキュメントの 1 行で気づき、SUN に直した経緯と、デプロイ後に schedules を確認する手順、D1 の読み取りを「1 起動あたりの行数」で 3 時間ごとに見張る Worker の設計をまとめます。
EmDash の記事を API で一括更新する。_rev の場所と draft → publish
Astro 製の CMS「EmDash」で、タイトル 51 本・本文の言い回し 15 本・記事間リンク 32 本を 1 日で直すために、API を直接叩いて一括更新しました。楽観ロックの _rev は GET レスポンスの item の外側(data._rev)にあり、version を渡すと 400 になります。PUT を status: draft で保存して publish を分け、Portable Text は text キーだけを歩けばコードブロックと HTML ブロックを壊しません。タグの割り当てはコンテンツ更新と別経路で、キャッシュ失効も updatedAt も動き方が違う点まで、110 記事で踏んだことをまとめます。
Astro の /_astro/ 画像が移行後に 404 で叩かれ続ける原因と対処
WordPress をヘッドレスに使った Astro の静的ビルドから EmDash へ作り直したあと、旧ビルドの /_astro/<名前>-1024x717_<ハッシュ>.jpeg に週 700 回以上の 404 が来ていました。User-Agent は普通のスマートフォンのブラウザで、画像検索からの着地でした。EmDash のリダイレクト表は拡張子付きのパスを引かないため 747 行入れても発火せず、ミドルウェアでファイル名からメディアを引いて 301 する形にして、週 1,640 件の 404 を 156 件に減らしました。残った 1 枚は同じ画像が別名で 2 回アップロードされていたもので、Redirect Rule 1 本で受けた経緯までまとめます。
Astro + Cloudflare で公開した記事が 404 のままになる原因
記事を公開したのに、記事の URL だけが「ページが見つかりません」のままでした。公開前にその URL を一度でも開いていると、404 のレスポンスがエッジに 30 日キャッシュされ、公開しても消えません。Astro の routeRules はリクエストのパスで照合してステータスを見ず、EmDash が公開時に失効させるタグ(コレクション名と記事 ID)は 404 には付いていないためです。purge の仕組みを足す案を作って取り下げ、404.astro で Astro.cache.set({ maxAge: 300, swr: 0 }) を呼んで 5 分に上書きするだけにした経緯と、swr を忘れると起きることを実測で書きます。
Cloudflare にデプロイしたのに反映されないのはブラウザのキャッシュ
デプロイ後にブラウザで開くと古いページのままで、ハードリロードすると新しくなる状態になりました。エッジは正しく更新されていて、原因はレスポンスに Cache-Control が 1 つも無かったことです。指示が無いとブラウザは Last-Modified から有効期限を推測するため、古い記事ほど長く保持されます。エッジ用のヘッダはエッジで取り除かれるので、ブラウザ向けの指示は別に出す必要がありました。
EmDash で記事にタグを付けても本番に反映されない原因
記事にタグを付け直しても、本番のタグ一覧に出てきませんでした。CMS がキャッシュを自動失効させているのはコンテンツの CRUD だけで、タグの割り当て・サイト設定・メニュー・ウィジェットエリアは別経路になっていました。エッジキャッシュの保持時間を 30 日に延ばしたことで顕在化した穴で、CMS 本体に patch を当てず、アプリ側のミドルウェア 1 か所で塞いだ記録です。