⚙️

Railsのcredentials.yml.encみたいなことをTypeScriptでやるやつを作った

2024/12/01に公開

本記事はAkatsuki Games Advent Calendarの1日目の記事です。

みなさんはTypeScriptでWebアプリを開発するとき、環境ごとに変化する設定や、APIキーなどの秘密情報はどのように注入していますか?

おそらく、環境変数に設定を記述し、複数の環境がある場合は.env.{development,production,staging}などのように、環境ごとに.envを作成して管理するといったことをしている方が多いと思います。

一方で、Railsではcredentials.yml.encというファイルが作成され、その中に設定をかけるようになっています。機密情報に関しても暗号化されているため、鍵が流出しない限り安全に管理することができます。
また、LaravelでもCredentials Packageのように同様のソリューションが提供されています。

今回、TypeScriptでも、環境変数ではなく暗号化したファイルによって設定を保存する仕組みを実現するためのライブラリを開発したので、開発の背景もふまえて紹介していきます。

開発の動機

環境変数ファイル多すぎ問題

そもそも、なぜTypeScriptで設定ファイルを使用したいと考えるようになったのかというと、環境変数を扱う上で触るべきファイルが多く、設定の更新頻度の高い開発フェーズでは最適ではないなと感じたからです。

例えば、TypeScriptで開発したサーバーコードをCloud Runにデプロイするケースを考えてみましょう。(というか実際にこういうことがあったのですが)

この場合、まずはローカルで扱う環境変数をまとめた.env.localを用意します。
また、ローカルからCloud Runにデプロイするときは--env-vars-fileオプションに環境変数をまとめたyamlを渡すことで、サービスに環境変数を適用できますが、これ用に.env.production.yamlのようなファイルを用意します。
更に、DockerfileでのENVにも環境変数があることを宣言しておきたいので、そこにも使用する環境変数を列挙したいです。

ここまでで3つのファイルに環境変数を反映していますが、これらのファイルは保存する場所も違えば、形式も、目的も異なります。
多くの企業ではGitHub Actionsで特定の手続きのターゲットにとって必要なファイルのみを用意して使うという方法が取られていると思いますが、GitHub Actionsって実は無料じゃなくて、開発者が私しかいない個人開発では無料枠を使い切るリスクが一定あるので、ローカルで完結させる方針をとっており、今の開発ワークフロー上適切ではなさそうです。

node-configはセキュアじゃなさそう

既存のライブラリとして、node-configというものがありました。

https://github.com/node-config/node-config

かなり広く知られているライブラリですが、こちらでは機密情報を暗号化する仕組みが提供されていないようです。
コード理解して拡張するのはかえって時間がかかりそうだったので、1から作ってみることにしました。

なぜTypeScriptでは.envが多用されるのか

The Twelve-Factor App

調査してみたところ、TypeScriptでの開発において.envが推奨される大きな理由の一つに、『The Twelve-Factor App』というドキュメントの影響があるようです。

『The Twelve-Factor App』とは、2011年にHeroku開発者の一人であるAdam Wigginsさんによって執筆されたもので、SaaS開発においての設計や運用の方法論がまとめられています。

設定ファイルの問題点

『The Twelve-Factor App』の『III. 設定』には、下記の様な記述があります。

アプリケーションは時に設定を定数としてコード内に格納する。これはTwelve-Factorに違反している。Twelve-Factorは 設定をコードから厳密に分離すること を要求する。設定はデプロイごとに大きく異なるが、コードはそうではない。

(中略)

設定に対するもう1つのアプローチは、バージョン管理システムにチェックインされない設定ファイルを使う方法である。例として、Railsにおけるconfig/database.ymlがある。この方法は、リポジトリにチェックインされる定数を使うことに比べると非常に大きな進歩であるが、まだ弱点がある。設定ファイルが誤ってリポジトリにチェックインされやすいことと、設定ファイルが異なる場所に異なるフォーマットで散乱し、すべての設定を一つの場所で見たり管理したりすることが難しくなりがちであることである。その上、これらのフォーマットは言語やフレームワークに固有のものになりがちである。

Twelve-Factor Appは設定を 環境変数 に格納する。 環境変数は、コードを変更することなくデプロイごとに簡単に変更できる。設定ファイルとは異なり、誤ってリポジトリにチェックインされる可能性はほとんどない。また、独自形式の設定ファイルやJava System Propertiesなど他の設定の仕組みとは異なり、環境変数は言語やOSに依存しない標準である。
設定管理のもう1つの側面はグルーピングである。アプリケーションは設定を名前付きのグループ(しばしば“環境”と呼ばれる)にまとめることがある。グループは、Railsにおけるdevelopment、test、production環境のように、デプロイの名前を取って名付けられる。この方法はうまくスケールしない。アプリケーションのデプロイが増えるにつれて、新しい環境名(stagingやqa)が必要になる。さらにプロジェクトが拡大すると、開発者はjoes-stagingのような自分用の環境を追加する。結果として設定が組み合わせ的に爆発し、アプリケーションのデプロイの管理が非常に不安定になる。

長く引用してしまいましたが、つまるところ、『The Twelve-Factor App』で問題視しているのは下記と考えられます。

  1. コード内に定数として設定をハードコードする場合、設定をデプロイごとに出し分けられない
  2. 設定ファイルを用いる場合、間違ってリポジトリに設定ファイルがコミットされる可能性がある
  3. 細かい環境ごとの出し分けたいとき、環境ごとに設定を出し分けるのは柔軟さに欠ける

設定ファイルでいいんじゃないか

先に上げた問題点のうち、1. コード内に定数として設定をハードコードする場合、設定をデプロイごとに出し分けられない は多くのエンジニアの間で共有された価値観でしょう。

では、2, 3はどうでしょうか。特に私が先述の課題に遭遇している個人開発においての妥当性を検討してみましょう。

2. 設定ファイルを用いる場合、間違ってリポジトリに設定ファイルがコミットされる可能性がある については、多くのフレームワークでデフォルトで用意される.gitignore.envが入っているなどで一定防がれているのかもしれないですが、基本的には.envも同様のリスクをはらんでおり、どちらかというとより管理しやすくして設定をコミットするリスクを最小化する努力をすることが本質と考えられます。

3. 細かい環境ごとの出し分けたいとき、環境ごとに設定を出し分けるのは柔軟さに欠ける については、異個人開発というケースにおいては考える必要が無いと考えられます。品管部門なぞという贅沢な協力者は私にはいないからです!(涙目)

というわけで、開発者1人、ローカルで開発~コミットまでを行うという、個人開発のシチュエーションにおいては、『The Twelve-Factor App』の方法論を遵守しなくても、同書で提示されている問題を解決、または無視できるのではないかと思います。

今回作ったもの

https://github.com/boke0/typed-secure-config

今回開発した『@typed-secure-config』は、下記の2つの要素から構成されています。

  1. CLIツール: 設定ファイルに暗号化した値を入力したり、環境を追加したりする機能を提供します
  2. ライブラリ: アプリケーションコード内で、設定ファイルを読み込むメソッドを提供します

CLIツール

https://github.com/boke0/typed-secure-config/tree/main/apps/cli

このツールでは、下記の作業を行うことができます。

  1. プロジェクトに設定管理用のディレクトリを初期化する
  2. 環境を追加する
  3. 設定を追加する
  4. 設定を確認する

プロジェクトに設定管理用のディレクトリを初期化する

npx typed-secure-config initを実行すると、設定や、必要なファイル類を管理するディレクトリを対話型インターフェースで作ることができます。

$ npx typed-secure-config init
? Enter the path to place the config file example-config

$ ls
example-config

$ ls example-config
_secrets.json  types.ts

うまくいくと、指定されたディレクトリ内に_secrets.jsontypes.tsが作成されます。
types.tsはアプリケーションコード側で読み込むときに型を提供するためのものですが、別にツールが型生成してくれるとかではないので、自分で書いてください。

// types.ts
export default interface Config {
  // Add your config type definition here
}

環境を追加する

以降も同様にnpx typed-secure-configを用います。先ほど設定したパスは-cフラグに毎度与えていきます。
add-envというサブコマンドを実行すると、対話型インターフェースで新しい環境名を入力するよう要求されるので入力しましょう。

$ npx typed-secure-config -c example-config add-env
Enter the new environment name development

$ ls example-config
_secrets.json  development.json  types.ts

うまくいくと、<入力した環境名>.jsonという空のJSONファイルが作成されます。
また、_secrets.jsonに暗号化キーが保存されます。

// _secrets.json
{
  "development": "8f48c507cac4820f15be1162fdaf74d06bd80d9c6bae80c22fad21dcbb5279b8"
}

設定を追加する

setサブコマンドを実行すると、これまた対話的に値を設定できます。

$ npx typed-secure-config -c example-config set
✓ Select an environment development
✓ Enter a key (split by "." to nest) mysql.host
✓ Enter a value localhost:3306

うまくいくと、次のように先ほど生成されたJSONに反映されます。

{
  "mysql": {
    "host": "localhost:3306"
  }
}

機密情報に関しては、set-encryptというサブコマンドを用いて設定できます。

$ npx typed-secure-config -c example-config set-encrypt
✓ Select an environment development
✓ Enter a key (split by "." to nest) mysql.password
✓ Enter a value password

set-encryptを使用すると、_secrets.jsonに記述された鍵を用いて暗号化された暗号文と、IVが環境のJSONに反映されます。

{
  "mysql": {
    "host": "localhost:3306",
    "password": {
      "_encrypted": "229b955302c3b7737e3837a486b4a8f1",
      "_iv": "84c2c164efc60a48d87436e06c8249bd"
    }
  }
}

ライブラリ

https://github.com/boke0/typed-secure-config/tree/main/packages/core

ライブラリはnpmなどを用いて別途インストールしましょう。

npm i @typed-secure-config/core

TypeScript向けのDIライブラリであるtsyringeを用いて、環境変数を注入するコード例を紹介します。

@injectable()
class DataBase {
  constructor(
    @inject("Config") private config
  ) {}
  ...
}

const config = await typedSecureConfig({
  directory: path.resolve("src/config/"),
  file: process.env.NODE_ENV + ".json",
  encryptionKey: process.env.SECURE_CONFIG_KEY,
})
container.registerInstance("Config", decryptConfigObject)

コード内では、鍵を環境変数から渡して上げるとよいかと思います。「結局環境変数使ってますやん」と思うかもしれないですが、上記のコードで必要となる環境変数はNODE_ENVとSECURE_CONFIG_KEYのみで、それ以外はすべてtyped-secure-configで管理すれば良くなるので、変数の削除時や追加時に必要な操作は、環境変数を扱うファイルの数だけ少なくなります。

まとめ

今回、TypeScriptで環境変数ではなく設定ファイルを用いた安全な設定の管理を行うライブラリとツールを作成しました。
また、TypeScriptにおいて、設定の出し分けが設定ファイルで行われる機会が少ない理由を調査し、個人開発におけるこの方針の是非を検討しました。
環境ごとの細かいパッチをあてたり、リポジトリに反映しないローカル専用のconfigを作成する機能などを用意すれば、『The Twelve-Factor App』の要求も満たせるかもしれないので、検討の余地がありそうです。

とりあえず作っているものにさっさと適用したかったので、ドキュメントやテストはほとんど内に等しいのですが、良かったらリポジトリを見に行っていただけると嬉しいです。PRやIssueもお待ちしています!

Discussion