Azure FunctionsをElixirで

# はじめに

※使用ライブラリを、`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で継続デプロイ](https://qiita.com/erin/items/6d968c94fa7619b726f2)」という投稿で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なので下記のような感じでインストールしておきます。

```bash
brew tap azure/functions
brew install azure-functions-core-tools@4
```

## Azure環境の作成

azureの管理画面にてリソースグループを作成し、関数アプリ（Azure Functions）を作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94455/352057d4-b92b-a668-849b-3d5d3af74a3e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94455/555cfb67-03d4-bbfc-ea4d-73c17bbe48ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94455/c99799f7-0989-d514-3925-eb0473ba6e9c.png)

作成の際には、ランタイム スタックを`Custom Handler`に、オペレーティング システムを`Linux`にして作成するようにしてください。

## アプリケーション作成

まずは`mix new upcase`とプロジェクトを作成し、`mix.exs`にAzure Functions用のライブラリを追加し、アプリケーションとして起動します。

```elixir
  def application do
    [
      mod: {FaasBase.Azure.Application, []}
    ]
  end

  defp deps do
    [
      {:faas_base, "~> 1.1.1"}
    ]
  end
```

```bash
$ mix deps.get
```

次に`upcase.ex`に下記のようにハンドラを実装します。（サンプルとして小文字を大文字にするだけの簡単な関数アプリです）

```elixir
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のヘッダー情報を返したい場合などは下記のように記述します。

```elixir
    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)}
```

試しにコンソールでハンドラの動作確認をしてみます。

```bash
$ _HANDLER=Upcase iex -S mix
```

```bash
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`にデプロイに必要な情報が作られます。

```bash
$ 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
```

デプロイします。

```bash
$ 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で叩いて正常に動作することを確認します。

```bash
$ 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との連携なども試してみたいと思います。