API キーをコードに直書きしそうになって、ちょっと冷や汗をかきました。
LLM API を触っていると、手元で検証したいだけの瞬間があります。エラーを減らしたくて、SDK 初期化のところに一瞬だけ key を貼って、あとで消せばいいか、となりがちです。私はまさにそれをやりかけました。
ただ、API key は「あとで消す」ではだいたい負けると思っています。コミットに混ざる、スクショに映る、ログに出る、CI のキャッシュに残る、どこで残るかが読みづらいからです。
今回は説教っぽい話ではなく、自分の未遂をきっかけに、env、secret manager、quota、usage review の4つに分けて最低限の形に直したメモです。
3行まとめ
- API key はコードに置かず、local は env、team と production は secret manager に逃がす
- env に逃がしても漏れる前提で、quota と daily limit を小さく置く
- 週1回、dashboard の usage と repo 内の直書き痕跡を見る
先に置いた前提
この記事では、LLM API を呼ぶ小さな SaaS や社内ツールを想定しています。個人開発からチーム運用に移るくらいの温度感です。
「完璧な secret management を作る」よりも、「今日うっかり key を貼らない」「貼っても被害が広がりすぎない」ことを優先しています。私はこの手の運用を最初から綺麗に作れないタイプなので、まず事故りやすい場所から塞ぐ方針にしました。
OpenAI の production best practices でも、API key はコードや public repo に出さず、environment variables か secret management service でアプリに渡す、という趣旨の説明があります。usage page で key の tracking を見られる点も、運用としてはかなり大事だと思いました。
まず repo 内を疑う
最初にやったのは、手元の repo に「もう貼ってないか」を見ることでした。
厳密な secret scanner の代わりではないですが、まず雑に grep します。false positive は出ます。出ますが、何も見ないよりはだいぶましです。
git grep -nE '(apiKey: *"|Authorization: Bearer |OPENAI_API_KEY=.*[^=])' -- ':!*.md' ':!*.lock'
私は最初、README.md のサンプルまで引っかかって少し面倒でした。なので、docs や markdown は用途によって除外してもよいと思います。ただし、本当に事故っている場合はドキュメントに貼っていることもあるので、最初の1回は除外しない方が安心です。
見る場所はだいたいこのあたりです。
| 場所 | 見る理由 |
|---|---|
src/ |
SDK 初期化に直書きされやすい |
scripts/ |
検証用スクリプトに残りやすい |
.github/workflows/ |
curl の Authorization に混ざりやすい |
README.md |
動作例として本物を貼りがち |
.env.example |
example のつもりで実値が入ることがある |
CI でも軽く止める
次に、pull request で雑に止めるチェックも足しました。これは専用ツールの代替ではありません。あくまで「直書きっぽいものが混ざったら会話を始める」ための軽い網です。
name: secret-check
on:
pull_request:
jobs:
secret-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check suspicious API key usage
run: |
if git grep -nE '(apiKey: *"|Authorization: Bearer )' -- ':!*.md' ':!*.lock'; then
echo "possible hardcoded API key"
exit 1
fi
ちゃんとやるなら secret scanning や専用 scanner を入れるべきです。ただ、最初の一歩としては、SDK 初期化や curl の Authorization header を PR 上で見つけるだけでも効果がありました。
env に逃がすだけでは足りなかった
直書きしないために、まず local では env から読む形にしました。
やりかけた悪い例はこうです。
// bad
const client = new OpenAI({
apiKey: "ここに実キーを貼る",
});
直した例はこうです。
import OpenAI from "openai";
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY is not set");
}
export const client = new OpenAI({ apiKey });
これだけだと普通の話ですが、ポイントは「env にしたから安全」ではなく、「コードレビューで key の値を見なくてよくする」くらいに捉えることだと思っています。
env は普通に漏れます。shell history に残ることもありますし、debug log に出してしまうこともあります。production やチーム運用なら、GitHub Actions secrets、AWS Secrets Manager、GCP Secret Manager、Vercel / Cloudflare などの secret 管理に寄せる方が自然です。
.env.example に残すもの
.env.example には値を入れず、名前と意図だけ残しました。
OPENAI_API_KEY=
FLATKEY_API_KEY=
AI_GATEWAY_BASE_URL=https://router.flatkey.ai/v1
LLM_DAILY_TOKEN_LIMIT=50000
LLM_WEEKLY_BUDGET_USD=20
ここで LLM_DAILY_TOKEN_LIMIT と LLM_WEEKLY_BUDGET_USD も一緒に置いたのが、自分の中ではよかったです。key そのものだけを見ると secret management の話で終わりますが、実際に怖いのは漏れた後に使われ続けることだからです。
quota を小さくして爆発範囲を決める
API key を env に移しても、漏れた瞬間に無制限で使えるなら怖さは残ります。なので、私は quota を「本番の都合」ではなく「漏れた時の爆発範囲」として見ることにしました。
たとえば個人開発や小さな検証なら、最初から大きい上限はいらないです。
| 用途 | 最初の quota 方針 |
|---|---|
| local 検証 | 1日で使い切っても痛くない上限 |
| preview 環境 | production よりかなり小さい上限 |
| production | alert と usage review 前提で必要量だけ |
| batch / agent | job 単位の上限と停止条件を置く |
私はここで少し反省しました。以前は「動かないと困るから余裕を持たせる」と考えがちでした。でも LLM API は、うっかりループや agent の暴走で使い方が跳ねます。余裕を持たせるなら、同時に review の仕組みも置かないと片手落ちでした。
個人開発の暫定ルール
今はざっくりこうしました。
- local key は検証用として quota を小さくする
- production key と local key を分ける
- preview key は production より小さい quota にする
- 使わなくなった key は消す
- 週1回、usage と key 一覧を見る
特に「local と production を分ける」は大事でした。local で雑に試す key と、本番ユーザーのリクエストを受ける key が同じだと、調査がしんどくなります。
usage review を週次にした
env と quota まで入れても、最後は見ないと気づけません。
なので、週次で usage review を見ることにしました。細かい FinOps をやるというより、変な増え方をしていないかを見ます。
見る項目
| 見る項目 | 気にすること |
|---|---|
| key ごとの usage | local key が妙に増えていないか |
| model ごとの usage | 高い model に意図せず流れていないか |
| project / environment | preview や batch が増え続けていないか |
| error と retry | 失敗 retry で token を溶かしていないか |
| quota 到達 | 上限が低すぎるのか、使い方が荒いのか |
OpenAI 側では Usage page や API key tracking を見る、gateway を挟んでいるなら gateway 側の dashboard も見る、という形です。key を複数作っている場合、どの key がどの用途か名前で分かるようにしておくのも地味に効きました。
Flatkey AI ではどう見るか
Flatkey AI は、複数の AI model を1つの OpenAI-compatible base URL と1つの key で呼べる API gateway です。local の知識ベース上では、https://router.flatkey.ai/v1 を base URL として、API keys、usage tracking、billing、quota limits、routing configuration を dashboard で見る前提になっています。
この手の gateway を使うときも、私は「key が1つで便利」で止めない方がよいと思いました。むしろ見る場所を集約できるので、次のように運用ルールへ落とせます。
| やること | Flatkey 側で見たいもの |
|---|---|
| key を用途ごとに分ける | API key 一覧 |
| 上限を小さく始める | quota limits |
| model の使い分けを見る | routing と model usage |
| 請求前に気づく | usage tracking と billing |
| 週次で棚卸しする | dashboard の usage review |
宣伝っぽくしたいわけではなく、key management は「どこで止められるか」と「どこで気づけるか」が大事です。1つの dashboard で key、quota、usage、billing を見られるなら、週次レビューの対象に入れやすいです。
やらなかったこと
逆に、今回はやらなかったこともあります。ここを混ぜると、最初の事故防止としては重すぎると思ったからです。
| やらなかったこと | 今回やらなかった理由 |
|---|---|
| 全社向けの複雑な権限設計 | まず個人開発と小チームの直書き防止が目的だった |
| 自動 key rotation | rotation の前に、用途分離と不要 key 削除を優先した |
| 完全な secret scanner 導入 | 最初は PR で会話できる軽い検知を優先した |
| 独自の課金アラート基盤 | dashboard と quota の定期確認から始めた |
こういうものは、もちろん必要になる場面があります。ただ、最初から全部を入れようとすると、面倒になって結局なにも入らないことがあります。私はそのパターンを何度かやっています。
なので今回は、repo に貼らない、用途ごとに key を分ける、quota を小さくする、週次で usage を見る、という小さい運用に寄せました。あとから強くする前提で、まず人間が続けられる形にする方がよさそうでした。
小さくても、続けられる運用の方が事故には強いと思います。
ハマったポイント
一番ハマったのは、直書き防止を secret management だけの話にしてしまうことでした。
実際には4層くらいあります。
| 層 | 目的 |
|---|---|
| env / secret manager | コードに key を置かない |
| repo check | 直書きの混入に早めに気づく |
| quota | 漏れた時の被害を小さくする |
| usage review | 変な使われ方に気づく |
env だけだと、漏れた後に止まりません。quota だけだと、そもそも key が repo に残ります。usage review だけだと、気づくまでの消費が読めません。
最初から全部を完璧にするのは重いですが、直書きしそうになった瞬間にこの4つを小さく入れるのが、自分にはちょうどよかったです。
まとめ
今回やったことは地味です。
- repo 内に直書きがないか見る
- local は env、team と production は secret manager に寄せる
- key を用途ごとに分ける
- quota を小さく始める
- 週1回 usage と key 一覧を見る
ただ、LLM API は key ひとつで実コストが動くので、この地味さがそのまま安心につながると思っています。
個人的には、API key 管理は「隠す」だけでは足りなくて、「漏れた時に小さく止まる」「増え方に気づく」まで含めて運用するのがよさそうでした。
参考
おわりに
API key を貼りそうになった時点で負け、というより、貼りそうになる前提で止め方を作るのが現実的だと思いました。
自分の repo で API key の直書きがないか見て、quota と usage review を週次で見る。まずはここからで十分かもしれません。
間違いあったらコメントください。よろしくお願いします。