APIキーをコードに直接書いたまま、そろそろまずい気がしている。そんな状態で手が止まっていませんか。
学習を進めて外部のサービスを触りはじめると、多くの人がこの壁にぶつかります。鍵をどこに置けばいいのか、という問題です。
答えは環境変数です。プログラムの外側に値を置いておき、実行するときに渡す仕組みのことを指します。
名前は聞いたことがあるけれど、venvと何が違うのかはよくわからない。そう感じている方も多いはずです。
この記事では、環境変数の考え方から.envファイルの使い方まで、手元で動かせるコードと一緒に順番に整理していきます。
環境変数は、プログラムの外に置く設定¶
まずは言葉の整理から始めましょう。環境変数とは、OSが持っている名前と値の組のことです。
プログラムを起動すると、その値がプロセスへ引き渡されます。Pythonのコードからは、受け取ったその値を読むことになります。
紛らわしいのですが、仮想環境のvenvとは別の話です。venvはライブラリを入れる場所を分ける仕組みで、環境変数は設定値を渡す仕組みだと考えてください。【関連記事】Pythonの仮想環境(venv)って何のためにある?プロジェクトごとに混ぜない管理法
ではなぜ、わざわざコードの外へ出すのでしょうか。理由は大きく2つあります。
1つめは安全のためです。APIキーをコードに書くと、Gitの履歴に残り、リポジトリを共有した相手全員から見えてしまいます。
2つめは環境ごとに値を変えるためです。手元の開発用と本番用でデータベースの接続先が違う、というのはごく普通のことです。
同じコードのまま、渡す値だけを差し替える。 これが環境変数を使ういちばんの理由です。
os.environとos.getenvで読み取る¶
読み取り方はとても簡単です。標準ライブラリのosモジュールを使います。
追加のインストールは要りません。まずは手元で動かしてみてください。
import os
# 辞書のように取り出す
print(os.environ["HOME"])
# 見つからないときの値を決められる
print(os.getenv("MY_API_KEY", "未設定"))
os.environ は辞書のように振る舞うオブジェクトです。存在しないキーを指定すると、KeyErrorが出てそこで止まります。
一方の os.getenv は、見つからないときにNoneを返します。第2引数を渡しておけば、そのときに使う値を自分で決められます。
どちらを使うか迷ったら、その値が無くても動くかどうかで決めてください。必須の設定は os.environ で読み、任意の設定は os.getenv で読む。 こう決めておくと、設定漏れに早く気づけます。
なお os.environ.get("KEY") という書き方も見かけます。辞書のgetと同じ動きなので、os.getenv とほとんど同じものだと考えて構いません。
もうひとつ、読み取った値をそのままprintしないでください。ログに鍵が残ると、ログを見られる人全員に鍵が渡ったのと同じことになります。
値はいつでも文字列で返ってくる¶
ここは初心者がつまずきやすいところです。環境変数の値は、必ず文字列として渡ってきます。
数値のつもりで書いた値も、Pythonから見ればただの文字列です。
import os
os.environ["MAX_RETRY"] = "3"
count = os.getenv("MAX_RETRY")
print(count + 1) # TypeError: can only concatenate str
print(int(count) + 1) # 4
真偽値はもっとやっかいです。空でない文字列はすべて真として扱われるため、falseと書いた文字列も真になってしまいます。
そこで、自分で判定してから使うのが定番の書き方になります。
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
真偽の判定でハマりやすい話は、別の記事で詳しくまとめています。【関連記事】Pythonの真偽値を深く知る!if文で意外とハマるNoneや空リストの挙動を解説
私は10年ほどエンジニアとして開発に関わってきましたが、DEBUGをfalseにしたはずのサーバーが、デバッグ表示のまま動いていた現場を見たことがあります。原因は、文字列のfalseが真になっていた、ただそれだけでした。
環境変数はいつ読み込まれるのか¶
もうひとつ、知っておくと得をする性質があります。読み込みのタイミングです。
Python公式ドキュメントによると、os.environ の中身は、osモジュールが最初にインポートされたときに取り込まれます。多くの場合、それはPythonの起動処理の中です。
つまり、プログラムが動き出したあとにOS側で環境変数を変えても、その変更は反映されません。別のターミナルで設定し直しても、すでに動いているプロセスには届かないということです。
逆に、Pythonのコードから os.environ を書き換えた場合はputenvが呼ばれます。この変更は、そこから起動する子プロセスへ引き継がれます。
設定を変えたらプログラムを再起動する。まずはこれを基本の作法として覚えておいてください。
.envファイルで開発中の手間をなくす¶
ここまで読んで、ひとつ面倒に気づいた方もいるはずです。必要な値を毎回ターミナルで設定するのは、正直つらい作業です。
しかもターミナルを閉じれば消えてしまいます。翌日また同じことを繰り返すことになります。
そこで使われるのが.envファイルです。プロジェクトの直下に置いて、必要な値をまとめて書いておくだけのテキストファイルになります。
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx
DATABASE_URL=postgresql://localhost/app
DEBUG=true
ただしPython本体には、この.envを読み込む機能がありません。読ませるには、python-dotenvというライブラリを使います。
python-dotenvの基本的な使い方¶
インストールはpipで一行です。読み込みも一行で終わります。
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv() # .env の中身を os.environ へ流し込む
print(os.getenv("OPENAI_API_KEY"))
load_dotenv() を呼ぶと、.envの内容が os.environ に取り込まれます。あとは、これまでどおりの読み取り方でそのまま使えます。
公式の説明によると、すでに設定済みの環境変数は既定では上書きされません。上書きしたいときだけ、override=True を渡します。
この既定はよくできています。本番サーバーで設定した値を、うっかり紛れこんだ.envに壊されることがないからです。
環境そのものは変えずに中身だけ確認したいときは、dotenv_values() が使えます。os.environ には触れず、読み取った内容を辞書で返してくれます。
.envの書き方はシェルのファイルによく似ています。イコールの左右に余計な空白を入れず、1行に1つずつ書くのが基本です。
呼び出す場所にも気をつけてください。load_dotenv() は、環境変数を読むコードより先に走らせる必要があります。
アプリの入り口となるファイルの、いちばん上のほうで呼ぶ。そう決めておけば、順番で悩むことはなくなります。
.envは絶対にコミットしない¶
ここがいちばん大事な話です。.envには鍵が書いてあるので、Gitに入れてはいけません。
ファイルを作る前に.gitignoreへ一行足しておきましょう。あとから思い出すのではなく、作業を始める前にやるのがコツです。
.env
代わりに、.env.exampleという見本を用意してそちらをコミットします。キーの名前だけ書いて、値は空にしておくやり方です。
こうしておけば、あとから参加した人も何を設定すればいいかすぐわかります。Gitの基本操作に不安が残っている方は、こちらもあわせてどうぞ。【関連記事】Python学習にGitは必要?初心者が最低限覚えたい使い方
なお、GitHubにはpush protectionという仕組みがあり、鍵らしき文字列を含むpushをブロックしてくれます。とはいえ、守ってもらう前提でコードを書くべきではありません。
一度でも公開の場所へ出してしまった鍵は、あとから消しても漏れたものとして扱います。鍵が漏れたら、消すのではなく作り直す。 これは実務での鉄則です。
置き場所は環境によって変わる¶
.envは開発中に便利な道具ですが、どこでも同じように使うわけではありません。環境ごとに置き場所が変わります。
代表的な場面をまとめておきます。迷ったときはここに戻ってきてください。
| 場面 | 置き場所 | 補足 |
|---|---|---|
| 手元での開発 | .envファイル | Gitには入れない |
| 本番サーバー | ホスティング側の設定画面 | 多くのサービスに入力欄がある |
| CI/CDの自動実行 | サービスのSecrets機能 | ログに出さない設定にする |
| 一時的な動作確認 | ターミナルで設定 | 閉じると消える |
名前の付け方にも、ゆるやかな慣習があります。大文字とアンダースコアで書くのが一般的です。
小文字でも動きはします。ただしWindowsではキーが大文字へ変換されるため、環境をまたいで動かすなら最初から大文字でそろえておくほうが安全です。
起動時にまとめて確認する¶
実務で効いてくる工夫を、最後にひとつ紹介します。必要な環境変数がそろっているかを、起動した直後に確かめる書き方です。
途中まで動いてから鍵が無いと気づくのが、いちばん時間を無駄にします。
import os
import sys
REQUIRED = ("OPENAI_API_KEY", "DATABASE_URL")
missing = [name for name in REQUIRED if not os.getenv(name)]
if missing:
sys.exit(f"環境変数が足りません: {', '.join(missing)}")
たった数行ですが、効果は大きいです。何が足りないのかを、その場で名指しで教えてくれます。
外部のAPIを呼ぶコードでは特に役立ちます。リクエストを投げる前に止まってくれるほうが、認証エラーの原因調査より何倍も速いからです。【関連記事】Pythonのrequestsとは?WebからデータをとってくるHTTP通信の基本を初心者向けに解説
私自身、認証エラーの原因を半日かけて追いかけた末に、環境変数の名前を1文字打ち間違えていただけ、という経験があります。この数行があれば、5秒で終わっていた調査でした。
実際にAPIキーを使うところまで進めてみたい方は、こちらでキーの扱いも含めて解説しています。【関連記事】PythonからOpenAIのAPIを使う方法を解説!
よくあるつまずきと対処¶
ここまでの内容を、実際に書くときへ気をつける点だけに絞ってまとめます。
| つまずき | 起きること | 対処 |
|---|---|---|
| .envをコミットしてしまう | 鍵が履歴に残り続ける | .gitignoreへ先に書き、鍵を作り直す |
| 取り出した値を数値として計算する | TypeErrorで止まる | int() で変換してから使う |
| falseという文字列を偽だと思う | 常に真になる | 文字列として自分で判定する |
| 設定し直したのに反映されない | 起動済みのプロセスには届かない | プログラムを再起動する |
load_dotenv() を呼び忘れる |
Noneが返ってくる | 起動直後にまとめて呼ぶ |
覚えることは、そう多くありません。外に出す、コミットしない、文字列として受け取る。 この3つを押さえておけば、当面は困らないはずです。
まずは手元のプロジェクトに.envを1つ作って、コードに直接書いた鍵を追い出すところから始めてみてください。作業そのものは5分で終わりますし、一度やってしまえば次からは当たり前になります。
ここまでお読みいただきありがとうございました。