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 をローカルで動作させて、実行させてみる。
ブラウザから実行してみると、環境変数も、Connection String もちゃんととれているのがわかる。
ちなみに、変数名とかを間違えると、こんな例外がでるのでびっくりするかもしれないが、 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 で簡単にデプロイできる。
こんな感じで、App Settings を Azure の Azure Functions 上で設定する。
これが、App Settings
実行してみて、動作確認する。
ローカル実行のために、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 上で、環境構築したら、その分だけ、先のコマンドでファイルを落とせばよい。
セッティングの整理
このように、環境ごとに、Azure Functions を Azure に設定したら、ダウンロードして、リポジトリにぶち込めばよい。名前は、local.settings.ushio.json
みたいに個人の名前をいれておくといい。それを、local.settings.json を、.gitignore
に入れておけば完了で、使いたいlocal.settings.json を都度、local.settings.jsonにコピーして使うとよい。
これは、人の名前がついているので、「なんで!」と思うかもしれないが、理由は、先ほどの暗号化は、各PCのKeyを使うので、該当PCでしか解凍できないようになっているから。
もし、この程度の情報はプライベートのリポジトリだからいいやという人は、平文で、のこして、IsEncrypted
を false にしておくとよい。プログラムに影響はない。
おわりに
私もこういう方法を思いつかなかったが、イケてる同僚がこの方法だったので、学んで試してブログにしてみました。