こうさくきろく

つくるのたのしい

Rails アプリケーションを Cloud Run にデプロイする

Cloud Run(Cloud Run on GKE でないほう)に Rails アプリケーションをデプロイします。

デプロイは下記の流れで進めます。

  1. Rails アプリの Docker イメージをローカルで作成して、Container Registry に Push
  2. Cloud SQL インスタンスとデータベースを作成
  3. ローカルで Cloud SQL Proxy を使って Schema Migration を実行
  4. Cloud Run にアプリケーションをデプロイ

今回作成したソースコードは下記においてあります。 scripts/ 以下に gcloud コマンドの実行サンプルもおいてあります。

github.com

Cloud Run

Cloud Run は GCP での Knative のマネージドサービスです。

cloud.google.com

cloud.google.com

Knative は Kubernetes 上で動作するソフトウェアで、Functions as a Service のような機能が使えます。

Cloud Run には Google が管理する Kubernertes 環境で動作する Cloud Run と自身の管理する GKE で動作する Cloud Run on GKE があります。

アプリケーションの構成

Active Record を使うシンプルな Rails アプリケーションを想定しています。

データベースには MySQL(Cloud SQL) を使います。 今回のデプロイ先は Cloud Run on GKE ではないため Cloud SQL へは VPC から接続できません。 Cloud SQL Proxy を使って Cloud SQL へ接続します。

Cloud Memorystore などの Cloud SQL 以外のサービスに接続するには現状 Cloud Run on GKE を使う必要がありそうなので、今回は扱いません。

Rails アプリケーションの作成

rails コマンドで Rails アプリを作成します。

rails new rails-cloud-run-sample --skip-bundle
cd rails-cloud-run-sample
bundle install --path vendor/bundle

Cloud SQLMySQL に接続するので mysql2 をインストールします。

bundle add mysql2

適当にモデルを作成します。

bundle exec rails g scaffold user name:string
bundle exec rails db:migrate

config/environments/production.rb を編集して、Production 環境でのアセットコンパイルを有効にしておきます。

- config.assets.compile = false
+ config.assets.compile = true

動作を一度確認するために、サーバーを起動して http://localhost:3000/users にアクセスします。

bundle exec rails s

f:id:mukopikmin:20190620232823p:plain

config/database.yml を編集します。

default: &default
  adapter: sqlite3
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

test:
  <<: *default
  database: db/test.sqlite3

production:
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <%= ENV['MYSQL_PASSWORD'] %>
  socket: <%= ENV['MYSQL_SOCKET'] %>
  database: sample
  username: root

次に、Dockerfile を作成します。

FROM ruby:2.6

ADD Gemfile Gemfile.lock /
RUN bundle install

WORKDIR /app

ADD . .

CMD ["bundle", "exec", "rails", "server"]

あらかじめ Container Registry API を有効にしておき、作成したイメージを Push します。

docker build -t rails-cloud-run-sample .
docker tag rails-cloud-run-sample gcr.io/<PROJECT_ID>/rails-cloud-run-sample
docker push gcr.io/<PROJECT_ID>/rails-cloud-run-sample

Cloud SQL インスタンスの作成

今回作成したアプリケーションが接続する MySQL サーバーを用意します。 Cloud Run が現時点で us-central1 でしか使えないので、それにあわせて us-central1 にインスタンスを作成します。

gcloud sql instances create cloud-run-sample \
  --region us-central1 \
  --assign-ip \
  --tier db-f1-micro \
  --root-password <PASSWORD>

Cloud Run では Cloud SQL Proxy 経由で接続するので、--assign-ip オプションを付けておきます。 パスワードは適当なものを設定してください。 (コンソールから作成すると乱数文字列を設定できます)

作成に成功すると下記のように出力されます。

Creating Cloud SQL instance...done.
Created [https://www.googleapis.com/sql/v1beta4/projects/<YOUR_PROJECT>/instances/cloud-run-sample].
NAME              DATABASE_VERSION  LOCATION       TIER              PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
cloud-run-sample  MYSQL_5_7         us-central1-a  db-f1-micro       xx.xx.xx.xx      -                RUNNABLE

次に、データベースを作成します。

gcloud sql databases create sample --instance cloud-run-sample

作成に成功すると下記のように出力されます。

Creating Cloud SQL database...done.
Created database [sample].
instance: cloud-run-sample
name: sample
project: <YOUR_PROJECT>

Schema Migration の実行

作成した Cloud SQL のデータベースに Rails アプリで使うテーブルを作成します。

まず、Cloud SQL にローカルから接続するために、ドキュメントにしたがってCloud SQL Proxy を用意します。

cloud.google.com

curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64
chmod +x cloud_sql_proxy
sudo mkdir /cloudsql
sudo chmod 777 /cloudsql

Cloud SQL Proxy は Docker でも実行できますが、Docker for Mac ではマウントした Unix ソケットを使って接続ができなかったのでローカルで実行するようにしています。 (TCP ソケットは Docker を使っても問題なく接続できました)

試してはいませんが、Linux 環境であれば Docker でも問題ないと思います。

Cloud SQL Proxy を起動します。 -credential_file-instances オプションなしで実行すると自動的に接続可能なインスタンスが選択され、gcloud でログインしているユーザーで実行されます。 複数のインスタンスが存在していたり、サービスアカウントを指定する場合は適宜設定してください。

./cloud_sql_proxy -dir /cloudsql

Cloud SQL Proxy を起動したままの状態で、Schema Migration を実行します。

export RAILS_ENV=production
export MYSQL_SOCKET=/cloudsql/<PROJECT_ID>:us-central1:cloud-run-sample
export MYSQL_PASSWORD=<PASSWORD>

bundle exec rails db:migrate

MYSQL_SOCKET にはプロジェクト ID と 次に作成する Cloud SQL インスタンスの名前とリージョンを組み合わせて指定します。

成功すれば下記のように表示されます。

== 20190620142533 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.1815s
== 20190620142533 CreateUsers: migrated (0.1816s) =============================

Cloud Run へのデプロイ

ドキュメントの記載に従ってデプロイします。

cloud.google.com

Rails アプリケーションで使う SECRET_KEY_BASE を生成します。

export SECRET_KEY_BASE=$(bundle exec rails g secret)

下記のコマンドを実行して、作成したアプリケーションを Cloud Run にデプロイします。

gcloud beta run deploy rails-cloud-run-sample \
  --image gcr.io/<PROJECT_ID>/rails-cloud-run-sample \
  --add-cloudsql-instances cloud-run-sample \
  --allow-unauthenticated \ 
  --region us-central1 \
  --memory 512Mi \
  --set-env-vars " \
    RAILS_ENV=production, \
    RACK_ENV=production, \
    MYSQL_SOCKET=/cloudsql/<PROJECT_ID>:us-central1:cloud-run-sample,\
    MYSQL_PASSWORD=<PASSWORD>, \
    SECRET_KEY_BASE=$SECRET_KEY_BASE \
  "

このとき、--add-cloudsql-instances オプションで接続する Cloud SQL インスタンスを指定しています。

デプロイに成功すると、下記のように表示さます。

Deploying container to Cloud Run service [rails-cloud-run-sample] in project [<PROJECT_ID>] region [us-central1]
✓ Deploying... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
  ✓ Setting IAM Policy...
Done.
Service [rails-cloud-run-sample] revision [rails-cloud-run-sample-00001] has been deployed and is serving traffic at https://rails-cloud-run-sample-xxxxxxxxx-uc.a.run.app

表示された URL にアクセスしてアプリケーションの動作を確認します。 初回アクセス時はコンテナを起動するのに 10 秒程度かかります。 2回目以降はすぐにレスポンスを返しますが、10 分から 15 分程度アクセスがなくなるとゼロスケールしてしまい、コンテナの起動を待つ必要があります。

f:id:mukopikmin:20190624235012p:plain

アプリケーションがステータス 500 を返す場合は下記のログが残っていないかを確認してください。 エラーメッセージにしたがって、Cloud SQL Admin API を有効にしてみてください。

CloudSQL connection failed: ensure that the account has access to "<YOUR-PROJECT>:us-central1:cloud-run-sample" (and make sure there's no typo in that name). 
Error during createEphemeral for <YOUR-PROJECT>:us-central1:cloud-run-sample: googleapi: Error 403: Cloud SQL Admin API has not been used in project XXXXXXXX before or it is disabled. 
Enable it by visiting https://console.developers.google.com/apis/api/sqladmin.googleapis.com/overview?project=XXXXXXXX then retry. 
If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

まとめ

Cloud Run を使うことで Docker コンテナのデプロイを PaaS 感覚で実行できました。 Cloud Run は Cloud Functions のようにリクエストがあったときの実行時間に対するリソース(メモリとCPU)に対して課金されるので、コスト最適化にも向いていると思います。

ただ現時点では制約が多く、ユースケースはよく検討する必要がありそうです。

1つ目は他のサービスとの連携です。 機能を拡大していくには VPC アクセスができる Cloud Run on GKE が選択肢に上がってきそうです。 Cloud Run on GKE では Cloud Run に加えて GKE のリソースに対する課金が発生するので注意が必要です。

cloud.google.com

2つ目はリージョンです。 現時点で US リージョンのみでの提供のためレイテンシは少し大きい点にも注意が必要です。

3つ目はオートスケールです。 Cloud Run の特徴でもあるオートスケールによって、アクセスがないときはアプリケーションはゼロスケールしてコンテナはゼロの状態になります。 この状態でアクセスするとコンテナの起動に少し時間がかかるため、ゼロスケールを防ぎたいのであれば Cloud Scheduler で定期的にアクセスさせるなどの工夫が必要です。

最初に Function as a Service のように使えると書きましたが、どちらかというと PaaS に近い気がします。 一般的な PaaS だとリクエストがない時間も課金対象ですが、そういった時間のリソースを効率的に管理してくれるという点で Function as a Service と PaaS のいいとこ取りをしているといってもいいかもしれません。