📂

手元の環境変数をいい感じに管理するzenv (version 2)

に公開

という記事を以前に書いたのですが、

https://zenn.dev/mizutani/articles/8af10c54b7feca

ver2をリリースしたので改めて記事にしました。v1系から約4年間ほど自分で使い倒してきた中で見えてきた課題を解決し、よりシンプルで使いやすい形に進化させることができたと考えています。

https://github.com/m-mizutani/zenv/releases/tag/v2.0.0

Goの環境がすでに構築されていれば、以下コマンドで利用できます。

go install github.com/m-mizutani/zenv/v2@latest

なにをするツールなのか

zenvは、カレントディレクトリの .env を読み込んで、環境変数をセットしたうえでコマンドを実行するツールです。プロジェクトごとに異なる環境変数を管理する際に、いちいち手動でexportしたり、シェルの設定ファイルを切り替えたりする手間を省くことができます。

例えばAWSのaccess keyとsecretを .env ファイルに書いておくと、それを読み取ってawsコマンドを実行できます。

% cat .env
AWS_ACCESS_KEY_ID=abcdefghijklmn
AWS_SECRET_ACCESS_KEY=********************************
% zenv aws s3 ls

このように、プロジェクトディレクトリに .env ファイルを配置しておくだけで、そのプロジェクト固有の環境変数を自動的に読み込んでコマンドを実行できるため、複数のプロジェクトを並行して開発する際にも混乱することがありません。

v0〜v1系では、単純に .env 上の 変数名=値 という形式だけでなく、実際の開発現場で必要になったいろいろな特殊機能を追加していきました。それはそれで便利だったのですが、機能追加を重ねるうちに独自記法が複雑になりすぎたこと、また他ツールとの互換性が失われてきたことから、破壊的変更を伴う修正が必要と感じたためv2にすることにしました。

v2での改修

.env はシンプルな使い方に限定

.env ファイルはシンプルに 環境変数名=値 のみに限定しました。v1系で独自記法を追加した結果、他の .env 管理ツール(例えば dotenv など)との互換性が失われてしまったという反省があります。そのため、v2では .env ファイルは標準的な記法のみをサポートすることにしました。これによって、ほかツールとの互換もしやすくなり、zenvを使わない環境でも同じ .env ファイルを流用できるようになります。

一方で、後述する .env.yaml と共存することも可能です。そのため、シンプルに記載するものは .env、複雑な処理が必要なものだけ .env.yaml に書くという使い分けができます。例えば、チームで共有するような基本的な環境変数は .env に記載し、個人の開発環境特有の複雑な設定だけを .env.yaml に書くといった運用が考えられます。

複雑な記法は .env.yaml で記述するように

.env.yaml では複雑な機能が使えるようになっています。基本的には 変数名: 値 というシンプルな記述もできますが、それ以外に 変数名: map型 で詳細な設定を行うことができます。

主な機能は以下の通りです。

(1) ファイルからの値読み込み

APIキーや秘密鍵など、ファイルとして保存されている値を環境変数として読み込むことができます。例えば、Google Cloudのサービスアカウントキーやデータベースのパスワードファイルなどを指定できます。

(2) 他の環境変数を使って値の組み立て

text/template の記法を利用して、既存の環境変数を組み合わせて新しい環境変数を作ることができます。例えば、DB_USERDB_PASSWORDDB_HOST から接続文字列を組み立てるといった用途に使えます。

(3) コマンドを実行して値を取得

外部コマンドを実行した結果を環境変数として設定できます。これは特に、クラウドサービスの認証トークンを取得する際に便利です。例えば gcloud auth print-identity-token を実行してGoogle CloudのID Tokenを取得し、それをAPIリクエストの認証に使うといったケースで活用できます。

(4) profile指定時に別の設定に上書き

開発環境、ステージング環境、本番環境など、環境ごとに異なる設定を切り替えることができます。zenv -p dev のようにprofileを指定することで、特定の環境変数を上書きできます。(下記例では DATABASE_URLsqlite://local.db に上書きされます)

DB_USER: "admin"
DB_HOST: "localhost"

DB_PASSWORD:
  file: "/path/to/db_secret"  # Load from file

API_KEY:
  file: "/path/to/api_key"

DATABASE_URL:
  value: "postgresql://{{ .DB_USER }}:{{ .DB_PASSWORD }}@{{ .DB_HOST }}/mydb"
  refs:
    - DB_USER
    - DB_PASSWORD
    - DB_HOST  # Build from variables
  profile:
    dev: "sqlite://local.db"  # Override with profile

CONFIG_DATA:
  command:
    - curl
    - -H
    - "Authorization: Bearer {{ .API_KEY }}"
    - https://api.example.com/config
  refs:
    - API_KEY  # Fetch data from API

秘匿値管理機能は一旦除外した

v1系にはmacOSのkeychainを利用した秘匿値管理機能がありましたが、v2では一旦除外しました。詳細は後述しますが、OS依存の問題や実際の利用状況を踏まえての判断です。秘匿値の管理については、前述のファイルからの読み込み機能を使って、別途管理している秘密情報ファイルを参照する形でも十分対応できるかと考えています。

余談: なぜv1は十分ではなかったか

.env 記法の複雑化

実際の開発現場で使っていると、いろいろな機能が必要になってきました。そのため、他の環境変数の使いまわし、ファイルからの環境変数読み出し(鍵ファイルなど)、コマンドを実行しての環境変数取得(例: gcloud auth print-identity-token によるID Token取得)といった機能を追加していきました。これらの機能自体は便利だったのですが、結果として独自記法がひどくなってしまいました。

例えば、v1系では以下のような記法を使っていました。

GOOGLE_OAUTH_DATA=&tmp/client_secret_00000-abcdefg.apps.googleusercontent.com.json
GOOGLE_TOKEN=`gcloud auth print-identity-token`

& でファイルパスを指定したり、バッククォートでコマンド実行を表現したりと、確かに雰囲気でわからなくもないかもしれません。しかし、これでは標準的な .env ファイルの記法から大きく逸脱しており、やはりあまり親切ではありません。

結果として、他ツール(例えば dotenv など)との互換性がなくなってしまいました。例えば、Node.jsの開発で dotenv パッケージを使いたい場合、zenv用に書いた .env ファイルをそのまま使うことができない可能性がありました。これは .env ファイルの可搬性という観点からやや問題でした。

keychain SDKの問題

v1系には、macOSのkeychainを利用した秘匿値管理機能がありました。APIキーやトークンなどの機密情報をkeychainに保存し、必要に応じて取り出すことができる仕組みです。macOSのkeychainは暗号化されており、システムレベルでの保護が期待できるため、当初は良いアプローチだと考えていました。

しかし、実際に運用していく中でいくつかの問題が見えてきました。

macOSセキュリティの厳格化

macOSのセキュリティが年々厳しくなり、新しいバイナリにアップデートすると全ての秘匿値取り出しに再認証が必要となるようになりました。セキュリティの観点からは正しい挙動なのですが、開発ツールとしての利便性はやや下がります。zenvを更新するたびに、保存している秘匿値の数だけ認証ダイアログが表示されるのは、かなりストレスでした。

ビルドの不安定性

技術的な問題として、keychain SDKのビルドがよく失敗するという課題もありました。https://github.com/keybase/go-keychain を使っていたのですが、おそらくxcode関連の依存関係の問題で、環境によってはビルドが通らないことがありました。単に .env から値を呼び出したいだけなのに、xcodeのバージョンアップデート後にビルドエラーで悩まされることがあり、本末転倒な状態でした。

実際の利用状況

さらに、zenvを利用してくれている方の使い方を観察してみても、この機能はあまり使われていませんでした。多くの場合、秘匿値は別の管理ツール(1Passwordなど)で管理されており、必要に応じてファイルとして書き出して読み込むという運用の方が一般的だったようです。

これらの理由から、v2では一旦この機能を除外し、今後は利用者からの要望や、他のvaultサービス(1Password、HashiCorp Vaultなど)との統合可能性を検討していくつもりです。

まとめ

ということで(あらためて)環境変数の管理が少しでも楽になれば幸いです。feature requestも可能な範囲で対応していきたいと思うので、気軽にissueあげてもらえれば嬉しいです。

Discussion