Chapter 27

Flyへのデプロイ

koga1020
koga1020
2021.11.23に更新

Flyへのデプロイ

必要なもの

このガイドに必要なものは、動作するPhoenixアプリケーションだけです。簡単なアプリケーションをデプロイする必要がある方は、起動ガイドをご覧ください。

ゴール

このガイドの主な目標は、Fly.io上でPhoenixアプリケーションを動作させることです。

手順

このプロセスをいくつかのステップに分けて、現在の状況を把握できるようにしましょう。

  • Fly CLIのインストール
  • Flyへのサインアップ
  • プロジェクトをFlyに対応させる
  • Flyアプリケーションの作成と設定
  • データベースを用意する
  • デプロイ
  • 役立つFlyリソース

Fly CLIのインストール

こちらにしたがって、Flyプラットフォームのコマンドラインインターフェイスをインストールします。

Flyへのサインアップ

CLIを使ってアカウントのサインアップを行います。

$ fly auth signup

Flyには、データベースを持たないアプリケーションのための無料枠があります。不正利用防止のため、アカウント設定の際にはクレジットカードが必要です。詳しくはpricingのページをご覧ください。

プロジェクトをFlyに対応させる

このガイドでは、Dockerfileを使用して、Flyのデプロイ用のリリースを構築します。内部的にはFlyのネットワークはIPv6を使用しているため、アプリケーションをスムーズに利用するために少しだけ設定を行います。

リリースの使用

リリースを使ったデプロイ のコンテナーのセクションを参照し、アプリケーションを設定します。FlyのドキュメントにElixirアプリケーションのデプロイに関するガイドがありますので、そちらも参考にしてください。

ランタイムの設定

リリースを使ったデプロイの手順で config/runtime.exs ファイルを作成したら、Fly用に設定する準備ができました。

以下の例のように、config/runtime.exs ファイルを更新します。

import Config

if config_env() == :prod do
  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise """
      environment variable SECRET_KEY_BASE is missing.
      You can generate one by calling: mix phx.gen.secret
      """

  app_name =
    System.get_env("FLY_APP_NAME") ||
      raise "FLY_APP_NAME not available"

  config :my_app, MyAppWeb.Endpoint,
    server: true,
    url: [host: "#{app_name}.fly.dev", port: 80],
    http: [
      # Enable IPv6 and bind on all interfaces.
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base

  database_url =
    System.get_env("DATABASE_URL") ||
      raise """
      environment variable DATABASE_URL is missing.
      For example: ecto://USER:PASS@HOST/DATABASE
      """

  config :my_app, MyApp.Repo,
    url: database_url,
    # IMPORTANT: Or it won't find the DB server
    socket_options: [:inet6],
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
end

注意すべき点は、以下の通りです。

  • エンドポイントのホストに FLY_APP_NAME を使用する
  • エンドポイントででPv6バインディングを使用する
  • Repoのソケットオプションに :inet6 を使用する

また、PostgreSQLインスタンスへの接続にTLSをオンにする必要はありません。Fly Private Networkは暗号化されたWireGuardメッシュ上で動作しているため、アプリケーションサーバーとPostgreSQL間のトラフィックは既に暗号化されており、TLSは必要ありません。

リリース用設定ファイルの生成

mix release.init コマンドを使って、./rel ディレクトリにいくつかのサンプルファイルを作成します。

$ mix release.init

ここで必要なのは、rel/env.sh.eex の設定だけです。このファイルは、リリースコマンドを実行するときに使用されます。重要な部分は以下の通りです。

#!/bin/sh

ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=$FLY_APP_NAME@$ip
export ELIXIR_ERL_OPTIONS="-proto_dist inet6_tcp"

ノードの実行時に完全なノード名を使用するように設定します。Flyに割り当てられたIPv6アドレスを取得し、それを $FLY_APP_NAME と一緒にノードの名前に使用します。最後に、BEAMにも inet6_tcp を設定します。

Flyアプリケーションの作成とセットアップ

作成したアプリケーションをFlyに伝えるために、ソースコードのあるディレクトリで fly launch を実行します。これにより、Flyアプリが作成され、設定されます。

$ fly launch

ソースコードがスキャンされ、結果がプリントされると、organizationの入力を求められます。organizationとは、Flyユーザー間でアプリケーションやリソースを共有するための方法です。すべてのFlyアカウントには、personal と呼ばれる個人用のorganizationがあり、自分のアカウントにのみ表示されます。このガイドではそれを選択しましょう。

次に、デプロイ先のリージョンを選択するプロンプトが表示されます。デフォルトでは、あなたにもっとも近い地域が選択されています。これを利用するか、他のリージョンに変更できます。サポートされているリージョンのリストはこちらをご覧ください。

この時点で、flyctl は新しい名前のFly-sideアプリケーションスロットを作成し、設定を fly.toml ファイルに書き込みます。その後、アプリをビルドしてデプロイするように促されます。まだデプロイしないでください。まず、生成された fly.toml ファイルを調整します。

fly.toml のカスタマイズ

fly.toml ファイルには、アプリをデプロイするためのデフォルトの設定が含まれています。使用する名前を指定しない場合は、名前は自動生成されます。

以下は、カスタマイズした fly.toml ファイルの例です。

app = "your-app-name-here"

kill_signal = "SIGTERM"
kill_timeout = 5

[env]

[deploy]
  release_command = "/app/bin/my_app eval MyApp.Release.migrate"

[[services]]
  internal_port = 4000
  protocol = "tcp"

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20

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

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

  [[services.tcp_checks]]
    grace_period = "30s" # allow some time for startup
    interval = "15s"
    restart_limit = 6
    timeout = "2s"

ここでは2つの重要な変更点があります。

  • [deploy] という設定を追加しました。これは、新しいデプロイ時に、データベースのマイグレーションを実行することをFlyに伝えるものです。ここでのテキストは、アプリケーション名に依存します。これはアプリケーションをアップデートした際に作成したモジュールを、リリースに合わせてデプロイするために呼び出すものです。
  • kill_signal には SIGTERM が設定されています。Elixirノードは、OSから SIGTERM を受け取ると、クリーンなシャットダウンを行います。

他にもいくつかの値を調整しました。internal_port がアプリケーションのポートと一致していることを確認してください。

Flyに環境変数を保存する

新しいアプリをデプロイする前に、まずFlyアカウントでいくつかの設定を行います。ソースコードにコンパイルしたくないシークレットは、外部に保存します。たとえば、Phoenixの key base secretなどです。

Elixirには、新しいPhoenix key base secretを生成するmixタスクがあります。これを使ってみましょう。

$ mix phx.gen.secret
REALLY_LONG_SECRET

長いランダムな文字列が生成されます。これをアプリのシークレットとしてFlyに保存しておきましょう。プロジェクトフォルダーでこのコマンドを実行すると、flyctlfly.toml ファイルを使って、どのアプリに値を設定しているかを知ることができます。

$ fly secrets set SECRET_KEY_BASE=REALLY_LONG_SECRET

データベースの準備

ほとんどのElixirアプリケーションはデータベースを使用しますが、デフォルトではPostgreSQLが使用されています。今回のアプリケーションのために、Flyにデータベースを用意しましょう。

$ fly postgres create

データベースの名前をつけるときには、my-app-db のようにします。デフォルトを使うと、小さなデータベースで遊び始めることができます。

次に、このデータベースをアプリケーションに "attach" する必要があります。

$ fly postgres attach --postgres-app my-app-db

データベースがアタッチされると、アプリケーションが必要とするシークレットが作成されます。どのようなシークレットが作成されたかは、この方法で確認できます。

$ fly secrets list

アプリケーションのリリース設定、パッケージ化のためのDockerfileの定義、config/runtime.exsrel/env.sh.eex ファイルの設定、Flyアプリの定義、Flyへのシークレットの保存、データベースのプロビジョニングが完了し、デプロイの準備が整いました!

デプロイタイム!

これでプロジェクトをFly.ioにデプロイする準備が整いました。

$ fly deploy

Note: Apple Silicon (M1)コンピューターでは、dockerはqemuを使ってクロスプラットフォームビルドを実行しますが、これがうまくいかない場合があります。以下のようなsegmentation faultエラーが発生した場合は、ご注意ください。

 => [build  7/17] RUN mix deps.get --only
 => => # qemu: uncaught target signal 11 (Segmentation fault) - core dumped

flyのリモートビルダーを使用するには、--remote-only フラグを追加します。

$ fly deploy --remote-only

デプロイの状態はいつでも確認できます。

$ fly status

アプリのログを確認するには、次のように入力します。

$ fly logs

問題がなければ、Flyでアプリを開きます。

$ fly open

稼働中のノードへのIExシェルの導入

Elixirは、稼働中のプロダクションノードにIExシェルを導入することをサポートしています。すでに rel/env.sh.eex の設定を行っていますので、この手順はとても簡単です。

いくつかの前提条件があります。まず、FlyのマシンへのSSH Shellを確立する必要があります。

このステップでは、あなたのアカウントにルート証明書を設定し、証明書を発行します。

$ fly ssh establish
$ fly ssh issue

SSHが設定されたので、コンソールを開いてみましょう。

$ fly ssh console
Connecting to my-app-1234.internal... complete
/ #

すべてが順調に進んでいれば、マシンにシェルを入れることができます! あとは、リモートのIExシェルを起動するだけです。デプロイメントのDockerfileは、アプリケーションを /app に取り込むように設定されています。ですから、my_app アプリのコマンドは次のようになります。

$ app/bin/my_app remote
Erlang/OTP 23 [erts-11.2.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]

Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(my_app@fdaa:0:1da8:a7b:ac4:b204:7e29:2)1>

これで、ノードにIExシェルが入りました。CTRL+C、CTRL+Cで安全に切断できます。

アプリケーションのクラスター化

ElixirとBEAMには、クラスター化してノード間でシームレスにメッセージをやり取りするという素晴らしい機能があります。このガイドでは、Elixirアプリケーションのクラスター化について説明します。

Flyでクラスターリングを素早く設定するには2つのパートがあります。

  • libcluster のインストールと使用
  • アプリケーションを複数のインスタンスにスケーリングする

libcluster の追加

ここでは、広く採用されているライブラリ libclusterが役に立ちます。

libcluster が他のノードを見つけて接続するために使用できる戦略は複数あります。ここでは、DNSPoll という戦略を使います。

libcluster をインストールしたら、次のようにアプリケーションに追加します。

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    topologies = Application.get_env(:libcluster, :topologies) || [].

    children = [
      # ...
      # setup for clustering
      {Cluster.Supervisor, [topologies, [name: MyApp.ClusterSupervisor]]}
    ]

    # ...
  end

  # ...
end

次のステップでは、topologies の設定を config/runtime.exs に追加します。

  app_name =
    System.get_env("FLY_APP_NAME") ||
      raise "FLY_APP_NAME not available"

  config :libcluster,
    topologies: [
      fly6pn: [
        strategy: Cluster.Strategy.DNSPoll,
        config: [
          polling_interval: 5_000,
          query: "#{app_name}.internal",
          node_basename: app_name
        ]
      ]
    ]

これは、libclusterDNSPoll 戦略を使用して、.internal プライベートネットワーク上で $FLY_APP_NAME を使用する他のデプロイ済みアプリを探すように設定します。

これは、rel/env.sh.eex ファイルが、$FLY_APP_NAME を使ってElixirノードに名前を付けるように設定されていることを前提としています。

クラスター化する前に、複数のインスタンスを用意する必要があります。次はノードインスタンスを追加します。

複数インスタンスの実行

複数のインスタンスを実行するには2つの方法があります。

  1. 1つのリージョンに複数のインスタンスが存在するようにアプリケーションを拡張する
  2. 別のリージョン(複数のリージョン)にインスタンスを追加する。

まず最初に、単一のデプロイメントのベースラインから始めましょう。

$ fly status
...
Instances
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
f9014bf7 26      sea    run     running 1 total, 1 passing 0        1h8m ago

単一リージョンでのスケールアップ

現在のリージョンで2インスタンスにスケールアップしてみましょう。

$ fly scale count 2
Count changed to 2

ステータスを確認すると、何が起こったのかがわかります。

$ fly status
...
Instances
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
eb4119d3 27      sea    run     running 1 total, 1 passing 0        39s ago
f9014bf7 27      sea    run     running 1 total, 1 passing 0        1h13m ago

これで同じリージョンに2つのインスタンスができました。

クラスター化されていることを確認しましょう。ログを見てみましょう。

$ fly logs
...
app[eb4119d3] sea [info] 21:50:21.924 [info] [libcluster:fly6pn] connected to :"my-app-1234@fdaa:0:1da8:a7b:ac2:f901:4bf7:2"
...

しかし、それではノードの中から見るほどの価値はありません。IExシェルから、私たちが接続しているノードに、そのノードがどのような他のノードを見ることができるかを尋ねることができます。

$ fly ssh console
$ /app/bin/my_app remote
iex(my-app-1234@fdaa:0:1da8:a7b:ac2:f901:4bf7:2)1> Node.list
[:"my-app-1234@fdaa:0:1da8:a7b:ac4:eb41:19d3:2"]

接続しているノードのIPアドレスを表示するために、IExプロンプトが含まれています。そして、Node.list を取得すると、もう一方のノードが返されます。これで2つのインスタンスが接続され、クラスター化されました。

複数のリージョンへのスケーリング

Flyは、ユーザーの近くにインスタンスを配置することを容易にします。DNSのマジックにより、ユーザーはアプリケーションが配置されている最寄りのリージョンに誘導されます。Flyのリージョンについてはこちらを参照してください。

ここでは、米国ワシントン州シアトルの sea に1台のインスタンスを配置した状態から、米国ニュージャージー州パーシッパニーの ewr を追加してみましょう。これにより、アメリカの両岸にインスタンスが置かれることになります。

$ fly regions add ewr
Region Pool:
ewr
sea
Backup Region:
iad
lax
sjc
vin

ステータスを見ると、カウントが1に設定されているので、1つのリージョンにしか入っていないことがわかります。

$ fly status
...
Instances
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
cdf6c422 29      sea    run     running 1 total, 1 passing 0        58s ago

2台目のインスタンスを追加して、ewr にデプロイする様子を見てみましょう。

$ fly scale count 2
Count changed to 2

ステータスを見ると、2つのインスタンスが2つのリージョンに分散していることがわかります。

$ fly status
...
Instances
ID       VERSION REGION DESIRED STATUS  HEALTH CHECKS      RESTARTS CREATED
0a8e6666 30      ewr    run     running 1 total, 1 passing 0        16s ago
cdf6c422 30      sea    run     running 1 total, 1 passing 0        6m47s ago

これらがクラスター化されていることを確認しましょう。

$ fly ssh console
$ /app/bin/my_app remote
iex(my-app-1234@fdaa:0:1da8:a7b:ac2:cdf6:c422:2)1> Node.list
[:"my-app-1234@fdaa:0:1da8:a7b:ab2:a8e:6666:2"]

北米大陸の西海岸と東海岸に配置されたアプリケーションの2つのインスタンスが、クラスター化されているのです。ユーザーは自動的に最寄りのサーバーに誘導されます。

Flyプラットフォームには分散サポートが組み込まれており、複数の地域に分散したElixirノードを簡単にクラスターリングできます。

役に立つFlyコマンドとリソース

自分のアカウントのダッシュボードを開く

$ fly dashboard

作成したアプリケーションをデプロイする

$ fly deploy

デプロイしたアプリケーションの状態を表示する

$ fly status

ログにアクセスして追跡する

$ fly logs

アプリケーションのスケールアップ、スケールダウン

$ fly scale count 2

その他の情報については、Fly Elixir documentationを参照してください。

また、Working with Fly applicationsでは、以下のような内容を扱っています。

  • ステータスとログ
  • カスタムドメイン
  • 証明書

トラブルシューティング

トラブルシューティングをご覧ください。

また、Flyコミュニティでは、解決方法やご質問を受け付けています。