fly.io に Pleroma を設置する

さいきん fly.io というサービスが面白くて触っている。 これはHerokuやCloud Runに近いPaas (CaaS?) で、Dockerfileとちょっとしたconfig fileを用意するとアプリをインターネット上にデプロイできるサービスだ。

個人的にすごい(面白い)と思った特徴として次のようなものがある。

  • Dockerfile が使えるので特定の言語やランタイムに依存しない
  • デプロイしたアプリは各地のEdge Serverで実行され、高速なレスポンスが期待できる
  • 東京Regionがある
  • 特別な設定なしで自動的にHTTPSなURLを提供してくれる
  • マネージドなPostgreSQLがある(無料枠付き!
  • 同じ組織からデプロイしたアプリはWireGuardのVPNを共有し、<app-name>.internal アドレスでお互いアクセスできる

特にPostgresの無料枠があるというのは個人で色々試すにはちょうど良さそうだと考え、fly.io が実際どれくらい使えるのか試したくなり、Pleroma の設置を試みた。

Pleroma とは、一時期話題になったMastodonと近い分散SNSで、Erlang/OTPで動作し、ストレージにPostgresを採用している。 Mastodonと比べると軽量で必要な周辺コンポーネントも少なめなので軽くデプロイするには丁度いいと感じて選択した。

PleromaをDocker化する

fly.io にデプロイするためにはまずアプリをコンテナで動くよう Dockerfile を記述する。 PleromaのREADMEに有志が作ったDockerfileへのリンクがあった ので、そのまま使わせてもらった。

GitHub - angristan/docker-pleroma: Docker image for the Pleroma federated social network

Pleromaの config.exs もこのリポジトリのものをそのままコピーしてきた。

fly.io の設定を作成する

fly.ioのドキュメント に従ってCLIツール flyctl を導入する。 そして flyctl launch で設定ファイル (fly.toml) を作る。 最後に「今すぐデプロイする?」と聞かれるが、まだPostgresなどを立てていないのでここはNoにする。

生成された fly.toml の設定はほぼデフォルトのままでOKだが、用意されたDockerfileでは公開するポート番号が4000なので、internal_port の部分を 4000 に変える。

# fly.toml
[[services]]
  http_checks = []
  internal_port = 4000 

Postgres Databaseを作成する

fly.ioのPostgresドキュメント の通りにPostgres Databaseを作成する。 ドキュメントにもある通り、Postgresのインスタンスも fly.io のアプリのひとつとして扱われる。(よって同じ組織内から <app-name>.internal でアクセスできる。 )

作成するとPostgresのユーザーやパスワードが表示されるので、これをメモしておく。

環境変数を Pleroma アプリに設定する

今回使う config.exs では一部の情報を環境変数から取得する。 fly.io には環境変数を設定ファイルに含めずに秘密情報として差し込む機能 Secrets があるので、それを利用してDBのパスワードやドメイン情報などを入れる。

export DOMAIN=$(flyctl info --host)
export DB_APP_NAME=<FLY_DB_APP>  # Postgres instanceのアプリ名
export DB_PASSWORD=<FLY_DB_PASSWORD>  # Postgres instanceのパスワード

flyctl secrets set \
    DOMAIN=${DOMAIN} \
    DB_HOST=${DB_APP_NAME}.internal \
    DB_USER=postgres \
    DB_PASS=${DB_PASSWORD} \
    DB_NAME=pleroma \
    ADMIN_EMAIL=example@example.com \
    NOTIFY_EMAIL=example@example.com

DBの初期化をする

作成したPostgresにローカル環境から直接入るのにflyctl proxy コマンドが便利である。 次のようにすると localhost:5432 に好きなPostgres clientから接続ができてDBが自由に触れる。

flyctl proxy --app <FLY_DB_APP> 5432:5432

このproxyを通したままの状態で、 postgres://postgres:{password}@localhost:5432/ に対して mix ecto.migrate を実行してDBの初期化を行う。せっかくアプリをDocker化したのに mix ecto.migrate はローカルから直接叩くのはイケてない感じがして、本当はこれもDockerの中でできれば良かったのだが、面倒でローカルにElixirを入れてやってしまった。。

Postgresに対して IPv6 接続を強制する

冒頭に書いたとおり、同じ組織からデプロイしたアプリはWireGuardのVPNを共有し、<app-name>.internal アドレスでお互いアクセスできる。しかしこのDNS名によるアクセスはIPv6が必要らしい1

よって、Ecto.Adapters.Postgresの設定 にIPv6接続のオプションを付けておく。

# config.exs
 config :pleroma, Pleroma.Repo,
   adapter: Ecto.Adapters.Postgres,
   username: System.get_env("DB_USER", "pleroma"),
   password: System.fetch_env!("DB_PASS"),
   database: System.get_env("DB_NAME", "pleroma"),
   hostname: System.get_env("DB_HOST", "db"),
   pool_size: 10,
+  socket_options: [:inet6]

Pleromaをデプロイする

ここまでできたらやっと最後のデプロイ。次のコマンドを打つだけでOK.

flyctl deploy

デプロイが完了したらWebブラウザで開いてみる。

flyctl open

Pleromaの画面が表示された! 🎉

f:id:castaneai:20220220171842j:plain

ユーザー登録、テキストの投稿、そしてリモートサーバーからのフォローなどを少々試してみたが問題なさそうだった。

メモリ上限を上げておく

デフォルトだとfly.ioのVMはメモリ256MBで動いているが、Pleromaはさすがに256MBでは厳しいようで ひとりユーザー登録するだけでOOM Killが発生してしまった。

なので、fly.ioのアプリ管理画面からVMのメモリを512MBに変更したら安定して動作するようになった。

f:id:castaneai:20220220172133p:plain

ただ、Metricsをみる感じ512MBでもギリギリな気もする。自分一人だけが登録してどこのサーバーもフォローしてない状態だから保てているが、フォローフォロワーが増えて処理量が多くなるとメモリ1GB以上が必要かもしれない。

f:id:castaneai:20220220172031p:plain

(補足)画像のアップロード先をS3 + Cloudfrontにする

コピーしてきた config.exs の初期設定では画像などのアップロード先はローカルのファイルシステム上になっている。 しかし、fly.ioのVMのストレージは永続化されておらず、永続化されたvolume mountも設定可能だが無料枠では3GBまでなので、このまま画像アップロードを多用するのは厳しい。

そこで、画像のアップロード先をS3 (+CloudFront) に変えておくとよい。 基本的な設定は公式ドキュメントの通り だが、いくつか注意点がある。

  • S3 Bucketの権限設定を ACL enabled にする

    • S3はACLを無効かしてpolicyのみで権限を管理することを推奨しているようだが、PleromaのS3 UploaderはACLを指定したアップロードをしている2 ため
    • また、Block public access の解除も必要
    • 本当はPleroma側を修正したほうがいいんだろうな…
  • AWSの region: の指定を忘れずに

    • これは上のドキュメントには書かれていないが、region: を指定しないと自動で us-east-1 をリクエストしてしまうため
  • CloudFrontを使う場合は truncated_namespace: "" を忘れずに
    • デフォルトではアップロードされた画像は <base_url>/<s3_bucket_name>/xxxx.jpg のようなURLになるが、CloudFrontでは <bucket_name> の部分は不要なので、truncated_namespace で明示的にbucket_nameを削る指示が必要3

以上の注意点を考慮して最終的に config.exs は次のようになった。

# config.exs
config :pleroma, Pleroma.Upload,
  uploader: Pleroma.Uploaders.S3,
  base_url: "https://xxx.cloudfront.net"

config :pleroma, Pleroma.Uploaders.S3,
  bucket: System.get_env("S3_BUCKET"),
  truncated_namespace: ""

config :ex_aws, :s3,
  access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
  secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
  host: "s3.ap-northeast-1.amazonaws.com",
  region: "ap-northeast-1"