Posted at

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

More than 1 year has passed since last update.

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

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

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


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

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

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 の内容も取得できる。さらに、クラスになっているので、キーの一覧取得とか、必要そうなメソッドがすでに実装されている。

var env = ConfigurationManager.AppSettings.Get("Environment");

var conn = ConfigurationManager.ConnectionStrings["EventHub"].ConnectionString;

こちらをとって実際に、Azure Functions をローカルで動作させて、実行させてみる。

Config01.png

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

Config02.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

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

Config04.png

これが、App Settings

Config05.png

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

Config06.png


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

Azure の方で、実際の Azure Functions の動作確認をしたら、どうするか?というと、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


セッティングの整理

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

Config08.png

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

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


おわりに

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


リソース