Fly.ioにFastAPI + PostgreSQLの環境をデプロイしてみた

# はじめに
以下構成のバックエンド環境をFly.ioに初めてデプロイしてみたので、その時の手順を共有します。
- FastAPI（Dockerコンテナ）
    - SQLAlchemy（ORM）
    - Psycopg（PostgreSQL用のデータベースアダプター）
    - Alembic（データベースのMigrationツール）
- Postgresql

特に、SQLAlchemy+psycopgを使ってFly.ioのPostgresqlに接続する場合、スムーズにいかなかったのでそのあたりのナレッジも残しておこうと思います。

# 対象読者
- Fly.ioにFastAPI + Postgresqlの環境をデプロイしたい方
- Fly.ioへのデプロイの流れや使い方をざっくり知りたい方

# 本記事の前提
## 開発環境
- MacOS 14.4.1
- Docker Desktop for Mac

## デプロイ対象のFastAPIとPostgresqlのコンテナ
- FastAPIのDockerイメージはDockerfileから作成
```dockerfile: Dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    build-essential && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY . .

RUN pip install poetry
RUN poetry install --no-interaction --no-ansi

EXPOSE 8000

CMD ["poetry", "run", "uvicorn", "--host", "0.0.0.0", "test_app.app:app"]
```

- FastAPIとPostgresqlは以下の`compose.yaml` で起動
    - Postgresqlへの接続文字列を環境変数で定義しています。
```yaml: compose.yaml
services:
  db:
    image: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: test_user
      POSTGRES_DB: test_db
      POSTGRES_PASSWORD: test_password
    ports:
      - "5432:5432"

  app:
    image: test_app
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    depends_on:
      - db
    environment:
      # psycopgアダプターを使用
      DATABASE_URL: postgresql+psycopg://test_user:test_password@test_db:5432/app_db
volumes:
  pgdata:
```
:::note info
コンテナの環境変数は`.env`等からロードするのが一般的ですが、本記事では便宜的にcompose.yamlにハードコーディングしています。
:::

- データベースのマイグレーションファイル
    - Alembicを使って以下のようにDBのマイグレーションファイルを管理しています。
```
.
├── alembic.ini
└── migrations
    ├── env.py
    ├── script.py.mako
    └── versions
        ├── 71bfea98db2b_create_todo_table.py # <- マイグレーションファイル
        └── 7c2f73b28d63_create_user_table.py  # <- マイグレーションファイル
```

上記前提のもと、本記事では以下の内容を説明します。
- Fly.ioにFastAPIアプリケーションをデプロイする
- Postgresqlデータベースは、Fly.ioが提供するマネージドサービスを利用する
- デプロイしたFastAPIにssh接続してデータベースのマイグレーションを実行する

# Fly.ioとは？
Fly.ioとは、アプリケーションをクラウド上で簡単に起動できるPaaS（Platform as a Service）型のプラットフォームです。

https://fly.io/

多くのプログラミング言語とフレームワークに対応しており、特にDockerコンテナを使用してのデプロイが可能です。
また、Fly.ioはPostgreSQLやRedisなど、アプリケーションで使用するデータベースも提供しています。

## Fly.ioの無料プラン
Fly.ioは無料プランを提供しており、個人開発等でちょっと試しにデプロイしてみる環境にはちょうどいいです。
以下のリソースが無料で使えます。



- 最大3つのshared-cpu-1x 256MB VMs
- 3GBの永続ボリュームストレージ
- 合計160GBのアウトバウンドデータ転送

詳細は以下参照。

https://fly.io/docs/about/pricing/#new-customers-get-a-free-trial

---
それではここからFly.ioにデプロイする手順を説明していきます。

# ①Fly.ioのアカウントを作成する
まずはFly.ioの以下ページからアカウントを作成しましょう。

https://fly.io/app/sign-in

アカウントを作成すると、以下のダッシュボード画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/198688d1-3f81-d6ec-1440-6a164540de0c.png)


## クレジットカードの情報を登録する
無料枠が付いているものの、Fly.ioが提供するPostgresqlを利用するためにはクレジットカードの登録が必要になります。以下の手順で登録しておきましょう。[^1]

- ダッシュボード画面上部に表示されている`Add a payment method to keep using our platform.`をクリック
- 以下のHobby Planの下にある`Continue`をクリックしてカード情報を登録する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/ff95047d-8f6f-f5fb-0cbc-7b898cfc6d0d.png)
- ダッシュボードに$5.00の利用ができる旨、メッセージが表示されればOK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/34201b6d-7692-2abf-9521-a35b128031cc.png)


# ②`flyctl`をインストールする
Fly.ioをローカル環境から操作するためのCLIアプリケーション `flyctl` をインストールします。
インストール方法は、**[公式のインストールガイド](https://fly.io/docs/hands-on/install-flyctl/)** を参照してください。

インストール後、以下のコマンドでflyctlのバージョンが表示されればOKです。
```bash
$ flyctl version
flyctl v0.2.50 darwin/arm64 Commit: b699e9.... BuildDate: 2024-05-08T14:04:18Z
```

# ③`flyctl`からFly.ioにログインする
`flyctl`からFly.ioを操作するための準備として、以下のコマンドでログインを行います。
```bash
$ flyctl auth login
Opening https://fly.io/app/auth/cli/223fascc23411973591eeaee63e2 ...

Waiting for session...
```

上記を実行するとブラウザウィンドウが開き、ログインを求められるので、作成したアカウントでログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/84ac0225-fa10-dc28-56c6-069dc50ab64b.png)

ログインに成功したら、ブラウザを閉じるとターミナルにログインが成功した旨のメッセージが表示されていればOKです。
```bash
...
Waiting for session... Done
successfully logged in as <ログインアカウントのメールアドレス>
```
以降、`flyctl`を使ってアプリケーションのデプロイをしていきます。

# ④Fly.ioにAppsを作成する
デプロイする前に、まずは新しいアプリケーション（Fly.ioの`Apps`というもの）を以下のコマンドで作成します。
```bash
$ flyctl launch --no-deploy
```

- `flyctl launch`
    - Fly.io上に新しいアプリケーションを作成するコマンド
    - アプリケーションの設定に必要な`fly.toml`ファイルなどがローカルに生成される

- `--no-deployオプション`
    - 実際のアプリケーションのデプロイは行わないようにするオプション
    - こうすることで、アプリケーションの設定やコードを確認・修正してからデプロイすることができる

実行すると以下のメッセージが表示されます。
```bash
$ flyctl launch --no-deploy
Scanning source code
Detected a Dockerfile app
Creating app in /<youre-directory>
We're about to launch your app on Fly.io. Here's what you're getting:

Organization: <account name>         (fly launch defaults to the personal org)
Name:         fly-demo               (derived from your directory name)
Region:       Hong Kong, Hong Kong   (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres:     <none>                 (not requested)
Redis:        <none>                 (not requested)
Sentry:       false                  (not requested)

? Do you want to tweak these settings before proceeding? (y/N) 
```

上記の表示内容について、ポイントとしては以下です。
- カレントディレクトリにDockerfileが存在する場合、`flyctl`が自動的に検出してくれる（3行目）
- `Organization:` 以降は、作成するアプリケーションのデフォルト設定を示している
    - デプロイ先のリージョン (なぜかHong Kongになっている・・・)
    - アプリケーションの実行マシン構成（`shared-cpu-1x, 1GB RAM`）
    - Postgres オプション（設定なし）

最後に`Do you want to tweak these settings before proceeding? (y/N) ` と聞かれますが、今回はデフォルト設定から変更したいので `y` を入力します。

そうすると、再びブラウザウィンドウが開き、以下の画面が表示されるのでそれぞれ設定を変更していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/91d899c3-fc47-82bd-4de6-ccf3f350c7c4.png)



私の場合は、Fly.ioのお試し利用が目的だったのでマシンスペックや可用性はすべて最小構成にしました。
また、Postgresqlを使うのでDatabaseの設定を入力しています。

入力後、`Confirm Settings`をクリックしてターミナルに戻ると、以下の通り、アプリケーションおよびPostgresqlを作成した旨のメッセージが表示されます。

```diff: flyctl launchコマンドの実行結果の抜粋
Created app 'fly-demo-test' in organization 'personal'
Admin URL: https://fly.io/apps/fly-demo-test
Hostname: fastzeroapp.fly.dev
Creating postgres cluster in organization personal
Creating app...

...省略

Postgres cluster fly-demo-test-postgres created
  Username:    postgres
  Password:    YIawXXplW2rRe2b
  Hostname:    fly-demo-test-postgres.internal
  Flycast:     fdaa:9:4169:0:1::2
  Proxy port:  5432
  Postgres port:  5433
+  Connection string: postgres://postgres:YIaPPtXCRrRe2b@fly-demo-test-postgres.flycast:5432

...省略

Postgres cluster fly-demo-test-postgres is now attached to fly-demo-test
The following secret was added to fly-demo-test:
+ DATABASE_URL=postgres://fly_demo_test:cQv0vvfasf7FM@fly-demo-test-postgres.flycast:5432/fly_demo_test?sslmode=disable
Postgres cluster fly-demo-test-postgres is now attached to fly-demo-test
? Create .dockerignore from .gitignore files? (y/N)
```

上記の差分として表示している箇所は、作成したPostgresqlデータベースへの接続文字列です。
**この情報はFly.ioのWeb画面でも閲覧できない機密情報になるため、この情報は大切に保管し、共有はしないようにしましょう。**

また、最後の質問（`? Create .dockerignore from .gitignore files? (y/N)`）については、`.dockerignore`が必要であれば`y`にしましょう。

```bash: flyctl launchコマンドの続き
? Create .dockerignore from .gitignore files? (y/N) Yes

Created <src-path>/.dockerignore from 4 .gitignore files.
Wrote config file fly.toml
Validating <src-path>/fly.toml
✓ Configuration is valid
Your app is ready! Deploy with `flyctl deploy`
```

上記の通り、アプリケーションをデプロイする準備が整ったようです🎉

また、カレントディレクトリを見てみると、作成したアプリケーションの構成情報（`fly.toml`）ファイルが作成されていると思います。

# ⑤環境変数をFly.ioの`Secrets`に登録する
アプリケーション内で利用している環境変数がある場合、デプロイする前にFly.io上で読み込める状態にする必要があります。

これを実現するために、Fly.ioでは`Secrets`という機能を提供しています。
`Secrets`を使用すると、デプロイしたアプリに資格情報などを環境変数として渡すことができます。

https://fly.io/docs/reference/secrets/

どうやってSecretsを登録するかというと、
`flyctl`の `secrets set` コマンドを使って登録することができます。

```bash
$ fly secrets set MY_SECRET_NAME="test secret"
```

## SQLAlchemy + PsycopgでPostgresqlに接続する方法

本記事では、元々[`compose.yaml`](https://qiita.com/inetcpl/items/454fe7b7aef2f9326c04#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E5%AF%BE%E8%B1%A1%E3%81%AEfastapi%E3%81%A8postgresql%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A)内で定義していたデータベースの接続文字列 `DATABASE_URL` を手順④で払い出された接続文字列に変更し、その値をアプリケーションからロードできる状態にします。

登録する前に、まずは`flyctl secrets list`で現在登録されている環境変数を確認してみます。

```bash
$ flyctl secrets list
NAME            DIGEST                  CREATED AT 
DATABASE_URL    eb0aac24db6ab082        1h10m ago
```

実は上記の通り、手順④を実行したときにDATABASE_URLが環境変数として登録されています。
内容は以下の値です。
```bash: flyctl launchコマンドの実行結果の抜粋
Connection string: postgres://postgres:YIaPPtXCRrRe2b@fly-demo-test-postgres.flycast:5432
```

ただし、SQLAlchemyとPsycopgを使ってPostgresqlに接続している場合、**上記の接続文字列を有効な値として認識してくれません。**
元々、compose.yamlの`DATABASE_URL`で定義していたようにプレフィックスの部分を`postgresql+psycopg`に修正する必要があります。
```diff
- postgres://postgres:YIaPPtXCRrRe2b@fly-demo-test-postgres.flycast:5432
+ postgresql+psycopg://postgres:YIaPPtXCRrRe2b@fly-demo-test-postgres.flycast:5432
```

以下のコマンドで環境変数`DATABASE_URL`を修正した値で上書き登録できます。
```bash
$ flyctl secrets set DATABASE_URL=postgresql+psycopg://postgres:YIaPPtXCRrRe2b@fly-demo-test-postgres.flycast:5432
Secrets are staged for the first deployment
```

# ⑥アプリケーションのデプロイ
デプロイする準備が整ったので、`flyctl deploy`コマンドでアプリケーションをデプロイします。
```bash
$ fly deploy --local-only --ha=false
```

- `--local-only`
    - このオプションをつけると、デプロイ処理（コンテナのビルド等）がローカル環境で実行されます。
    - このオプションを指定しない場合、デプロイ処理はFly.io上で実行されますが、ローカル環境でテストやデバッグをしたい場合にこのオプションを使用します。

- `--ha=false`
    - 高可用性デプロイを有効にするオプションです。アプリケーションが複数のインスタンスで立ち上がり、障害時に自動でフェイルオーバーしてくれます。
    - 今回は無料枠内で検証したいので本機能をFalseで無効化しています。

上記コマンドを実行すると、以下のようにDockerイメージのビルドが実行され、その後Fly.ioにデプロイされます。
```bash
$ fly deploy --local-only --ha=false
==> Verifying app config
Validating <your-directory>/fly.toml
✓ Configuration is valid
--> Verified app config
==> Building image
==> Building image with Docker
--> docker host: 25.0.5 linux aarch64
[+] Building 51.0s (12/12) FINISHED                                                                                                                          
 
 ...省略
 
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/fly-demo-test]
8c65950a7b44: Pushed 
afe5e98a69a7: Pushed 
2ff1765ea85f: Pushed 
52664eb4ef6b: Pushed 
b0190595c83a: Pushed 
67704d8b406c: Pushed 
216328af4e79: Pushed 
238975b59c5f: Pushed 
3069e121245c: Pushed 
146826fa3ca0: Pushed 
5d4427064ecc: Pushed 
deployment-01HY09TSQNFP1FB83CXDR9H3SZ: digest: sha256:8c0297ce2be1e15c1b513ff41a05c613cb5c01d590db5c3200dd59df15009358 size: 2633
--> Pushing image done
image: registry.fly.io/fly-demo-test:deployment-01HY09TSQNFP1FB83CXDR9H3SZ
image size: 812 MB

Watch your deployment at https://fly.io/apps/fly-demo-test/monitoring

Provisioning ips for fly-demo-test
  Dedicated ipv6: 2a09:8280:1::35:b49f:0
  Shared ipv4: 66.241.125.58
  Add a dedicated ipv4 with: fly ips allocate-v4

This deployment will:
 * create 1 "app" machine

No machines in group app, launching a new machine
Finished launching new machines
-------
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling

-------
Checking DNS configuration for fly-demo-test.fly.dev

Visit your newly deployed app at https://fly-demo-test.fly.dev/
```

上記メッセージの最後の方に、アプリケーションのデプロイに関する重要な情報が示されています。
- 監視URL（https://fly.io/apps/your-app-name/monitoring）
    - コンテナのログを閲覧できる
- アプリケーションにアクセスするためのURL（https://your-app-name.fly.dev/）

監視URLにアクセスすると、以下の通りFastAPIが起動していることが確認できると思います🎉🎉
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/319375/ac567b30-7f65-4f60-c32d-f53de3d58bb0.png)


# ⑦アプリケーションにssh接続してAlembicでデータベースのマイグレーションを実行する
ただし、今の状態だとFly.ioに作成したPostgresqlはテーブル情報を保持していません。
そのため、DBのマイグレーションファイルを使ってPostgresqlにテーブルを作成する必要があります。

Fly.ioではデプロイしたアプリケーションにssh接続し、任意のコマンドを実行することができます。
`flyctl ssh console　-a <app-name> -C "実行するコマンド"`

本コマンドを使ってAlembicのマイグレーションを実行します。
```bash
$ flyctl ssh console -a fastapi-zero -C "alembic upgrade head"
Connecting to fdaa:9:4109:a7b:b4f1:12d8:1709:2... complete
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 7c2f73b28d63, create user table
INFO  [alembic.runtime.migration] Running upgrade 7c2f73b28d63 -> 71bfea98db2b, create todo table
```

その後、デプロイしたFastAPIの
swagger UI（https://your-app-name.fly.dev/docs） 等にアクセスし、DBに対してCRUD操作が行えることを確認できればOKです。

# ⑦アプリケーションの削除
デプロイしたアプリケーションとPostgresqlを削除したい場合、それぞれ以下のコマンドで削除できます。
```shell
# アプリケーションの削除
$ fly apps destroy <app-name>

# Postgresqlの削除
$ fly apps destroy <db-name>
```


# さいごに
以上、FastAPI/SQLAlchemy/psycopg/Alembic/PostgreSQLの組み合わせをFly.ioにデプロイする手順の紹介でした。

この組み合わせがピンポイントでハマる人がいるかは微妙ですが、誰かの役に立ったら嬉しいです。

Fly.ioを使ってみた感想として、デプロイ操作は直感的でやりやすかったのですが、Fly.ioのWeb画面がもっさりしているのが少し気になりました。


[^1]: クレジットカードを登録しない状態でdeployすると以下のエラーが発生します。
Error: We need your payment information to continue! Add a credit card or buy credit: https://fly.io/dashboard/<アカウント名>/billing