ASP.NET Core / IIS で設定に環境変数を使う

クロスワープの大鷲です。

ASP.NET Core 1.0 がリリースされてから、そろそろ 2 か月になります。
導入は進んでいるでしょうか?
当社でも、まだ少数ながら、ASP.NET Core を本番投入したプロジェクトがあります。

以前の ASP.NET 4.5 とは全く別物と言っていいほどで、変更点をいちいち挙げるとキリがありません。
表面的なコード レベルの話ではなく、方針の転換という点で一つ挙げるならば、環境変数を使うようになったという点は大きいと考えています。

ASP.NET Core のアプリケーション設定

ASP.NET Core 1.0 のスケルトンのコードの中にこんな部分がありますね。

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();

    this.Configuration = builder.Build();
}

以前はアプリケーションの設定ファイルとして web.config を使っていましたが、ASP.NET Core では appsettings.json を使うようになりました。
上記のコードは、

  1. アプリケーションのベース ディレクトリにある appsettings.json
  2. 同ディレクトリにある環境ごとの appsettings.*.json
  3. 環境変数

の 3 つをマージして、アプリケーション設定を構築しています。
後で指定しているものほど優先度が高いので、ベースの設定を環境ごとの json ファイルで上書きし、さらにそれを環境変数の設定で上書きすることができます。

環境ごとの設定ファイル

そうそう、環境ごとの json ファイルというのが導入されたのも大きな変更点です。
これは、

  • appsettings.Development.json
  • appsettings.Staging.json
  • appsettings.Production.json

というようなファイルを用意しておいて、実行時にどのファイルを使うかを決定して動的にマージするという仕掛けです。

これまでは、単一の web.config しかありませんでした。
ソースコード レベルでは web.debug.config とか web.release.config とかがありましたが、それらを元に、ビルド時やデプロイ時に XML Document Transform のような機能を使って、各環境ごとの web.config を事前に生成していたと思います。

ASP.NET Core では、この作業は不要になりました。
デプロイ先のディレクトリにはすべての環境用の appsettings.*.json をただ置いておくだけでよく、事前に面倒な変換をする必要はありません。
実行時に、どのファイルをマージするかが動的に決定されるからです。
これは大変楽になったと言えます。
また、どの環境であっても、デプロイされるファイルセットが同一でよくなったというのもポイントです。

実際にどのファイルを使うかを決定するのには環境変数が使われます。
ASP.NET Core 1.0 では、ASPNETCORE_ENVIRONMENT という環境変数に、環境名をセットしておきます。
この環境名とは、上記のファイル名の一部である

  • Development
  • Staging
  • Production

のことです。
また、これら以外にも任意の名前を使用することができます。

設定の値として環境変数を使う

上記のように環境変数によって読み込むファイルを変えるだけでなく、設定の値そのものを環境変数に持たせることもできます。
例えば、プロジェクト作成時のデフォルトの appsettings.json はこうなっています。

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

ここで、本番環境ではデフォルトのログレベルを Warning 以上に絞りたいとします。
既に見たように、appsettings.Production.json にこう書いても構いません。

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

別の方法として、"Logging:LogLevel:Default" という名前の環境変数の値を "Warning" に設定することでも、同じ効果が得られます。
JSON の階層は ":" で表されます。

ログレベルの設定であれば、appsettings.Production.json に持たせた方が分かりやすいと思います。
しかし、中にはファイルに書きたくない設定もあります。
例えば、データベースの接続文字列とか、何らかの外部のサービスにアクセスするためのトークンといった、機密情報を含むものです。
ファイルに書きたくないというより、機密情報を書いたファイルをソース管理システムにコミットしたくないと言うべきですね。
GitHub などを利用したオープンな開発で、こうした設定ファイルをうっかりコミットすると、機密情報が他者の目に触れることになってしまいます。
AWS などは、アクセスキーが GitHub(の公開リポジトリ)にコミットされたのを見つけると警告を飛ばしてきます。

企業での開発であればプライベート リポジトリを利用するでしょうから、ソース管理に入れてしまってもいいかもしれません。
そのあたりは各企業でのポリシーの問題になりますので、ここでは深入りを避けます。
ともあれ、オープンなリポジトリでの開発であっても、機密情報を利用でき、「うっかりコミット」という事態を根本的に避けられる方法がサポートされたのは良いことです。*1

Azure の場合、こういった機密情報は「アプリケーション設定」に持たるのが一般的です。
直接的に環境変数に持たせるわけではないのですが、デプロイするファイルに含まず、アプリケーション環境に持たせるという点では共通しています。また、結果的に環境変数にも設定されます。

f:id:cw_owashi:20160822160918p:plain

また、Twelve-Factor App でも、設定を環境変数に持たせることが推奨されています。

12factor.net

なお、開発中に利用できる機密情報の保持手段として「ユーザー シークレット」という機能があります。
本稿では詳述しませんので、詳しくは以下の記事などを参考にしてください。

blog.shibayan.jp

で、肝心の環境変数の設定方法は?

ご存知のように、環境変数は 3 つのレベルで設定できます。

  1. マシン単位
  2. ユーザー単位
  3. プロセス単位

アプリケーションの設定を持たせるわけですから、プロセス単位に保持させたいところです。IIS で言えばアプリケーション プール単位ですね。
しかし、これまでは IIS のアプリケーション プールに環境変数を設定することはできませんでした。
ユーザー単位でも、通常は Application Pool Identity という機能を使うため、通常の方法では設定できません(Application Pool Identity を使わず、従来の Windows ユーザーアカウントを使えば可能です)。
唯一簡単に設定できるのはマシン単位なのですが、ASPNETCORE_ENVIRONMENT のような値であればまだしもサーバーごとに設定するのも理解できますが、データベースの接続文字列などは完全にアプリケーション固有ですから、マシン単位の設定とするのは抵抗があります。名前の重複を避ける配慮も必要になりますし。

IIS 10 の方法

喜ばしいことに、IIS 10 からは、applicationHost.config 上で、アプリケーション プール単位の環境変数設定がサポートされます。

Environment Variables <environmentVariables> | Microsoft Docs

個人的にはどうしてこれがもっと早くサポートされなかったのかと思います。Azure Web Apps でアプリケーション設定に機密情報を持たせるのが一般的になった頃にサポートして欲しかった。

しかし、喜んでばかりもいられません。
IIS 10 は Windows Server 2016 世代、つまりまだリリースされていないのです。
正式リリースされるまでは導入できないという方も少なくないのではないかと思います。

web.config を使った方法

ASP.NET Core の web.config はこんなにシンプルになりました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
  </system.webServer>
</configuration>

この、新しく導入された aspNetCore 要素の子要素として、環境変数を設定することができます。
こんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false">
      <environmentVariables>
        <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production"/>
      </environmentVariables>
    </aspNetCore>
  </system.webServer>
</configuration>

しかし、この方法では、デプロイする度に再設定が必要になってしまいます。良い方法ではありません。
個人的には、この方法は過渡期のものと位置づけ、IIS 10 なり他の方法なりが利用可能になった段階でそちらに移るべきだと考えます。

その他の方法

docker を使うという手もあると思います。
コンテナー単位に環境変数を設定することもできるはずなのですが、まだきちんと評価することができていません。
こちらは追って検証したいと思います。

docker も Windows Server 2016 からネイティブ サポートされます。
ますます Windows Server 2016 が待ち遠しいですね。

*1:ソース管理の無視リストに入れるという手もありましたが、運用が面倒でした。