APIキーやパスワードをソースコードに書いてしまうと、GitHubなどで公開された瞬間に悪用される危険があります。
この記事では、秘密情報はコードに書かず、.envファイルとpython-dotenvで安全に読み込むという実践的な方法を、Python初心者の方にも分かるように丁寧に解説します。
APIキーをコードに書かない理由
秘密情報(APIキー/トークン/パスワード)とは
アプリが外部サービスや自社のサーバにアクセスする際には、権限を示すためのAPIキーやアクセストークン、パスワードなどが必要になります。
これらは第三者に知られてはいけない値で、漏えいすると不正利用や課金被害、データ流出につながります。
具体例としては、クラウドのアクセスキー、DB接続パスワード、Webhookの署名シークレットなどがあります。
ハードコードが危険な理由
コード内に直書き(ハードコード)すると、次のようなリスクが一気に高まります。
まず、バージョン管理(Git)で履歴に永久に残るため、後から消しても過去コミットに情報が残ります。
また、レビューやデバッグで他人のPCやログに転記されやすいことも問題です。
さらに、複数環境(開発/本番)で値が異なるため、環境ごとに分けて管理する仕組みが必要です。
漏えいのよくある原因(GitHub/ログ)
漏えいの典型例は次の通りです。
GitHubなどパブリックリポジトリに誤ってコミット、CIログやアプリログに秘密値をそのまま出力、スクリーンショットやチュートリアル資料に実値を貼り付け、画面共有中のターミナル履歴などです。
どれも「うっかり」で起きるため、仕組みとして秘密をファイルと環境変数で切り離すことが重要です。
.envとpython-dotenvの準備
必要なもの(.env/python-dotenv)
.envとは、環境変数を書き並べたテキストファイルです。
python-dotenvは、このファイルを読み込んでPythonのos.environ
にセットするライブラリです。
これにより、コードはos.getenv
で値を取得するだけになり、秘密をコードから分離できます。
インストール(pip install)
仮想環境の有無は問いませんが、プロジェクト単位での管理をおすすめします。
インストールは次の通りです。
pip install python-dotenv
Collecting python-dotenv
Installing collected packages: python-dotenv
Successfully installed python-dotenv-...
.envの基本フォーマット
.envは1行に1つ、KEY=VALUE形式で書きます。
値にスペースや記号がある場合は引用符で囲みます。
コメントは#
で書けます。
# これはコメントです
API_KEY="sk-example-xxxxxxxx"
DEBUG=true
PORT=8080
TIMEOUT_SECONDS=5
# カンマ区切りの列挙
ALLOWED_ORIGINS="https://example.com,https://api.example.com"
# 改行を含む秘密鍵は \n を使って表現するのが安全
PRIVATE_KEY="-----BEGIN KEY-----\nABCDEF...\n-----END KEY-----"
.gitignoreに.envを追加
.envは絶対にリポジトリに含めないようにします。
.gitignoreに追記しましょう。
# 環境変数ファイルはコミット禁止
.env
.env.*.local
.env.exampleを用意
チーム開発では、必要なキー名だけを並べたサンプル(.env.example)を用意します。
実値は入れずダミーや空欄にします。
# 各自ここを自分の値で埋めて .env にコピー
API_KEY=""
DEBUG=false
PORT=8000
TIMEOUT_SECONDS=5
ALLOWED_ORIGINS=""
PRIVATE_KEY=""
python-dotenvの基本的な使い方
まずload_dotenv()
最初にload_dotenv()
を呼ぶと、カレントディレクトリから上位に向かって.env
を探索し、見つかった値を環境変数として読み込みます。
デフォルトでは既存の環境変数を上書きしません
。
# app_basic.py
from dotenv import load_dotenv
import os
# .env を探して読み込む(既存の環境変数は保持)
load_dotenv()
# 例: API_KEY を取り出して使う
api_key = os.getenv("API_KEY")
print("API_KEY is set:", bool(api_key))
想定される出力:
API_KEY is set: True
os.getenvで取得
os.getenv("KEY")
で値を文字列として取得します。
未設定時はNone
が返ります。
# app_getenv.py
from dotenv import load_dotenv
import os
load_dotenv()
print("API_KEY:", os.getenv("API_KEY")) # 文字列 or None
print("DEBUG:", os.getenv("DEBUG")) # "true" など (文字列)
print("PORT:", os.getenv("PORT")) # "8080" など (文字列)
API_KEY: sk-example-xxxxxxxx
DEBUG: true
PORT: 8080
デフォルト値とエラー対策
必須の値は存在チェックし、あれば使う、なければエラーにするのが安全です。
任意の値はデフォルトを用意します。
# app_defaults.py
from dotenv import load_dotenv
import os
from typing import Optional
load_dotenv()
def require_env(name: str) -> str:
"""必須の環境変数を取り出す。未設定なら例外を送出。"""
value = os.getenv(name)
if not value:
raise RuntimeError(f"Required environment variable '{name}' is missing")
return value
def get_env(name: str, default: Optional[str] = None) -> str:
"""任意の環境変数を取り出す。未設定ならデフォルトを返す。"""
value = os.getenv(name)
return value if value is not None else default
api_key = require_env("API_KEY")
timeout = int(get_env("TIMEOUT_SECONDS", "5")) # 文字列→整数に変換
print("API key length:", len(api_key))
print("Timeout seconds:", timeout)
API key length: 19
Timeout seconds: 5
真偽値/数値の変換
環境変数は文字列なので、必要に応じて型変換します。
真偽値は表記ゆれに注意しましょう。
# app_casting.py
from dotenv import load_dotenv
import os
load_dotenv()
def to_bool(value: str, default: bool = False) -> bool:
"""true/false, 1/0, yes/no, on/off を扱う簡易変換"""
if value is None:
return default
return value.strip().lower() in ("1", "true", "yes", "on")
def get_int(name: str, default: int) -> int:
v = os.getenv(name)
try:
return int(v) if v is not None else default
except ValueError:
# 値が不正ならデフォルトにフォールバック
return default
debug = to_bool(os.getenv("DEBUG"), default=False)
port = get_int("PORT", 8000)
print("Debug mode:", debug)
print("Port:", port)
Debug mode: True
Port: 8080
.envの場所と読み込み順
load_dotenv()はカレントディレクトリから親ディレクトリを探索して最初に見つかった.env
を読み込みます。
複数ファイルをレイヤーして使いたい場合は、読み込み順を明示します。
# app_layers.py
from dotenv import load_dotenv
from pathlib import Path
# まず共通設定
load_dotenv(dotenv_path=Path(".") / ".env")
# 環境固有(ローカルの上書き設定)
# override=True で既存値を上書き可能にする
load_dotenv(dotenv_path=Path(".") / ".env.local", override=True)
上書きの優先順位のイメージ:
由来 | 既存値を上書きするか |
---|---|
OSやCIが最初に与えた環境変数 | いいえ(デフォルト設定では保持) |
load_dotenv(…, override=False) | いいえ(未設定のときのみセット) |
load_dotenv(…, override=True) | はい(後から読むものが勝つ) |
直接os.environに代入 | はい(最後に書いたものが勝つ) |
Jupyterでの読み込み
Notebookでは、最初のセルでload_dotenv()
を呼ぶのが確実です。
カーネルの再起動後も同じセルを実行すれば再設定できます。
# In[1]: Jupyter の最初のセル
from dotenv import load_dotenv
load_dotenv() # .env を読み込む
# In[2]: 値の確認
import os
print("API_KEY exists:", os.getenv("API_KEY") is not None)
API_KEY exists: True
VSCodeのenvFile設定
VSCodeでデバッグやターミナル起動時に自動で.env
を読みたい場合、設定を追加できます。
拡張機能のバージョンにより挙動は異なりますが、代表的な設定は次の通りです。
- settings.jsonでPython拡張の
python.envFile
を指定する方法 - launch.jsonのデバッグ構成に
envFile
を指定する方法
// .vscode/settings.json
{
"python.envFile": "${workspaceFolder}/.env"
}
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"envFile": "${workspaceFolder}/.env"
}
]
}
ただしツール依存の設定に過度に頼らず、アプリ側でload_dotenv()
を呼ぶ実装にしておくと、どの環境でも確実に動作します。
安全運用のコツと注意点
本番/CIは環境変数(Secrets)で渡す
本番やCIでは、.envファイルを配布せず、プラットフォームのSecrets機能で環境変数として注入するのが原則です。
GitHub Actionsの例を示します。
# .github/workflows/ci.yml
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # リポジトリ/Org Secrets
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- run: pytest -q
アプリ側はos.getenv("OPENAI_API_KEY")
で取得でき、python-dotenvはローカル開発専用として位置づけられます。
ログに秘密を出さない
秘密値をそのままログに出力しないようにします。
表示が必要な場合はマスクします。
# logging_redact.py
from dotenv import load_dotenv
import os
load_dotenv()
def mask(secret: str, keep: int = 4) -> str:
if not secret:
return "(none)"
s = str(secret)
return "*" * max(0, len(s) - keep) + s[-keep:]
api_key = os.getenv("API_KEY")
print("Using API key:", mask(api_key)) # 末尾4桁だけ見せる
Using API key: ************xxxx
うっかりコミットした時の対処
最優先はキーの無効化/ローテーションです。
その後、履歴を消去します。
- まず提供元のコンソールでキーを無効化し、新しいキーを発行します。流出元の環境に即時反映します。
- リポジトリ履歴から秘密を除去します。最近は
git filter-repo
が推奨です(BFGでも可)。
# 例: 特定の文字列を履歴から置換(機密値は secrets.txt に記載)
# secrets.txt のフォーマット: 'oldsecret==>REDACTED'
git filter-repo --replace-text secrets.txt
git push --force
- 影響範囲を点検し、第三者アクセスや課金状況を監査します。必要に応じて通知や報告も行います。
GitHubのSecret ScanningやPush Protectionも有効化しておくと、事前検知が期待できます。
文字コードはUTF-8に
.envはUTF-8で保存し、BOMなしを推奨します。
python-dotenvはload_dotenv(encoding="utf-8")
を指定できます。
日本語コメントや非ASCII文字が混じる場合に文字化け防止になります。
from dotenv import load_dotenv
load_dotenv(encoding="utf-8")
チームでのチェックリスト
最小限の確認事項を共有しておくと、ヒューマンエラーが減ります。
- リポジトリに.envを絶対にコミットしない。.gitignoreで遮断し、PRでレビュー。
- .env.exampleを更新し、必要なキー名と説明を常に最新化。
- ローカルはpython-dotenv、本番/CIはSecrets。役割分担を明文化。
- ログや例外メッセージに秘密を出さない。表示時はマスク。
- 漏えい時の手順(無効化→履歴除去→監査)をドキュメント化。
まとめ
秘密情報はコードに書かず、環境変数として扱うのがセキュアな開発の第一歩です。
.envとpython-dotenvを使えば、ローカルでは快適に、リポジトリや本番では安全に管理できます。
初心者の方は、まず.env
とload_dotenv()
の基本から始め、os.getenv
で取得、デフォルト値や型変換、上書きのルールを身につけてください。
運用では、本番/CIはSecretsで注入、ログはマスク、万一は即ローテーションという原則を徹底すれば、安心してAPIや外部サービスを活用できるようになります。