ある朝、定期実行しているスクリプトのログに 403 PERMISSION_DENIED がずらりと並んでいて、心臓が少し冷えました。コードは一行も変えていません。変わったのは Gemini API 側の運用ポリシーでした。2026年6月19日以降、利用制限をかけていない「未制限キー」からのリクエストが遮断されるようになっています。不正利用や意図しない課金を防ぐための変更ですが、放置気味に動かしていた自動処理ほど、ある日いきなり静かに止まる類の更新です。
厄介なのは、エラーの見た目が「キーに制限をかけすぎて弾かれる」ケースとよく似ている点です。原因の切り分けを誤ると、せっかく足した制限をまた外してしまい、振り出しに戻ります。ここでは、自分のキーが対象かを確かめ、稼働中の処理を止めないまま制限を足していく流れを、個人開発で自動処理を日々回している、私自身の点検手順に沿って整理します。
「未制限キー」とは具体的に何を指すのか
Google Cloud / AI Studio で発行する API キーには、大きく二種類の制限を設定できます。ひとつは、そのキーで叩ける API を絞る「API 制限」。もうひとつは、リクエスト元を絞る「アプリケーション制限」(HTTP リファラー・IP アドレス・Android/iOS アプリなど)です。どちらも設定していないキーが、今回遮断対象になった「未制限キー」です。
つまり「Generative Language API だけ有効にしてある」状態でも、アプリケーション制限が空なら未制限扱いになります。私は当初ここを勘違いしていて、「API は絞ってあるから大丈夫」と思い込んでいました。実際には両方を見られていると考えたほうが安全です。
| 制限の種類 | 絞る対象 | 未制限のままだと |
|---|---|---|
| API 制限 | このキーで呼べる API(例: Generative Language API のみ) | 漏れた際にあらゆる API を叩かれうる |
| アプリケーション制限 | リクエスト元(リファラー / IP / アプリ) | 誰がどこから使ってもキーが通る |
まず自分のキーが対象かを確かめる
管理画面を開く前に、稼働中のスクリプトから直接叩いて、いま通っているのか弾かれているのかを確かめます。個人開発の小さなスクリプトでは、私は切り分け用に、生成ではなく models の一覧取得のような軽い呼び出しを使っています。本処理と同じキーで叩くのが要点です。
# 稼働中の自動処理と同じ環境変数のキーで叩く
curl -s -o /dev/null -w "%{http_code}\n" \
"https://generativelanguage.googleapis.com/v1beta/models?key=${GEMINI_API_KEY}"200 が返れば現時点では通っています。403 なら本文を確認します。レスポンスの error.message に「API key not valid」ではなく、リクエスト元やキーの制限に関する文言が出ているかで、未制限遮断なのか別の制限エラーなのかを見分けます。
curl -s "https://generativelanguage.googleapis.com/v1beta/models?key=${GEMINI_API_KEY}" \
| python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('error',{}); print(e.get('status'), '|', e.get('message'))"ここで「リファラー制限により拒否された」系のメッセージが出る場合は、今回の未制限遮断ではなく、制限のかけ方そのものの問題です。その切り分けは「Gemini API キーに HTTP リファラー制限をかけたら本番サイトから 403 エラーになる原因と対処」で扱っているので、症状が一致したらそちらを確認してください。
本番を止めずに制限を足す——「既存キーを直接いじらない」
ここがいちばん事故りやすいところです。稼働中のキーの制限欄を直接編集して保存すると、設定が反映された瞬間から、想定外の呼び出し元がすべて弾かれます。自分の頭の中にあるリクエスト元と、実際にそのキーを使っている場所がずれていると、その差分がそのまま障害になります。私は一度これで、想定し忘れていたバッチ用サーバーを巻き込みかけました。
そこで、既存キーには触れず、制限済みの新しいキーを並行して用意し、検証してから差し替える形を取ります。
- 制限を設定した新規キーを発行する(API 制限=Generative Language API のみ、アプリケーション制限=実際の呼び出し元に合わせる)
- 新キーを本番とは別の環境変数(例:
GEMINI_API_KEY_NEXT)に置き、検証スクリプトで200を確認する - 想定する全ての呼び出し元(ローカル・CI・本番サーバー)から新キーで通ることを確かめる
- 問題なければ本番の環境変数を新キーへ差し替える
- 旧キーは即削除せず、数日ログを見て参照が消えてから無効化する
差し替え自体の安全な進め方は「Gemini API キーをダウンタイムゼロでローテーションする実装パターン」と同じ考え方です。新旧を一定期間併存させ、ログで参照ゼロを確認してから落とす、という順序を守るとほぼ事故りません。
自動運用で「静かに止まる」を防ぐ常設のカナリア
今回いちばん怖かったのは、エラーそのものより「気づくのが遅れたこと」です。定期実行は成功も失敗も同じように静かなので、403 が出ても次の実行までログを見なければ分かりません。そこで、本処理の冒頭に軽い疎通確認を一つ挟み、特定のエラーだけ検知して自分に通知するようにしました。
// 本処理の前に1回だけ叩く疎通確認。403 PERMISSION_DENIED のときだけ通知する
async function assertKeyUsable(apiKey) {
const url =
"https://generativelanguage.googleapis.com/v1beta/models?key=" + apiKey;
const res = await fetch(url);
if (res.ok) return;
const body = await res.json().catch(() => ({}));
const status = body?.error?.status ?? String(res.status);
const message = body?.error?.message ?? "unknown error";
// 制限・キー無効など、設定起因の遮断だけを区別して扱う
if (res.status === 403) {
await notifyMe(`Gemini API key blocked: ${status} / ${message}`);
throw new Error(`KEY_BLOCKED: ${message}`);
}
// 5xx など一時的な失敗は握りつぶさず、通常のリトライへ委ねる
throw new Error(`API check failed: ${res.status} ${message}`);
}notifyMe の中身は、私の場合は自分宛ての簡単な通知で十分でした。大事なのは「設定が原因の遮断(403)」と「サーバー側の一時障害(5xx)」を同じ扱いにしないことです。前者は人が制限を直さないと永遠に直りませんが、後者は待てば戻ります。一緒くたにリトライへ流すと、直らない遮断を延々と叩き続けて、別の課金やレート制限を呼び込みます。
次の一手
まずは手元の全キーを一覧で出し、「アプリケーション制限が空のもの」を洗い出してください。AI Studio や Google Cloud のキー一覧で、制限列が空のキーが今回の対象です。一つでも未制限のキーが自動処理に使われているなら、上の「新キーで検証してから差し替える」手順で、稼働を止めずに制限を足すところから始めるのが安全です。放置運用ほど、止まる前の点検が効いてきます。