Azure Functions での環境変数の切り替え

Azure Functions を使うときに、C# をつかうと、特に便利だ。世界最強の統合 IDE が Functions を使うときでも使うことができる。しかも、デバッグもできるし、デプロイも出来るので超便利だ。

さて、いま、Azure Functions を活用したプロジェクトにいるのだが、環境変数の切り替えみたいなものはどのようにマネージしたらいいだろうか？プロダクション環境、ステージング環境、テスト環境みたいなもの。

12 factors に従うと、環境変数ということになるが、それをどのようにマネージするのがよさげか？という話である。実はここらへんもうまい仕組みがあるので、それを活用していきたので、その知見をシェアしたい。

# 1. Visual Studio で Azure Functions を作成する

まずは簡単なプログラムを書いてみよう。 Azure Functions のテンプレートを使うと、デフォルトで、`local.settings.json` というファイルが生成される。こちらは、Azure Functions をローカル実行するときのコンフィグを書くことができる。

```csharp
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using System.Configuration;

namespace ConfigurationSpike
{
    public static class ConfigFunction
    {
        [FunctionName("ConfigFunction")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            log.Info("C# HTTP trigger function processed a request.");
            var env = ConfigurationManager.AppSettings.Get("Environment");
            var conn = ConfigurationManager.ConnectionStrings["EventHub"].ConnectionString;

            return req.CreateResponse(HttpStatusCode.OK, $"Current Environment: {env} Connection String: {conn}");

        }
    }
}
```

_local.setting.json_

```
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "AzureWebJobsDashboard": "",
    "Environment": "Test"
  },
  "ConnectionStrings": {
    "EventHub": "Endpoint=SomeConnection"
  }
}
```

この`Value` の中の項目は、Environment Variables になる。だから、`Environment.GetEnvironmentVariable(string);` のメソッドで取れるのだが、お勧めしない。お勧めは上記でつかわれているとおり、`ConfigurationManager` を使う方法だ。ConfigrationManager は、本来、`app.config` とかの内容を管理できるものだが、Azure Functions では、`local.settings.json` や、Azure Function を Azure 上にデプロイしたときの、`App Settings` の内容も取得できる。さらに、クラスになっているので、キーの一覧取得とか、必要そうなメソッドがすでに実装されている。

```csharp
var env = ConfigurationManager.AppSettings.Get("Environment");
var conn = ConfigurationManager.ConnectionStrings["EventHub"].ConnectionString;
```

こちらをとって実際に、Azure Functions をローカルで動作させて、実行させてみる。
![Config01.png](https://qiita-image-store.s3.amazonaws.com/0/3470/83d9862a-c00c-5613-36ff-a3c108081416.png)

ブラウザから実行してみると、環境変数も、Connection String もちゃんととれているのがわかる。

![Config02.png](https://qiita-image-store.s3.amazonaws.com/0/3470/0e7a817f-c502-8a4c-98dd-a43e4f1ba6af.png)

ちなみに、変数名とかを間違えると、こんな例外がでるのでびっくりするかもしれないが、` Object reference not set to an instance of an object.` が出たときは、単に、コードから呼ばれる環境変数がみつからないだけだ。

```
<ApiErrorModel xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Azure.WebJobs.Script.WebHost.Models">
<Arguments xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" i:nil="true"/>
<ErrorCode>0</ErrorCode>
<ErrorDetails>
Microsoft.Azure.WebJobs.Host.FunctionInvocationException : Exception while executing function: Functions.ConfigFunction ---> System.NullReferenceException : Object reference not set to an instance of an object. at async ConfigurationSpike.ConfigFunction.Run(HttpRequestMessage req,TraceWriter log) at c:\users\tsushi\Source\Repos\ConfigurationSpike\ConfigurationSpike\ConfigFunction.cs : 19 at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at async Microsoft.Azure.WebJobs.Script.Description.FunctionInvokerBase.Invoke(Object[] parameters) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionInvoker`1.InvokeAsync[TReflected](Object[] arguments) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.InvokeAsync(IFunctionInvoker invoker,Object[] invokeParameters,CancellationTokenSource timeoutTokenSource,CancellationTokenSource functionCancellationTokenSource,Boolean throwOnTimeout,TimeSpan timerInterval,IFunctionInstance instance) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithWatchersAsync(IFunctionInstance instance,IReadOnlyDictionary`2 parameters,TraceWriter traceWriter,ILogger logger,CancellationTokenSource functionCancellationTokenSource) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(??) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(??) End of inner exception at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.ExecuteWithLoggingAsync(??) at async Microsoft.Azure.WebJobs.Host.Executors.FunctionExecutor.TryExecuteAsync(IFunctionInstance functionInstance,CancellationToken cancellationToken) at Microsoft.Azure.WebJobs.Host.Executors.ExceptionDispatchInfoDelayedException.Throw() at async Microsoft.Azure.WebJobs.JobHost.CallAsyncCore(MethodInfo method,IDictionary`2 arguments,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.ScriptHost.CallAsync(String method,Dictionary`2 arguments,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.WebScriptHostManager.HandleRequestAsync(FunctionDescriptor function,HttpRequestMessage request,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.Controllers.FunctionsController.ProcessRequestAsync(HttpRequestMessage request,FunctionDescriptor function,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.Controllers.FunctionsController.<>c__DisplayClass3_0.<ExecuteAsync>b__0(??) at async Microsoft.Azure.WebJobs.Extensions.Http.HttpRequestManager.ProcessRequestAsync(HttpRequestMessage request,Func`3 processRequestHandler,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.Controllers.FunctionsController.ExecuteAsync(HttpControllerContext controllerContext,CancellationToken cancellationToken) at async System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) at async System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.Handlers.SystemTraceHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) at async Microsoft.Azure.WebJobs.Script.WebHost.Handlers.WebScriptHostHandler.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken) at async System.Web.Http.HttpServer.SendAsync(HttpRequestMessage request,CancellationToken cancellationToken)
</ErrorDetails>
<Id>26080856-a5b1-4715-8191-e65e8008ce60</Id>
<Message>
Exception while executing function: Functions.ConfigFunction -> Object reference not set to an instance of an object.
</Message>
<RequestId>9db538e4-0a0b-4fab-ab15-7cbcf30e74e4</RequestId>
<StatusCode>InternalServerError</StatusCode>
</ApiErrorModel>
```

# Azure にデプロイして設定する

私のお勧めは、このような方法で、環境ごとに、この設定ファイルを作ることではない。Azure Functions は様々なサービスを連携させることが多いので、これらの設定ファイルを手でゴリゴリ書いていてはとても面倒だ。しかも、こういうシークレットをがっつりとリポジトリにぶち込むのも抵抗がある。じゃあ、どうするかというと、Azure 上に、このアプリをデプロイして、そこで環境を作ろう。

プロジェクトを右クリックして、Publish で簡単にデプロイできる。

![Config03.png](https://qiita-image-store.s3.amazonaws.com/0/3470/913efa11-9ac4-0a65-e55d-2331fae34f2c.png)

こんな感じで、App Settings を Azure の Azure Functions 上で設定する。

 ![Config04.png](https://qiita-image-store.s3.amazonaws.com/0/3470/fc4eea3e-809b-1011-a2db-e91d22253c71.png)

これが、App Settings

![Config05.png](https://qiita-image-store.s3.amazonaws.com/0/3470/bc9b0b9a-9b7c-c732-7a32-955cabee6e5e.png)

実行してみて、動作確認する。

![Config06.png](https://qiita-image-store.s3.amazonaws.com/0/3470/8e06bb51-1130-6fa9-324a-e26d1fe82b9c.png)

# ローカル実行のために、App Settings を取得する

Azure の方で、実際の Azure Functions の動作確認をしたら、どうするか？というと、[Azure Functions CLI](https://www.npmjs.com/package/azure-functions-cli) というものがあるので、それをインストールしておく。

```
npm i -g azure-functions-cli
```

そして、このコマンドをうてばいい。

```
func azure functionapp fetch-app-settings AZURE_FUNCTION_NAME
```

すると、Azure 上の Azure Functions で設定している内容がごっそりファイルになって落ちてくる。local.settings.json は、暗号化にも、非暗号にも対応しているが、暗号化された結果が返ってくるので、中身がわからなくなっているが、`ConfigrationManager` はこれに対応している。こうしてあげると、ローカルで実行していても、Azure で、Azure Functions を動かしている感じで実行できる。これはいい感じだ。
だから、Azure 上で、環境構築したら、その分だけ、先のコマンドでファイルを落とせばよい。


![Config07.png](https://qiita-image-store.s3.amazonaws.com/0/3470/b22fe085-a34a-3a93-a76c-032228a01359.png)

# セッティングの整理

このように、環境ごとに、Azure Functions を Azure に設定したら、ダウンロードして、リポジトリにぶち込めばよい。名前は、`local.settings.ushio.json` みたいに個人の名前をいれておくといい。それを、local.settings.json を、`.gitignore` に入れておけば完了で、使いたいlocal.settings.json を都度、local.settings.jsonにコピーして使うとよい。

![Config08.png](https://qiita-image-store.s3.amazonaws.com/0/3470/0e3fe96a-8a2c-be68-864a-233aab0dca98.png)

これは、人の名前がついているので、「なんで！」と思うかもしれないが、理由は、先ほどの暗号化は、各PCのKeyを使うので、該当PCでしか解凍できないようになっているから。

もし、この程度の情報はプライベートのリポジトリだからいいやという人は、平文で、のこして、`IsEncrypted` を false にしておくとよい。プログラムに影響はない。

# おわりに

私もこういう方法を思いつかなかったが、イケてる同僚がこの方法だったので、学んで試してブログにしてみました。

## リソース

* [Azure Functions Tools for Visual Studio](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-vs)
* [Code and test Azure functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local)
* [Configuration Manager Class](https://msdn.microsoft.com/en-us/library/system.configuration.configurationmanager(v=vs.110).aspx)
* [Improve error message for invalid connection property](https://github.com/Azure/azure-webjobs-sdk-script/issues/183)
* [【C#プログラミングレッスン】- 暗号化編 configファイルの暗号化](http://melma.com/backnumber_120830_5796312/)