🐷

BanditとPlugによるElixir製のシンプルなWebアプリケーションをfly.ioにデプロイする

2022/03/11に公開

はじめに

ElixirでWebアプリケーションを作る際には、まずはPhoenixの利用を検討することになると思います(私を含むElixirコミュニティの有志で執筆したPhoenix入門記事が最近刊行されました。「WEB+DB PRESS Vol.127にElixirとPhoenixの特集を寄稿しました」をぜひご覧ください)。一方で、普通のWebアプリケーションならそれでいいのですが、たとえばバックエンドのシステムに対するHTTPインタフェイスを外部に提供したいだけというような場合、ややオーバースペックであるような気もします。

そこで、BanditというPlugアプリケーション用のHTTPサーバ実装を用いて、シンプルに実現してみるという選択肢があり得ます。というか、ElixirConf 2021 - Mat Trudel - bandit on the loose! Networking in Elixir demystified - YouTubeで話題になったライブラリなので、使ってみたいという気持ちの方が大きかったりします。

そんなわけで、この記事では、Banditを用いて簡単なアプリケーションを作成し、fly.ioというPaaSにデプロイするところまでやってみたいと思います。

プロジェクトの準備

今回、検証用にbandit_flyというmixプロジェクトを作成します。

$ mix new bandit_fly --sup

このプロジェクトを用いて検証したコードは、kentaro/bandit_flyに置いてあります。

fly.ioの実行環境の作成と設定

Dockerfileの準備

Deploy an Elixir Phoenix Applicationでは、Phoenixアプリケーションのデプロイについてドキュメントされていますが、今回はPhoenixを使わずBanditだけでアプリケーションを作るので、Dockerfileでビルド用の設定を用意するようにします。

Dockerfileは「Elixirアプリケーションのコンテナをできるだけダイエットした話」からいただいてきました。

FROM elixir:latest

WORKDIR /app
ENV MIX_ENV=prod

RUN mix local.hex --force && \
  mix local.rebar --force

COPY mix.exs mix.exs
COPY mix.lock mix.lock
RUN mix deps.get --only prod && \
  mix deps.compile

COPY . .
CMD ["mix", "run", "--no-halt"]

実行環境の作成

fly launchで、このアプリケーション用の実行環境を作成します。コマンドのインストールについては、Installing flyctlの通りです。

$ fly launch
An existing fly.toml file was found for app dark-snow-3104
? Would you like to copy its configuration to the new app? No
Creating app in /Users/kentaro/src/github.com/kentaro/bandit_fly
Scanning source code
Detected a Dockerfile app
? App Name (leave blank to use an auto-generated name):
Automatically selected personal organization: Kentaro Kuribayashi
? Select region: nrt (Tokyo, Japan)
Created app long-wood-6631 in organization personal
Wrote config file fly.toml
? Would you like to setup a Postgresql database now? No
? Would you like to deploy now? No
Your app is ready. Deploy with `flyctl deploy`

上記を実行すると、最後のデプロイするか聞かれますが、準備が終わっていないのでNoを選択します。実行が終わると、fly.tomlというファイルができています。

実行環境の設定

Flyへのデプロイ|Phoenix v1.6 hexdocs 日本語訳」を参考にしつつ、編集しました。コメントで変更内容が書かれています。

# fly.toml file generated for long-wood-6631 on 2022-03-09T18:48:53+09:00

app = "long-wood-6631"

kill_signal = "SIGTERM"        # SIGINTからSIGTERMへ変更
kill_timeout = 5
processes = []

[env]
  PORT = "8080"                # ↓で設定した8080番ポートを環境変数に設定

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8080         # Elixir側のサーバで待ち受けるHTTPのポートを設定
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    handlers = ["http"]
    port = 80

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "30s"       # スタートアップ時のために長くしておく
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

Banditを用いたWebアプリの作成

mix.exs{:bandit, "~> 0.4.3"}を足して、いつも通りmix deps.getします。

$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  bandit 0.4.7
  hpax 0.1.1
  mime 2.0.2
  plug 1.13.3
  plug_crypto 1.2.2
  telemetry 1.0.0
  thousand_island 0.5.3
All dependencies are up to date

lib/bandit_fly/application.exを以下の通り変更します。ポート番号の設定では、前述のfly.tomlで設定しておいたポート番号を環境変数経由で設定できるようにします。

diff --git a/lib/bandit_fly/application.ex b/lib/bandit_fly/application.ex
index bae531e..257469d 100644
--- a/lib/bandit_fly/application.ex
+++ b/lib/bandit_fly/application.ex
@@ -10,6 +10,7 @@ defmodule BanditFly.Application do
     children = [
       # Starts a worker by calling: BanditFly.Worker.start_link(arg)
       # {BanditFly.Worker, arg}
+      {Bandit, plug: BanditFly.Plug, scheme: :http, options: [port: String.to_integer(System.get_env("PORT"))]}
     ]
 
     # See https://hexdocs.pm/elixir/Supervisor.html

BanditはPlug APIをサポートしているので、Plugアプリケーションがそのまま動きます。ここでは、Plug.Routerを使ってサクッと書いておきましょう。

defmodule BanditFly.Plug do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/" do
    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, "Hello, Fly.io!")
  end

  match _ do
    send_resp(conn, 404, "404 Not Found")
  end
end

fly.ioへのデプロイ

ここまできたら、あとはfly deployコマンドでデプロイするだけです(以下は、1度試した後の2度目のデプロイなので、1度目とは表示がやや異なります)。

$ fly deploy
==> Verifying app config
--> Verified app config
==> Building image
Remote builder fly-builder-weathered-fire-1284 ready
==> Creating build context
--> Creating build context done
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
[+] Building 24.7s (0/1)
[+] Building 1.5s (11/11) FINISHED
 => [internal] load remote build context                                                                                 0.0s
 => copy /context /                                                                                                      0.3s
 => [internal] load metadata for docker.io/library/elixir:latest                                                         1.2s
 => [1/7] FROM docker.io/library/elixir:latest@sha256:419d048ecb93be00989db72a56d51b5ecda75bd677bde451a64713fb6f54e22b   0.0s
 => CACHED [2/7] WORKDIR /app                                                                                            0.0s
 => CACHED [3/7] RUN mix local.hex --force &&   mix local.rebar --force                                                  0.0s
 => CACHED [4/7] COPY mix.exs mix.exs                                                                                    0.0s
 => CACHED [5/7] COPY mix.lock mix.lock                                                                                  0.0s
 => CACHED [6/7] RUN mix deps.get --only prod &&   mix deps.compile                                                      0.0s
 => CACHED [7/7] COPY . .                                                                                                0.0s
 => exporting to image                                                                                                   0.0s
 => => exporting layers                                                                                                  0.0s
 => => writing image sha256:609465eda9d60dcec9d6b6258dfafa56eaf9239860c97bad30ea8bb605e9397a                             0.0s
 => => naming to registry.fly.io/long-wood-6631:deployment-1646987815                                                    0.0s
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/long-wood-6631]
2c7bdae6865e: Layer already exists
535e94b0f39a: Layer already exists
06e5c936f723: Layer already exists
eb09693757fb: Layer already exists
ba15aa3f2ff2: Layer already exists
6d673b0cb0fd: Layer already exists
f07fb97dae2e: Layer already exists
38d7a71f498a: Layer already exists
69511e6347b4: Layer already exists
6c6a49f72778: Layer already exists
316e3949bffa: Layer already exists
e3f84a8cee1f: Layer already exists
48144a6f44ae: Layer already exists
26d5108b2cba: Layer already exists
89fda00479fc: Layer already exists
deployment-1646987815: digest: sha256:be3538a436125506e9f227ac51ed2d38ed704ed9e9e2094e2ee2da1e5f57fdee size: 3475
--> Pushing image done
image: registry.fly.io/long-wood-6631:deployment-1646987815
image size: 1.3 GB
==> Creating release
--> release v2 created

--> You can detach the terminal anytime without stopping the deployment
==> Monitoring deployment

 1 desired, 1 placed, 1 healthy, 0 unhealthy
--> v2 deployed successfully

デプロイが成功したら、fly openでWebアプリにアクセスしてみましょう。

$ fly open

ブラウザが開いて、Webアプリの動作確認ができました。

おわりに

やっていることは、普通にDockerコンテナにパッケージングしたアプリケーションを動かすようにしているだけなので、特に面白味のある内容ではありません。しかし、BanditとPlug.RouterでサクッとWebアプリを書いて、デプロイも楽にできるのはけっこういい感じなのではないかと思います。

Discussion