はじめに
「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用のライブラリを追加し、アプリケーションとして起動します。
def application do
[
mod: {FaasBase.Aws.Application, []}
]
end
defp deps do
[
{:faas_base, "~> 1.2.0"}
]
end
$ mix deps.get
次にupcase.ex
に下記のようにハンドラを実装します。(サンプルとして小文字を大文字にするだけの簡単なLambdaです)
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
は下記のようになります。
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
は下記のようになります。
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でコードを書くこと自体が楽しいので個人的には満足です。