はじめに
※使用ライブラリを、AWS Lambda
, Azure Functions
, IBM Cloud Functions
で使える共通ライブラリにしたので、記事を若干修正しました。(2021/07/07)
※Azure Functions Core Tools V4に対応しました(2023/06/24)
以前、AWS LambdaのアプリケーションをElixirで簡単に実装できるライブラリを作ったときに、「ElixirでAWS LambdaでGithubActionsで継続デプロイ」という投稿でAWS Lambdaへのデプロイまでを紹介させていただきました。
今回、Azure FunctionsのアプリケーションをElixirで簡単に実装できるライブラリを作ってみたので、それを使ってAzure Functionsへのデプロイまでをやってみたいと思います。
ライブラリはこちらで公開
→ https://github.com/imahiro-t/faas_base
→ https://hex.pm/packages/faas_base
ElixirでAzure Functions
Azure Functionsでは2020年末にカスタムハンドラーがGAになり、今までサポートされていなかった言語を使ってのAzure Functionsのアプリケーション開発が可能になりました。
ざっくりとした要件としては、Azure Functionsのランタイム上で稼働するwebサーバを使いたい言語で立ち上げて、ある一定のルールでランタイムとやりとりするというもので、ElixirであればCowboyをベースに作成するのが簡単かなと思います。
webサーバの立ち上げやAzure Functionsのランタイムとのやりとりについては、作成したライブラリ側で吸収しているので、今回はこの辺りをあまり意識せずにデプロイまでやっていくことになります。
やってみる
Azure Functions Core Toolsのインストール
Azure Functionsをローカル環境で試してみたり、デプロイしたりできるツールです。ローカル環境はMacなので下記のような感じでインストールしておきます。
brew tap azure/functions
brew install azure-functions-core-tools@4
Azure環境の作成
azureの管理画面にてリソースグループを作成し、関数アプリ(Azure Functions)を作成します。
作成の際には、ランタイム スタックをCustom Handler
に、オペレーティング システムをLinux
にして作成するようにしてください。
アプリケーション作成
まずはmix new upcase
とプロジェクトを作成し、mix.exs
にAzure Functions用のライブラリを追加し、アプリケーションとして起動します。
def application do
[
mod: {FaasBase.Azure.Application, []}
]
end
defp deps do
[
{:faas_base, "~> 1.1.1"}
]
end
$ mix deps.get
次にupcase.ex
に下記のようにハンドラを実装します。(サンプルとして小文字を大文字にするだけの簡単な関数アプリです)
defmodule Upcase do
use FaasBase, service: :azure
alias FaasBase.Logger
alias FaasBase.Azure.Request
alias FaasBase.Azure.Response
@impl FaasBase
def init(_context) do
# call back one time
:ok
end
@impl FaasBase
def handle(%Request{body: body} = request, event, context) do
Logger.info(request)
Logger.info(event)
Logger.info(context)
{:ok, Response.to_response(body |> String.upcase, %{}, 200)}
end
end
AzureFunctionsのランタイムからはリクエスト情報が格納されたevent
と環境変数等の情報が格納されたcontext
がMap型で渡ってきます。
第一引数のrequest
にはevent
情報をbody
やらheaders
やらmethod
やらを扱いやすいように展開して格納しています。
戻り値は{:ok, String.t}か{:error, String.t}のように文字列のみを返すか、{:ok, FaasBase.Azure.Response.t}か{:error, FaasBase.Azure.Response.t}のようにヘッダー情報やステータスコードを含めた情報を返す必要があります。
例えばmethodがOPTIONSのレスポンスでCORSのヘッダー情報を返したい場合などは下記のように記述します。
headers = %{
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "POST, GET, OPTIONS",
"Access-Control-Allow-Headers" => "X-PINGOTHER, Content-Type"
}
{:ok, Response.to_response("", headers, 200)}
試しにコンソールでハンドラの動作確認をしてみます。
$ _HANDLER=Upcase iex -S mix
iex> alias FaasBase.Azure.Request
iex> Upcase.handle(%Request{body: "{\"data\": \"hello\"}"}, %{}, %{})
{:ok,
%FaasBase.Azure.Response{
body: "{\"DATA\":\"HELLO\"}",
headers: %{},
status_code: 200
}}
問題なさげです。
デプロイ
Azure Functionsの実行環境はLinuxになるので、ビルドは、Linux上で行う必要があります。ライブラリには、ビルド用のmixタスクが用意されていて、こちらを実行すると、ビルドとともに、_build/_azure
にデプロイに必要な情報が作られます。
$ handle_module=Upcase
$ method_name=upcase
$ mkdir -p _build
$ docker run -d -it --rm --name elx erintheblack/elixir-azure-functions-builder:1.10.3
$ docker cp lib elx:/tmp
$ docker cp mix.exs elx:/tmp
$ docker exec elx /bin/bash -c "mix deps.get; MIX_ENV=prod mix azure.release ${handle_module} ${method_name} 'get post'"
$ docker cp elx:/tmp/_azure ./_build
$ docker stop elx
デプロイします。
$ cd ./_build/_azure
$ func azure functionapp publish ${functionName} --publish-local-settings --overwrite-settings
functionName
には関数アプリ作成時の関数名を指定します。
デプロイが成功すると関数のエンドポイントとなるURLが表示されるのでメモしておきます。
Functions in ********:
upcase - [httpTrigger]
Invoke url: https://********.azurewebsites.net/api/upcase?code=u8UF9689a/vHGu1Zd36Fzi0qb5YfHJgKxiZ6jJzHLsP9689X1AZkwQ==
動作確認
curlで叩いて正常に動作することを確認します。
$ curl -X post -H 'content-type:application/json' -d '{"data": "hello"}' https://********.azurewebsites.net/api/upcase?code=u8UF9689a/vHGu1Zd36Fzi0qb5YfHJgKxiZ6jJzHLsP9689X1AZkwQ==
{"data":"HELLO"}
さいごに
AWS LambdaでElixirを動かすのは、実際に運用していたりするので、多少は実績があるのですが、Azure Functionsは運用実績もなく、どこまでできるのか、まだまだ未知数です。
Azure Functionsでは、httpのトリガーだけでなくタイマーや他のトリガーも豊富みたいなので、その辺りの組み合わせや、Azure StorageやAzure Cosmos DBとの連携なども試してみたいと思います。