🚀

Rails + Next.js アプリを Render + Vercel + PlanetScale にデプロイしてみた

2022/10/10に公開

概要

https://twitter.com/rindaaaa94/status/1555492972514639878?s=20&t=uW9ivtjQXMXe10uiBrMSDg

こういう願望があったので、その構成のアプリを現在開発しています(フロントは素のReactではなくNext.jsですが)。

それにあたり、デプロイするサービスは以下のものを選択しました。

  • front: Vercel
  • backend: Render
  • DB: PlanetScale

この組み合わせでデプロイしてみた、という記事がなく、設定にあたり少し手間取ったため、やったことを書いていこうと思います。

デプロイ先の選定理由は?

一番は無料に収まるからです。
それぞれのプラン情報はこんな感じですが、各サービス無料プランがあることがわかります。

Vercel

Render

※ FreeプランのPricing欄に「with limits」とあると思うのですが、このリンクに飛ぶとlimitの詳細がわかります。以下はそのページの一部抜粋です。

The free plan allows for 750 hours of running time per month across all free Web Services in your account and 100 GB of egress bandwidth for each free service. Bandwidth usage in excess of 100 GB is charged at $0.10/GB.

月750時間までは無料ですよ、と書かれています。24時間(1日)×31(1ヶ月分)=744時間 なので、基本年中無料と考えていいと思います。

ただし100GBを超える帯域幅の使用は、$0.10/GBで課金される、とも書いてあるので、そこは注意が必要です。

PlanetScale

これらのhobbyおよびfreeプランを利用したら、無料でアプリケーションを作れるな〜と思ったため、そうしました。

あとは、TwitterやZennで話題によく挙がるPaaSを使ってみたいという理由や、Herokuが10月末からHerokuの無料プランが終了するので、他のPaaSを知っておきたいという理由で、PlanetScaleやRenderを選んだところもあります。

それでは以下から、設定・実装です。

前提

  • ローカルでRails APIとNext.jsはそれぞれ既に動いていて、連携もできている
    • 未実装の方は、こちらの記事を参考にするとスムーズに作れると思います

https://zenn.dev/kei178/articles/43172ba33eece4

  • Vercel, Render, PlanetScaleのアカウントを作成している
    • まだの方はぽちぽちしてください

PlanetScale

まずはPlanetScaleにデプロイします。
デプロイと言っているものの、今回の想定では、ローカルでもPlanetScaleのDBを使っていくため、ローカルと本番用としてPlanetScaleでDBを作る、というモチベーションで以下進めていきます。

基本はこれらの記事の内容をマージしている感じなので、原文がよい方はこっちを見に行ってみてください。
https://planetscale.com/docs/tutorials/connect-rails-app
https://planetscale.com/docs/concepts/nonblocking-schema-changes

1. gemの追加

gemfileにmysql2を追加します。追加したら、bundle installをしてください。

gemfile
gem "mysql2"

2. PlanetScale CLIのインストール & ログイン

こちらでPlanetScale CLIをインストールしてください。

$ brew install planetscale/tap/pscale

CLIの中にはMySQL Clientを必要とするコマンドもあるので、それも以下を叩いて入れておきましょう。

$ brew install mysql-client

入れたら、併せてCLIからPlanetScaleにログインをしておきます。

$ pscale auth login

3. DBの作成

PlanetScale上にDBを作成します。

$ pscale database create blog

名前は適当にblogとしていますが、ここには自分が開発しているアプリケーションの名前を入れてください。
以下では引き続き、アプリの名前はblogとして進めていきます。

DBにパスワードの名前を設定します。

$ pscale password create blog main <PASSWORD_NAME>

注意する必要があるのは、ここではパスワード自体を設定しているのではなく、パスワードを示す名称を決定している、ということです。
このコマンドを叩くとパスワード自体の方は自動生成されます。

PlanetScaleでは、そのパスワードを管理するための名称を決める必要があり、上のコマンドではそれを実行しているものです。
なので、特にこだわりがないのであれば、PASSWORD_NAMEにはdevelopment-passwordなどを入れておくのがよいでしょう。

4. database.ymlの設定

database.ymlを設定します。
開発用と本番用と合わせて、以下のようにセッティングします。

database.yml
default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_general_ci

development:
  <<: *default
  database: blog
  username: <%= ENV['DB_USERNAME_DEV'] %>
  host: <%= ENV['DB_HOST_DEV'] %>
  password: <%= ENV['DB_PASSWORD_DEV'] %>
  ssl_mode: verify_identity
  sslca: "/etc/ssl/cert.pem"

production:
  <<: *default
  database: blog
  username: <%= ENV['DB_USERNAME_PROD'] %>
  host: <%= ENV['DB_HOST_PROD'] %>
  password: <%= ENV['DB_PASSWORD_PROD'] %>
  ssl_mode: verify_identity
  sslca: "/etc/ssl/certs/ca-certificates.crt"

defaultの設定の説明については省略します。

developmentの環境変数はローカルのenvファイルで、productionの環境変数はAPIをデプロイする、Render側のGUIで設定します。

developmemt側は本セクションの6.で説明しますが、productionはAPIのデプロイが終わらないとできないので、とりあえずどちらも、値の設定は後回しとしましょう。

また、hostなどの基本的な項目に加え、ssl_mode, sslcaという項目も併せて設定しています。
ssl_modeがssl通信の方式で、sslcaがCA(Certificate Authorities)により発行された電子証明書が存在するパスです。
これらは、PlanetScaleでは、DBにアクセスする際SSLによる通信が義務付けられているために、必要なものになります。

詳しくはこちらのページで述べられていますが、中間者攻撃などからの脆弱性を加味して、そのような方針を採用しているようです。

developmentのsslcaについて、私のmacOSにはデフォルトでこのパスに証明書が存在していたので、このパスを指定しています。
ローカルでDockerを使用している方は注意が必要で、コンテナ内には証明書は存在していない(私の場合はそうでした)ので、明示的にdocker-compose.ymlのvolumesなどでマウントしておく必要があります。

productionのsslcaについては、Renderが管理するサーバーの証明書はここに存在しているようだったので、このパスを指定しています。

5. マイグレーション

そろそろ終わりが見えてきました。
それでは、ローカルで適当にテーブルを作成し、マイグレーションをしてみてください。

そして、以下でDBのmainブランチに接続し、

$ pscale shell blog main

このSQLコマンドを叩くと、

describe <TABLE_NAME>;

作成したテーブルがDBに反映されているのが確認できるはずです。

6. ブランチの設定

5.まででローカルのデータベースはうまく作成できたのですが、この記事の目的はデプロイなので、最後に本番用データベースの作成をして終わりとします。

5.の中でmainブランチという言葉を使いましたが、PlanetScaleにはgitと同様に、ブランチという概念が存在します。

本番用のDBとしてはmainブランチを、ローカル用のDBとしては適当にmainから切ったブランチを適用する形です。

そして、ローカル用ブランチを本番用にデプロイすると、そのスキーマを本番用に反映してくれます(実データの反映はされません)。

この節ではdevelopブランチの作成、およびdevelopからmainへのデプロイの方法を説明し、それをもってPlanetScaleへのデプロイを完了といたします。

まずは5.までで作成したmainブランチを本番用DBとして設定します。

$ pscale branch promote blog main

次にローカル用のdevelopブランチを作成します。

$ pscale branch create blog develop

これでさくっとブランチの作成自体は完了しました。

ローカル用のbranchを作成できたので、4.で宿題となっていた、database.ymlのdevelopmentの環境変数設定を行いましょう。

各変数(host, username, password)の値は、PlanetScaleのGUIにて、このプロジェクトを選択したあとのダッシュボードで、「develop」ブランチを選択し、その後「Overview」 -> 「connect」とクリックしていくと確認することができます。
それをenvファイルに設定してください。

あとは、ローカルのスキーマ変更を本番に適用したいときに、以下をもとに、ローカルから本番へのDBデプロイを実行してみてください。

// デプロイのリクエストをdevelopブランチから投げる
$ pscale deploy-request create blog develop

// リクエストの番号を確認する
$ pscale deploy-request list blog

// 指定した番号のリクエストについて、デプロイを受け入れる
$ pscale deploy-request deploy blog <deploy-request-number>

7.done!

DBについてはこれにて完了です🎉
続いて、Renderに対してAPIのデプロイを行っていきます。

Render

RenderもRailsユーザー向けドキュメントがあるので、それを参考に進めていきます。
https://render.com/docs/deploy-rails

原文を参考に進める方は、こちらのドキュメントはAPI用ではなく、Viewまでを含めたWebアプリケーション用なので、若干注意が必要です。

1. API用のプロジェクトを作成する

RenderにGUIでサインインして、「New +」ボタンを押した後、「New Web Service」というページに遷移します。
そこでGithub連携をしたのち、connectionしたいレポジトリを選択します。

その後プロジェクトの設定画面に遷移するので、好きな具合に設定してください。
ただし、以下の項目のみはこのようにしておく必要があります。

  • Environment: Ruby
  • Plans: Free

また、Build CommandとStart Commandについてはデフォルトのままにしておいてください。
後々設定を入れます。

すべて設定し終えれたら、「Create Web Service」を押して、最初のデプロイを行ってください。
この時点では設定が足りないため何かしらエラーを吐かれますが、一旦放っておきましょう。

2. config/puma.rbの設定

この内容で設定します。諸項目の説明は省略します。

backend/config/puma.rb
backend/config/puma.rb
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port        ENV.fetch("PORT") { 3000 }

# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }

# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
workers ENV.fetch("WEB_CONCURRENCY") { 4 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
preload_app!
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

3. Build時のスクリプトを作成

1.でデフォルトのままにしておいた、Build Commandで実行するためのスクリプトを作成します。

この内容のファイルをbinの下に作成してください。

backend/bin/render-build.sh
#!/usr/bin/env bash
# exit on error
set -o errexit

cd backend
bundle install
bundle exec rake db:migrate

ここで、

  • root
    • backend
    • frontend

というディレクトリ構成になっている場合、cd backendが必要なことに注意してください。
bundleコマンドを叩く前に、ルートからbackend用ディレクトリに移動する必要があるからです。

そしてスクリプトファイルに権限を与えないと実行できないので、以下で権限を与えます。

$ chmod a+x backend/bin/render-build.sh

4. render.yamlの作成

render.yamlという、Renderにデプロイするための設定ファイルを作成します。

内容は以下です。

render.yaml
services:
  - type: web
    name: dictionary
    env: ruby
    buildCommand: "./backend/bin/render-build.sh"
    startCommand: "bundle exec puma -C ./backend/config/puma.rb ./backend/config.ru"
    envVars:
      - key: RAILS_MASTER_KEY
        sync: false

buildCommandでは、3.で作成したビルド用スクリプトを実行しています。
またstartCommandでは、Render側でWebサーバーの起動を行っています。

5. Blueprint Instanceの作成

RenderのGUIから、「Blueprints」ページに移動したのち、「New Blueprint Instance」ボタンをクリックします。

その後、1.同様、connectionしたいGithubリポジトリを選択して、Blueprint instanceを作成します。

このBlueprint instanceというものは、4.で作成したrender.yamlを用いてデプロイを行うために必要な工程です。

6. 環境変数の設定

GUIから「Dashboard」ページに移動したのち、「Environment」の項目をクリックしてください。
そこが環境変数を設定できるページです。

しかし、設定すべき環境変数のうち、本番用DBの各情報は取ってこれていないので、別タブを開いてPlanetScaleのGUIを開いてください。

そこでmainブランチを選択し、「Overview」 -> 「connect」というクリックすると、情報を確認することができます。その中から、host, username, passwordをコピーして以下のようにRender側で設定しましょう。

  • DB_HOST_PROD: XXX
  • DB_USERNAME_PROD: XXX
  • DB_PASSWORD_PROD: XXX

最後に、ローカルのbackend/config/master.keyにある、マスターキーの値を併せて以下のように設定してください。

  • RAILS_MASTER_KEY: XXX

7. done!

これでAPIのデプロイも完了です🎉
最後に、フロントのデプロイを行いましょう。

Vercel

1. 新しいプロジェクトを作成

Vercelの管理画面にログインし、「Add New Project」を選択して、フロント用の新しいプロジェクトを作成します。
また、Vercelでも同様にGithubと連携してください。

ここで、GUIの「Settings」 -> 「General」から、Root Directoryをfrontend用のトップディレクトリにしておいてください。

2. 環境変数の設定

あとは、フロントからバックエンドとやりとりするために必要なエンドポイントを、環境変数として設定します。
GUIの「Settings」 -> 「Environment Variables」から、以下のように設定してください。

  • NEXT_PUBLIC_BACKEND_URL: Render側のAPIのURL

またこの時点でフロント側のURLがわかるので、Render側のGUIでも、エンドポイントを以下のように設定してください。

  • FRONTEND_URL: Vercel側のフロントのURL

3. done!

フロントはこれだけで終了です🎉
Vercel、なんてかんたんなんだ...

まとめ

これにてすべてデプロイ完了です!
実際に、フロント <-> API <-> DBと、連携ができているのを確認してみてください。

所感

途中Renderの方でけっこうつまづいたので、Vercelのかんたんさに癒されました。

補足

もしRenderのログで、cannot load such file -- net/smtp的なエラーが出て、Pumaの立ち上げに失敗する場合は、Gemfileに以下のgemを追加しておく必要があるかもしれません。

gemfile
gem 'net-smtp', require: false
gem 'net-imap', require: false
gem 'net-pop', require: false

どうやら、Ruby3.1からこれらのgemが標準ライブラリから外れてしまっており、自分で入れる必要があるようです。

名前から察するにメール用の各種プロトコルを扱うgemのようですが、それが実際どういう風に使われているのかまではよくわかりませんでした。

ただ調べていると、Herokuでも同様のエラーが発生していたため、本番環境でPumaを立ち上げるのに必要な場合があるのかもしれません。

参考記事

https://zenn.dev/yu_9/scraps/29c42206515bf6
https://qiita.com/koki_73/items/60b327a586129d157f38

Discussion