このレッスンのロックを解除するには、まず「CI/CDパイプラインを設定して継続的デプロイを実現」を完了してください。前のレッスンに戻る

はじめに

AI 生成された Web アプリを公開するとき、コードは戦いの半分にすぎません。もう半分は、コードがやりとりするすべてのものです ― 推論用の API キー、データベース接続文字列、サードパーティサービスの認証情報、機能のフラグ。本番環境でこれらを誤って扱ってしまうと、実際の障害や実際のセキュリティインシデントの最も一般的な原因になります。

このレッスンでは、ローカル開発、ステージング、本番の 3 つの環境でシークレットを安全に保つための実践的なワークフローを解説します。Vercel、Railway、Render、Fly.io、生の VPS のいずれにデプロイしても機能する単一のメンタルモデルを学びます。

3 つの環境

あなたが構築するすべてのアプリは少なくとも3 つの場所に存在し、場所ごとにシークレットに対するルールが異なります。

ローカル開発はあなたのノート PCで行われます。迅速なイテレーションが必要なので、シークレットは `.env` ファイル(git で無視される)に保存し、`dotenv` のようなライブラリで読み込みます。このファイルはコミットされず、あなたのマシンを離れることもありません。

ステージングはフェイクデータを使った本番環境のコピーです。ユーザーが目にする前にバグを捕捉するために存在します。シークレットはホスティングサービス(Railway Variables、Vercel Environment Variables など)から注入され、その環境のみにスコープされます。

本番環境は実際のユーザーがアクセスする環境です。シークレットは同じホスティングサービスから取得しますが、より厳格なネームスペースに格納され、多くの場合アクセス制御が分離されています。ステージングキーが漏洩すると厄介ですが、本番キーが漏洩すると請求で大惨事を招きます。

dev/staging/prod の 3 環境とそれぞれ異なるシークレット供給源を示す図

コード内での環境変数の読み込み

下記のパターンは、ほぼすべての Node.js または Python バックエンドで機能します。重要な考え方は、起動時に一度だけ読み込み、必要なシークレットがなければ大きく失敗することです。

javascript
import dotenv from 'dotenv';
dotenv.config();
const required = ['DATABASE_URL', 'OPENAI_API_KEY', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
export const config = {
databaseUrl: process.env.DATABASE_URL,
openaiKey: process.env.OPENAI_API_KEY,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development',
port: Number(process.env.PORT) || 3000,
};

起動時のチェックに注目してください。深夜3 時に `undefined` をサイレントにデータベースクライアントに送るより、起動時にクラッシュするほうがはるかに優れています。`dotenv` の呼び出しはファイルが存在する場合にのみファイルを読み取るため、ホスティングが変数を直接注入する本番環境でも同じコードが変更なしで動作します。

Python の場合は `pydantic-settings` を使った等価な方法があります:

python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
openai_api_key: str
jwt_secret: str
node_env: str = "development"
port: int = 3000
settings = Settings()

Pydantic は必要なフィールドが欠落していると明確なバリデーションエラーを発生させ、同じフェイルファスト動作を実現します。

データベース接続パターン

Neon、Supabase、Railway などのマネージドサービス上の Postgres データベースの場合、接続文字列があなたのシークレットです。パスワードのように扱ってください。

javascript
import pg from 'pg';
import { config } from './config.js';
export const pool = new pg.Pool({
connectionString: config.databaseUrl,
max: 10,
idleTimeoutMillis: 30000,
ssl: config.nodeEnv === 'production' ? { rejectUnauthorized: true } : false,
});

ここで重要な点が 2 つあります。第一に、接続プーリングによってアプリがリクエストごとに新しい TCP 接続を開くのを防ぎます。第二に、本番環境では SSL が必要です。多くのマネージドデータベースは暗号化されていない接続を拒否しますが、明示的に指定しておけばデフォルトが変更された場合の保護になります。

サーバーレスプラットフォーム(Vercel Functions、Cloudflare Workers)の場合は、関数の外側に存在する接続プーラーを使用してください ― Neon には組み込みのものがあり、Supabase には `pgbouncer` のトランザクションモードがあります。上記のプールオブジェクトはプロセスメモリ内に存在するため、サーバーレスではコールドスタートごとに新しいプールが作成され、これは低速で、トラフィック急増時にデータベース接続を枯渇させる可能性があります。

AI サービス用 API キーの管理

AI API(OpenAI、Anthropic、Google など)はトークン単位で課金されます。キーが漏洩すれば数時間でクレジットカードの残高を使い切ることができます。安全に保つための 3 つのルールがあります。

第一に、プロバイダーのダッシュボードで厳格な使用上限を設定してください。すべての主要プロバイダーがこの機能を提供しています。第二に、環境ごとに別々のキーを作成してください ― `myapp-dev`、`myapp-staging`、`myapp-prod` ― そうすれば他に影響を与えずに1 つを失効させられます。第三に、キーは絶対にログに出力せず、ロギングサービスに送信されるエラーメッセージにも含めないでください。

javascript
const openaiHeaders = {
'Authorization': `Bearer ${config.openaiKey}`,
'Content-Type': 'application/json',
};
async function callOpenAI(messages) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: openaiHeaders,
body: JSON.stringify({ model: 'gpt-4o-mini', messages }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`OpenAI error ${res.status}: ${text}`);
}
return res.json();
}

エラーハンドラーはデバッグのためレスポンスボディをキャプチャしますが、それはキーを含まないため問題ありません ― アップストリームプロバイダーのエラーのみが含まれます。デバッグのためにリクエストヘッダーをログに記録する必要がある場合は、まず `Authorization` フィールドをマスキングしてください。

よくある間違いとその回避方法

最も頻繁な間違いは `.env` ファイルをコミットしてしまうことです。一度だけでも。Git の履歴は永遠に残り、ボットはプッシュから数分以内に GitHub をスキャンして公開されたキーを探します。`.env` を `.gitignore` に初日から追加し、CI で `git-secrets` や `gitleaks` などのツールを使って誤ったコミットを防いでください。

2番目の間違いは、開発と本番で同じデータベースを使用することです。ローカルスクリプトの `DROP TABLE` が本番を消すまでは便利に感じられます。常に別々のデータベースを使用し、理想的には環境ごとに別々のプロバイダープロジェクトを使用してください。

3 番目の間違いは、計画なしにキーをローテーションすることです。データベースのパスワードをローテーションすると、アプリの実行中のインスタンスはすべて、再起動するまで再接続に失敗します。サポートされているプラットフォームでは、パスワードを含む接続文字列を使用し、起動時にリトライするようにアプリを設計してください。Vercel のようなプラットフォームでは、シークレットをローテーションした後に再デプロイすれば自動的に処理されます。

まとめ

すべてのシークレットを明日漏洩する前提で扱ってください ― 最終的にそうなるからです。ローカルでは `dotenv` または `pydantic-settings` と共に `.env` ファイルを使用し、ステージングと本番ではホスティングプラットフォームに変数を注入させ、起動時に必須シークレットを検証し、本番では `ssl: true` でデータベース接続をプールし、すべての AI API キーに使用上限と環境固有のキーを与えてください。このセットアップは半日で配線でき、本番インシデントの最も一般的なカテゴリからあなたを救います。

レッスンの練習

レッスンのチェックポイント

学んだ内容を復習し、回答へのフィードバックを受けましょう。

0/1
回答済み · 0/1