fly.ioでWebアプリケーションと定期処理プロセス(Cron)を爆速デプロイ

## はじめに
PaaSはアプリケーションコードを書くだけでほとんどインフラを意識せずに爆速デプロイできるサービスであり、個人開発する際に主にとても重宝しています。PaaSで有名なものに[Heroku](https://www.heroku.com/)がありますが、無料プランの廃止や以前のHerokuのセキュリティインシデントなどを受けて、別のPaaSを探している方も多いようです。

つい最近、また個人開発を進める機会があり、私自身も別のPaaSに興味を持ち、今回はその中でも(主観的な調査のもと)特に知名度や評判の良いと判断した、[fly.io](https://fly.io/)を使ってみることにしました。個人開発を進めるにあたって、ただシンプルなWebアプリケーションが公開できるだけでなく、定期的にデータベースにレコードを追加する処理要件がありました。

そこで本記事では、fly.ioを活用し、Webアプリケーションと定期処理プロセス(Cron)を爆速でデプロイする方法について紹介します。Webアプリケーションのデプロイは正直、超簡単でインターネット上に情報も豊富にありましたが、定期処理プロセス(Cron)のデプロイ方法まで含めてまとまっているところがあまり見つけられませんでした。fly.ioの公式ドキュメントにもいくつか記載はあるのですが、通しで書いてあるわけではなく、少しだけ手間取ったためここにまとめます。念のため書いておくと、fly.ioの公式ドキュメントはかなり分かりやすく便利です。

## 目的
fly.ioで、データベースと接続できるWebアプリケーションと定期処理プロセス(Cron)をとにかく爆速で動かすことを目的として記事を書きます。また、GitHub Actionsで自動的にデプロイする手順についてもおまけとして書きます。

「他にどんなPaaSがあるんですか？」や「fly.ioの具体的な料金プランはどうなんですか？」などの疑問についてはほとんど触れません。別の記事やドキュメントなどを参照してください。

## 手順完了後の状態
これから説明される手順通りに進めていった結果どうなるの？がすぐわかるようにあらかじめ共有します。

https://github.com/solt9029/fly-io-tutorial

## 手順
早速、fly.ioでデータベースとWebアプリケーション、定期処理プロセス(Cron)を爆速で動かすための手順を解説していきます。本記事では、サンプルとしてRuby on Railsを利用します。Railsアプリケーションを構築できる環境と、[Homebrew](https://brew.sh/)が使える状態を前提として進めていきます。

### 事前準備
fly.ioのアカウントをお持ちでない方は、 https://fly.io/ からアカウントを作成してください。また、クレジットカードの登録もしておく必要があるかもしれません。

次に、`flyctl`コマンドを使える状態にする必要があります。以下のコマンドを打ってください。

```shell
brew install flyctl # インストール
flyctl auth login # CLI上でのログイン
```

次に、WebアプリケーションとしてRailsを初期化します。

```shell
rails new fly-io-tutorial --api -d postgresql
```

これで事前準備は完了です。

### データベースとWebアプリケーションのデプロイ
初期化したRailsのディレクトリに入って、以下のコマンドを打ちます。これによって、fly.ioへのデプロイに必要な設定ファイルが自動生成されます。いくつか質問されます。

```shell
cd fly-io-tutorial
fly launch

# fly.io上での好きなアプリケーション名を指定してください。自動生成されるURLにも利用されます。
# アプリケーション名は後から変更できないようです。 参考: https://community.fly.io/t/how-to-rename-my-apps/3098 
? Choose an app name (leave blank to generate one): fly-io-tutorial

? Choose a region for deployment: Tokyo, Japan (nrt)
? Would you like to set up a Postgresql database now? Yes
? Select configuration: Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk

# 必要あれば、後からこの設定は変更できます
# https://community.fly.io/t/how-to-turn-off-postgres-scaling-to-zero-after-1-hour/12522
? Scale single node pg to zero after one hour? No

# 雑に個人開発する時にRedisまで使うことがあまりないので、今回はNoとしましたが、必要なら追加してください
# 本記事ではRedisについては一切触れません
? Would you like to set up an Upstash Redis database now? No
```

さて、これでデータベースはデプロイが完了しました！また、アプリケーションの設定が作成されています。もちろん、この時点ではまだアプリケーションはデプロイされていません。

![Screenshot 2023-05-06 at 21.10.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141427/fd43cf02-a97e-9c4c-401d-125089912a50.png)

また、Railsのディレクトリを改めてみると、いくつかのファイルが自動生成されています。

- Dockerfile
- .dockerignore
- fly.toml

お察しの通り、`Dockerfile`を利用して作成されたDockerイメージを利用してデプロイが行われます。`fly.toml`はfly.ioへデプロイする際の設定などをまとめたファイルです。後で触れますが、一旦ここではスルーします。

これでWebアプリケーションのデプロイの準備が完了しました！ただ、デプロイできたことが確認しやすいように、`routes.rb`を以下のように修正します。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/178551f4e7326c0fb3c78372e8bf039e5c16bcdf)

```config/routes.rb
Rails.application.routes.draw do
  root to: "rails/welcome#index"
end
```

完璧です。デプロイを進めていきましょう。

```shell
fly deploy
```

### Webアプリケーションのデプロイ確認

無事に完了したら、デプロイコマンドで最後に出てきたURLにアクセスして、Railsアプリケーションがちゃんとデプロイされていることを確認しましょう。私の場合は、URLは https://fly-io-tutorial.fly.dev/ でした。

補足1: 以下のコマンドを用いて、何台のインスタンスが立ち上がっているか確認できます。私の場合は、冗長性を確保するためか、自動的に2台が立ち上がっていました。

```shell
fly scale show
```

補足2: fly.ioのダッシュボードから対象のWebアプリケーションのSecretsを見てみると、`DATABASE_URL`や`RAILS_MASTER_KEY`などが自動で設定されています。さすが便利ですね。他に秘匿の環境変数を追加したくなった場合は、CLI経由で追加できます。

![Screenshot 2023-05-06 at 21.33.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141427/ae230ce3-e38b-34f8-1a5a-de2c94837dc0.png)

### データベースの接続確認

雑にマイグレーションファイルおよびモデルファイルを作ります。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/aa0a068d0f22f6065f5473edfaf1c110f89ea5ec)

```shell
rails g model Hello
```

もう一度fly.ioのデプロイをしましょう。

```shell
fly deploy
```

これでマイグレーションが行われているか確認します。以下のようなコマンドを打ち、データベースと接続することができます。

```shell
fly postgres connect -a fly-io-tutorial-db # デプロイされたデータベース名を指定
```

```sql
postgres=# \c fly_io_tutorial --データベース名を指定
You are now connected to database "fly_io_tutorial" as user "postgres".
fly_io_tutorial=# \d
                     List of relations
 Schema |         Name         |   Type   |      Owner      
--------+----------------------+----------+-----------------
 public | ar_internal_metadata | table    | fly_io_tutorial
 public | hellos               | table    | fly_io_tutorial
 public | hellos_id_seq        | sequence | fly_io_tutorial
 public | schema_migrations    | table    | fly_io_tutorial
(4 rows)
```

`hellos`テーブルがちゃんと作成されていますね！

### 定期処理プロセス(Cron)の設定
本記事で重要な、Cronの設定もしていきます。ここからの説明は https://fly.io/docs/app-guides/supercronic/ をベースとしています。

はじめに、定期的に実行したいRakeタスクを定義してみます。`lib/tasks/create_hellos.rake`を新規作成し、以下のような内容にします。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/aa40e933fa4feeaf631b4f38fc061d07316b8140)

```lib/tasks/create_hellos.rake
task "create_hellos": :environment do
  Hello.create!
end
```

次に、定期処理の設定を記載する場所である`crontab`ファイルを以下の内容で作成します。サンプルとして、5分に1回、`hellos`テーブルのレコードを1つ増やすようにしています。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/d4faa633aafe0f9cf5e7e7ebd3253e8cf87c481a)

```crontab
*/5 * * * * bundle exec rake /rails/lib/tasks/create_hellos.rake create_hellos
```

次に、Dockerfileを修正して、定期的に処理が実行できる状態を作ります。以下の記述を最終ステージに追加します。また、最後の`CMD ["./bin/rails", "server"]`の記載を削除してしまいます。のちに、同等の処理を`fly.toml`に記載するからです。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/7c12d3062213fdc0fabd322b5327bac86077038b)

```Dockerfile
# この`apt-get update -qq`以降の記載は元からありますが、ここでcurlもインストールするように修正します
# Install packages needed for deployment
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y postgresql-client curl && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# # Latest releases available at https://github.com/aptible/supercronic/releases
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.24/supercronic-linux-amd64 \
    SUPERCRONIC=supercronic-linux-amd64 \
    SUPERCRONIC_SHA1SUM=6817299e04457e5d6ec4809c72ee13a43e95ba41

RUN curl -fsSLO "$SUPERCRONIC_URL" \
    && echo "${SUPERCRONIC_SHA1SUM}  ${SUPERCRONIC}" | sha1sum -c - \
    && chmod +x "$SUPERCRONIC" \
    && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
    && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic

# You might need to change this depending on where your crontab is located
COPY crontab crontab

# 最後の`CMD ["./bin/rails", "server"]`の記載を削除します
```

ここで、改めて自動生成された`fly.toml`を見ると、以下のような内容になっています。概ね、設定されている情報は雰囲気で読み取れるでしょう。気になる方は公式ドキュメントを見てください。

```fly.toml
# fly.toml app configuration file generated for fly-io-tutorial on 2023-05-06T21:01:45+09:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = "fly-io-tutorial"
primary_region = "nrt"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true

[[statics]]
  guest_path = "/rails/public"
  url_prefix = "/"
```

これに対して、以下のように修正します。 [差分](https://github.com/solt9029/fly-io-tutorial/commit/f412815bbaf3d3a9fbcc881f97985f1ffd2cca86)

```fly.toml
app = "fly-io-tutorial"
primary_region = "nrt"

[processes]
  web = "bin/rails server"
  cron = "supercronic /rails/crontab"

[http_service]
  processes = ["web"]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true

[[statics]]
  guest_path = "/rails/public"
  url_prefix = "/"
```

さて、processesというものを加えました。これによってWebアプリケーションのプロセスとは別に、Cron専用のプロセスを立ち上げることができます。Dockerイメージは共通のものが使われますが、それぞれ指定したコマンドが最後に実行され動くことになります。また、http_serviceに`processes = ["web"]`を指定しています。processesの記載がない場合には、プロセスが確実に1つのため暗黙的に指定されるのですが、processesの記載を増やした場合には、明示的にどのプロセスをhttp_serviceで利用するのか指定する必要があるためです。

準備完了です！お馴染みのコマンドでデプロイしましょう。

```shell
fly deploy
```

これでCronのプロセスが立ち上がります。以下のコマンドを打って、インスタンスの数を確認します。

```shell
fly scale show
```

すると、私の場合、Cronのプロセスまで冗長性をもって2台が起動してしまっていました。定期的に1台で実行されれば十分な場合がほとんどのはずです。そこで、1台にスケールインさせます。

```shell
fly scale count cron=1 --process-group fly-io-tutorial # アプリケーション名を指定
```

補足: 最初から1台だけ起動するようにできないのかなあ、と思ったものの、公式ドキュメントでこのような方法が紹介されていたので、多分ないのかなあと思います。 https://fly.io/docs/app-guides/supercronic/

少し待ってみて、Cronの実行が行われたら、`hellos`テーブルにレコードが追加されているか確認しましょう！

```shell
fly postgres connect -a fly-io-tutorial-db # デプロイされたデータベース名を指定
```

```sql
postgres=# \c fly_io_tutorial --データベース名を指定
You are now connected to database "fly_io_tutorial" as user "postgres".

fly_io_tutorial=# select * from hellos;
 id |         created_at         |         updated_at
----+----------------------------+----------------------------
  1 | 2023-05-06 13:30:01.523303 | 2023-05-06 13:30:01.523303
(1 rows)
```

見事に追加されていました。これで、本記事の目的は達成しました！

## おまけ: GitHub Actionsで自動デプロイ
本セクションは https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ をベースとしています。

以下のコマンドを使って、デプロイに利用できるトークンを発行します。

```shell
flyctl tokens create deploy -x 999999h
```

その後、お手元のGitHubのリポジトリのSecretsで`FLY_API_TOKEN`を追加します。値はもちろん、上記で発行されたトークンをそのまま貼り付けます。

そして、`.github/workflows/fly.yml`を新規作成し、以下の内容を記載し、コミット&プッシュすれば完了です。

```.github/workflows/fly.yml
name: Fly Deploy
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
```

## 最後に
本記事では、fly.ioを活用して、Webアプリケーションと定期処理プロセス(Cron)を爆速デプロイする方法を紹介しました。かなり爆速で環境構築ができて良かったです。

雑プロダクトの個人開発を前提にして環境構築や調査を進めたため、もしかしたら曖昧な表現や誤っている部分があるかもしれません。その際はぜひご指摘ください。

久々に軽い気持ちで記事を書き始めましたが、ちゃんとまとめようとすると大変ですね😇 どなたかのお助けになれば幸いです。最後までお読みいただきありがとうございました。