ElixirでAWS LambdaでGithubActionsで継続デプロイ

# はじめに

「Elixirで簡単にAWS Lambdaの開発ができるようになれば、もっとElixirが流行るのでは？」

そんな想いから、Elixirで実装されたAWS Lambdaの関数をGithubにプッシュすると、Github Actionsが自動でビルドしAWSにデプロイしてくれる仕組みを試してみました。

# ElixirでAWS Lambda

AWSの公式としてElixir用のAWS Lambdaランタイムは用意されていないので、ElixirでAWS Lambdaを動かすためには、Elixirで実装したコードを動かすためのカスタムのAWS Lambdaランタイムを用意する必要があります。

また、Elixirでは1.9から`mix release`でElixirやErlang不要で実行可能なパッケージを作成することができるのですが、このパッケージを実行するためには、パッケージを実行する環境で作成する必要があり、AWS Lambdaの場合は、Amazon Linuxの環境でビルドする必要があります。

つまり、ElixirでAWS Lambdaを動かす場合、Amazon Linuxの環境にてElixirの実行可能なパッケージの作成、パッケージとbootstrapを含めたZipファイルの作成、作成されたZipファイルをAWS Lambdaに関数登録、という手順を踏むことになります。

この辺りを実現するライブラリやDockerイメージについては既に作成・公開しているものがあるので、今回はそちらを利用して試してみます。

ライブラリはこちらで公開
→　https://github.com/imahiro-t/faas_base
→　https://hex.pm/packages/faas_base

## ハンドラの実装

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

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

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

```
$ mix deps.get
```

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

```elixir
defmodule Upcase do
  use FaasBase, service: :aws
  alias FaasBase.Logger
  alias FaasBase.Aws.Request
  alias FaasBase.Aws.Response
  @impl FaasBase
  def init(context) do
    # call back one time
    {:ok, context}
  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
```

LambdaのインプットはJson形式にする必要があり、渡されたJson形式のデータはMap型として引数のeventに展開され、環境変数等はMap型としてcontextに展開されます。

戻り値は{:ok, String.t}か{:error, String.t}のように文字列のみを返すか、{:ok, FaasBase.Aws.Response.t}か{:error, FaasBase.Aws.Response.t}のようにヘッダー情報やステータスコードを含めた情報を返す必要があります。

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

```
$ iex -S mix
```

```
iex> alias FaasBase.Aws.Request
iex> Upcase.handle(%Request{body: "{\"data\": \"hello\"}"}, %{}, %{})
{:ok,
 %FaasBase.Aws.Response{
   body: "{\"DATA\":\"HELLO\"}",
   headers: %{},
   status_code: 200
 }}
```

問題なさげです。

## Zipファイルの作成

ビルド用のDockerイメージが既にあるので、下記のコマンドで簡単にZipファイルを作成することができます。

```
$ mkdir -p _build
$ docker run -d -it --rm --name elx erintheblack/elixir-lambda-builder:al2_1.16.2
$ docker cp mix.exs elx:/tmp
$ docker cp lib elx:/tmp
$ docker exec elx /bin/bash -c "mix deps.get; MIX_ENV=prod mix aws.release"
$ docker cp elx:/tmp/_aws ./_build
$ docker stop elx
```

## 関数の登録

下記コマンドでAWS Lambdaに関数を登録します。

```
$ cd ./_build/_aws
$ aws lambda create-function \
    --function-name my-function \
    --runtime provided.al2 \
    --zip-file fileb://upcase-0.1.0.zip \
    --handler MyApp \
    --role arn:aws:iam::XXXXXXXX:role/XXXrole_for_lambdaXXX
```

## 動作確認

下記コマンドで動作確認してみます。

```
$ aws lambda invoke \
    --function-name my-function \
    --payload '{ "data": "hello" }' \
    response.json; cat response.json

{
    "ExecutedVersion": "$LATEST", 
    "StatusCode": 200
}
{"data":"HELLO"}
```

問題なくLambda関数が実行されました。

# Github Actionsで継続デプロイ

Github ActionsではGithubに対する各種イベントをトリガーとしてアクションを設定することができるので、今回はmasterにプッシュされた時にソースをビルドしAWSにデプロイする設定を行ってみます。

AWSへのデプロイはserverlessを利用します。

## serverless.ymlの作成

serverlessを実行するための`./serverless.yml`は下記のようになります。

```yaml
service: upcase

provider:
  name: aws
  runtime: provided.al2
  region: ap-northeast-1

package:
  artifact: ./upcase-0.1.0.zip

functions:
  upcase:
    handler: Upcase
    environment:
      LOG_LEVEL: "INFO"
    memorySize: 128
    timeout: 10
```

カスタムランタイムなので`runtime`に`provided`を指定します。環境変数に`LOG_LEVEL`を`DEBUG`, `INFO`, `WARN`, `ERROR`で設定することができ、ライブラリのログ出力のレベルを指定することができます。

## ワークフローの作成

ワークフローの`.github/workflows/main.yml`は下記のようになります。

```yaml
name: CI

on:
  push:
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Pull Docker Image for Build
      run: |
        docker run -d -it --name elx erintheblack/elixir-lambda-builder:al2_1.16.2

    - name: Build
      run: |
        docker cp mix.exs elx:/tmp
        docker cp lib elx:/tmp
        docker exec elx /bin/bash -c "mix deps.get; MIX_ENV=prod mix aws.release"
        docker cp elx:/tmp/_aws/upcase-0.1.0.zip .
        docker stop elx

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'

    - name: Deploy to Lambda
      uses: serverless/github-action@v3.2
      with:
        args: deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

ビルド用のDockerをプルしてビルドを行い、serverlessのインストールをしてデプロイします。

`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`についてはGithubのシークレットに登録してそちらを参照するようにします。

## 動作確認

ソースコードをプッシュするとGithub Actionsが動き出し、AWSにデプロイが行われます。

# さいごに

ライブラリやGithub Actionsを利用することで、開発者はハンドラの実装にのみ注力することができるようになりました。

ElixirでのLambdaの開発はベストプラクティスとは言い難いかもですが、Elixirでコードを書くこと自体が楽しいので個人的には満足です。