初心者でもできる！Cloud RunでRailsアプリをデプロイ【Rails/GCP】

# はじめに
今回はGCPにおけるKnativeのマネージドサービスであるCloud Runを用いてRailsアプリをデプロイしてみたという内容です。

背景としては、コンテナデプロイを自力でやってみたかったのと、最近は実務でGCPに触れる機会が増えたことから、GCPのコンテナサービスを利用することにしたという感じです。

GCPにおけるコンテナサービスと言えば、真っ先にk8sが浮かんできますが、個人的に今年の4月のGoogle Cloud Next 2019で発表された[CloudRun](https://cloud.google.com/run/?hl=ja)に興味があったこともあり、今回はそちらを試すことにしました。

色々と試行錯誤して動く形にしましたが、最適解ではない可能性も十分にありますので、その点はご注意下さい！
また、有識者の方々からもっとこうした方がいいよ！といったコメントをいただければ幸いです！

# Cloud Runとは？
公式から引用したものを一言で表現すると、**「コンテナをサーバーレスで実行できる環境」**といったところでしょうか。

> Bringing serverless to containers
Cloud Run is a managed compute platform that automatically scales your stateless containers. Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications. It is built from Knative, letting you choose to easily run your containers either fully managed with Cloud Run, or in your Google Kubernetes Engine cluster with Cloud Run on GKE.

使ってみた印象だと、Dockerイメージを用意してあげるだけで良く、k8sの複雑さを意識することがないので、デプロイするまでの労力がそれほど大きくなくて初学者の方でも十分に扱えるサービスだと思いました。

価格に関しても、リクエストを受けたときの実行時間に対して課金される形となっており、お遊び程度に使う分には大変安価に利用できるレベルなので大変お財布に優しいなと感じました。

なお、Cloud RunとCloud Run on GKEの2種類があるそうですが、今回はより安価なCloud Runの方を使っています。

- [Cloud Run](https://cloud.google.com/run/?hl=ja)
- [Cloud Run Pricing](https://cloud.google.com/run/pricing)

# Cloud Runへデプロイするまでの流れと今回の記事の内容
具体的に、本記事では以下の内容について触れます。

1. 実務未経験時に転職活動用に作成したポートフォリオ([GitHub](https://github.com/MasashiFukuzawa/MyCurryApp))をコンテナ化
2. Cloud RegistryにDockerイメージをpush
3. Cloud SQLインスタンスおよびユーザー・データベースの作成
4. ローカルでCloud SQL Proxyを使ってCloud SQLに接続
5. Cloud Runにアプリケーションをデプロイ

(2020.3.20更新)
1.についてはメンテナンスしなくなったのでCloudRunアプリ毎クローズしました。

# 前提
- macOS: Mojave 10.14.5
- Ruby: 2.5.5
- Rails: 5.2.3
- Docker: 18.09.2
- Cloud SDK: 254.0.0
- デプロイするアプリケーション: ポートフォリオ（[GitHub](https://github.com/MasashiFukuzawa/MyCurryApp)）
    - 本記事ではMyCurryAppという名前で進めてます
- 今回使用するGPCサービス
    - [Cloud SDK](https://cloud.google.com/sdk/?hl=JA)
    - [Cloud Registry](https://cloud.google.com/container-registry/?hl=ja)
    - [Cloud SQL](https://cloud.google.com/sql/?hl=ja)
    - [Cloud Run](https://cloud.google.com/run/?hl=ja)

※Googleアカウントの作成、対象プロジェクトの作成、Cloud SDKのインストールなどは完了している前提で進めます

# 1. ポートフォリオのコンテナ化
## Dockerfileの作成
まずはポートフォリオをコンテナで動かせるようにDockerfileを用意します。
特に凝った部分はなく、シンプルな感じにしてます。

```Dockerfile:Dockerfile
FROM ruby:2.5.5
ENV LANG C.UTF-8

RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs     

RUN gem install bundler -v 2.0.2

WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install

WORKDIR /MyCurryApp
COPY . /MyCurryApp
```

## docker-compose.ymlの作成
次にdocker-compose.ymlを作成していきます。
Cloud Runへデプロイするだけの目的の場合、docker-composeは必要ないのですが、ローカル環境でDockerを使って普通に開発したい場合には必要なのと、コンテナが動くか確認したかったので作ってます。

最初ポートは`3306:3306`としていたのですが、上手く行かなかったので、今回はホスト側のポートを5000番にしておきました。5000番は適当な数字なので、3306番やRailsサーバーの3000番以外なら何でも良いです。

上手く行かなかった原因はよく理解できてないのですが、以前このポートフォリオを作成した際のDBとバッティングしてしまったのかなとか思ってます。（お詳しい方がいれば是非ご教示いただければ幸いです。）

```yml:docker-compose.yml
version: '3'
services:
  mysql:  # config/database.yml中のローカル環境のホスト名と揃える
    image: mysql:5.7
    command: mysqld --character-set-server=utf8 --collation-server=utf8_general_ci
    ports:
      - '5000:3306' # 3306:3306だと上手く行かなかったので5000:3306とした
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      MYSQL_DATABASE: MyCurryApp_development  # ローカル環境のDB名
      MYSQL_USER: root                        # ローカル環境のDBを使用する時に使うユーザー名
      MYSQL_PASSWORD: password                # ローカル環境のDBを使用する時に使うパスワード
  app:
    tty: true
    stdin_open: true
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/MyCurryApp
    ports:
      - "3000:3000"
    depends_on:
      - mysql
volumes:
  mysql-data:
    driver: local
```

## database.ymlの修正
docker-compose.ymlと対応するように書き換えていきます。

```yml:config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: mysql

development:
  <<: *default
  database: MyCurryApp_development

test:
  <<: *default
  database: MyCurryApp_test

# productionの設定については後述
production:
  <<: *default
  username: <%= ENV['MYSQL_USER'] %>
  password: <%= ENV['MYSQL_ROOT_PASSWORD'] %>
  database: <%= ENV['MYSQL_DATABASE'] %>
  host: <%= ENV['MYSQL_HOST'] %>
  socket: <%= ENV['MYSQL_SOCKET'] %>
```

## ローカル環境で確認
ここまでできたらDockerイメージをビルドして、データベースを作成、seedデータを流し込んでコンテナを起動させます。

```
$ docker-compose build
$ docker-compose run app rails db:create
$ docker-compose run app rails db:seed
$ docker-compose up
```

そしてコンテナが立ち上がっているかを確認します。

```
$ docker-compose ps
       Name                     Command               State                 Ports
-----------------------------------------------------------------------------------------------
mycurryapp_app_1     start.sh bin/start               Up      0.0.0.0:3000->3000/tcp
mycurryapp_mysql_1   docker-entrypoint.sh mysql ...   Up      0.0.0.0:5000->3306/tcp, 33060/tcp
```

無事に立ち上がっているようです。

念の為、`localhost:3000`でブラウザでアクセスするとちゃんと表示されることが確認できました。
ただ、やや起動が遅かったりするのでその辺は今後詰めていきたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/5c8c34cf-04d7-dc0e-9438-f400a97083ae.png)

# 2. Cloud RegistryにDockerイメージをpush
さて、ここから徐々にCloud Runで本番環境にデプロイする準備をしていきます。

まずは、Cloud Registry（GCP上のDockerHub的なもの）にイメージをpushしていきます。

```
$ docker build -t my-curry-app .
$ docker tag my-curry-app gcr.io/<PROJECT_ID>/my-curry-app
$ docker push gcr.io/<PROJECT_ID>/my-curry-app
```

ここで、pushする際に以下のようなエラーが出た場合は、[Cloud Registry 認証方法](https://cloud.google.com/container-registry/docs/advanced-authentication)に従ってCloud Registryを認証してあげて下さい。

```
unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication

# 上記のようなエラーが出た場合は、下記コマンドで解決
$ gcloud auth configure-docker
```

GCPコンソールで確認すると、きちんと作成されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/0679f20d-a7d2-cc58-82e0-c9207c9c667a.png)

# 3. Cloud SQLインスタンスおよびユーザー・データベースの作成
続いて、本番環境用のMySQLを用意していきます。

まずはインスタンスを作成します。

```
$ gcloud sql instances create my-curry-app \
    --region us-central1 \      # 2019.7.21現在、CloudRunはUSリージョンでしか使えないのでMySQLもUSリージョンで使用するようにしておく
    --assign-ip \
    --tier db-f1-micro \
    --root-password <MYSQL_ROOT_PASSWORD>  # 好きなパスワードを設定
```

上手く行けば以下のようにインスタンスが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/9b47e0dd-9e09-64eb-1217-861231956ef2.png)

次にデータベースとユーザーを作成します。

```
# データベースを作成、<MYSQL_DATABASE>の部分は任意の名前を設定
$ gcloud sql databases create <MYSQL_DATABASE> --instance my-curry-app

# ユーザーを作成、<MYSQL_USER>の部分は任意の名前を設定
$ gcloud sql users create <MYSQL_USER> --instance my-curry-app
```

データベースとユーザーがきちんと作成されたか確認したかったら以下のコマンドで確認可能です。

```
$ gcloud sql databases list --instance=[INSTANCE_NAME]
$ gcloud sql users list --instance=[INSTANCE_NAME]
```

# 4. Cloud SQLへの接続
Cloud SQLのインスタンスが作成できたので、次に、Cloud SQL Proxyを使ってローカルのターミナルでCloud SQLに接続＆マイグレーションをしていきます。（Cloud SQLへの接続はCloud SQL Proxyを介して接続することが推奨されているとのことです。）

ここでやることのイメージとしては、[Cloud SQL Proxy について](https://cloud.google.com/sql/docs/mysql/sql-proxy?hl=ja)を見ると分かりやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/dbe48298-9904-3af0-6c38-bbe097848c99.png)
（上図の参照元：[Cloud SQL Proxy について](https://cloud.google.com/sql/docs/mysql/sql-proxy?hl=ja)）

### 4-1. Cloud API Adminの有効化
GCPコンソールでCloud API Adminを有効化しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/ec9c1d66-0b52-cec8-fd6a-9079ec0183e3.png)

### 4-2. プロキシのインストール
僕のPCはMacなので、[公式ドキュメント](https://cloud.google.com/sql/docs/mysql/connect-admin-proxy?hl=ja)の「MACOS 64 ビット」の部分を見ながら進めて行きます。

```
$ cd
$ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64
$ chmod +x cloud_sql_proxy
```

また、[Rails アプリケーションを Cloud Run にデプロイする](http://mukopikmin.hatenablog.com/entry/2019/06/27/000730)を参考に、ローカルのルートディレクトリにcloudsqlという名前のフォルダを作成しておきます。

```
$ cd /
$ sudo mkdir /cloudsql
$ sudo chmod 777 /cloudsql
```

この状態で`./cloud_sql_proxy -dir /cloudsql`を実行してもエラーが出たのでエラーの内容を追っていき、色々と試した結果、次に説明するstart.shというファイルを用意したら解決したので説明していきます。

### 4-3. start.shの作成
[Cloud Runを使うならこれだけ知ってると得した気分になる](https://qiita.com/chimame/items/e959843e86419f51e45a)を参考に以下のシェルスクリプトを作成しました。

このシェルスクリプトを用意する意図としては、

> ここで大事なのはPORTという環境変数があったら、そのポートでRailsを起動していること。
Cloud RunではPORTという環境変数が渡ってきて、そのポートで起動してるかのチェックが行われます。なので指定されたポートで起動する必要があるため、このような記載になっています。

とのことです。

このファイルなしでデプロイしようとするとポートが云々で怒られたので、きっとここでポートを指定おいてあげることが重要なのでしょう。

また、この中で`bundle exec rake assets:precompile RAILS_ENV=production`を実行してあげないとCSSが反映されなかったという背景があり、assets precompileの記述をしています。

```sh:start.sh
#!/bin/sh
RAILS_PORT=3000
if [ -n "$PORT" ]; then
  RAILS_PORT=$PORT
fi

# migration
bin/rails db:migrate RAILS_ENV=production

# assets precompile
bundle exec rake assets:precompile RAILS_ENV=production

# Remove a potentially pre-existing server.pid for Rails.
rm -f tmp/pids/server.pid

bin/rails s -p $RAILS_PORT -b 0.0.0.0
```

### 4-4. Dockerfileの修正
Dockerイメージをビルドする際に、start.shが毎度実行されるように、上記で作成したDockerfileの最後尾に以下を追記します。

```docker:Dockerfile
・
・
・
# 以下を追記
COPY start.sh /usr/bin/
RUN chmod +x /usr/bin/start.sh
ENTRYPOINT ["start.sh"]
EXPOSE 3000

CMD ["bin/start"]
```

### 4-5. Cloud SQL ProxyによるMySQLへの繋ぎ込み
Cloud SQL Proxyを起動してローカルのターミナルから本番のMySQLにアクセスできるようにしていきます。

```
$ ./cloud_sql_proxy -dir /cloudsql
```

起動したらターミナルの別タブを開いて必要な環境変数を注入していきます。
`<MYSQL_HOST>`には`localhost`のようなものを、`<MYSQL_SOCKET>`には`/cloudsql/[Cloud SQLのインスタンス名]`のような形で指定してあげると上手く行きました。

``` 
$ export RAILS_ENV=production
$ export MYSQL_DATABASE=<MYSQL_DATABASE>
$ export MYSQL_HOST=<MYSQL_HOST>
$ export MYSQL_ROOT_PASSWORD=<MYSQL_ROOT_PASSWORD>
$ export MYSQL_SOCKET=<MYSQL_SOCKET>
$ export MYSQL_USER=<MYSQL_USER>  # ここがrootだと上手く行かなかったです
```

ここまで来れば、残すはCloud Runでデプロイするだけになります！

### (余談)4-7.docker-compose.ymlの修正
Cloud Runでのデプロイとは関係ないですが、Dockerfileとstart.shを編集・作成したことで設定がやや変わり、そのままの設定ではローカル環境で動かなくなるためdocker-compose.ymlにも修正を加えておきます。

修正点としては、`app`の`rm -f tmp/pids/server.pid`と`bin/rails s -p 3000 -b 0.0.0.0`の部分です。
start.shで実行するようにしたので重複となりエラーが発生するようになるのでコメントアウトしておきます。

```yml:docker-compose.yml
　  ・
 　 ・
  　・
  app:
    tty: true
    stdin_open: true
    build: .
    # command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/MyCurryApp
    ports:
      - "3000:3000"
    depends_on:
      - mysql
  　・ 
  　・
  　・
```

# 5. Cloud Runでアプリケーションをデプロイ
いよいよCloud Runでアプリケーションをデプロイしていきます。
以下のコマンドを実行すればデプロイされます。

```
$ gcloud beta run deploy mycurryapp \
   --project <Project ID> \
   --image gcr.io/mycurryapp/my-curry-app \
   --add-cloudsql-instances mycurryapp:us-central1:my-curry-app \
   --allow-unauthenticated  \
   --region us-central1 \
   --memory 1000Mi \
   --set-env-vars RAILS_ENV=production \
   --set-env-vars RAILS_MASTER_KEY=b21bb0d9a126145ab705ace96208344d \
   --set-env-vars MYSQL_SOCKET=/cloudsql/<Project ID>:us-central1:<CloudSQLのインスタンス名> \
   --set-env-vars MYSQL_HOST=<MYSQL_HOST> \
   --set-env-vars MYSQL_DATABASE=<MYSQL_DATABASE> \
   --set-env-vars MYSQL_USER=<MYSQL_USER> \
   --set-env-vars MYSQL_ROOT_PASSWORD=<MYSQL_ROOT_PASSWORD>

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

GCPコンソールで確認してもちゃんとデプロイできてそうです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/1bfff42f-0f69-0938-9870-6509e3a44c21.png)

なお、512MB以下ではメモリ不足で500エラーになってしまったので、今回はメモリを1GBに設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/ea61f545-56d1-5c55-a0bf-dc22d6e602a7.png)

最後にseedデータを流し込みます。

```
$ bundle exec rails db:seed
```

https://mycurryapp-qyobofecda-uc.a.run.app にアクセスしてみると無事にデプロイされていることが確認できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307533/8fbda152-ba11-069d-c7ae-16861b8c4d8a.png)

本番での動作確認もしてみて、基本的に問題はなかったのですが、唯一Twitter認証ができなくなってしまったので、それについては追々調査・修正していこうと思います💡

# Cloud Runを使ってみた感想
- Cloud Runにデプロイするまでの道のり（Cloud SQL ProxyでCloud SQLへの接続＆マイグレーションするところ）でやや躓きましたが、Cloud Run自体は特にハマることもなく扱えたので、コンテナをデプロイできるサービスとしては大変使いやすいサービスなのではないかと思いました！
- レスポンスはやや遅い気もしますが、元々Herokuの無料プラン上で動いていたものをCloud Runに移行した感じなのでそこまで変化はないかもしれません。
- 安価・簡便なので、勉強用として使うには十分なのではないでしょうか！

# 参考URL
- [Rails アプリケーションを Cloud Run にデプロイする](http://mukopikmin.hatenablog.com/entry/2019/06/27/000730)
- [Cloud Runを使うならこれだけ知ってると得した気分になる](https://qiita.com/chimame/items/e959843e86419f51e45a)
- [Cloud SDK](https://cloud.google.com/sdk/?hl=JA)
- [Cloud Registry](https://cloud.google.com/container-registry/?hl=ja)
- [Cloud SQL](https://cloud.google.com/sql/?hl=ja)
- [Cloud Run](https://cloud.google.com/run/?hl=ja)
- [Cloud Run Pricing](https://cloud.google.com/run/pricing)
- [Cloud SQL Proxy を使用して MySQL クライアントを接続する](https://cloud.google.com/sql/docs/mysql/connect-admin-proxy?hl=ja)

# その他
今回の記事の続きとして、勉強のために、CircleCIでの自動デプロイ化や、プチデータ分析基盤の構築（Stackdriver→PubSub→Fluentd→BigQuery→re:dash）などにもちょこちょこ挑戦しているところなので、完成したら記事にまとめたいと思います💡

また、今回はCloudRunでコンテナデプロイをしてみましたが、近いうちに下記記事を参考にしながらk8sにもトライしてみたいと思います！

- [Kubernetes(GKE)にお安く入門する](https://tech.libinc.co.jp/entry/2019/03/22/105911)
- [GKEで最安値のKubernetesクラスタを作る方法](https://sleepless-se.net/2018/12/11/gke-kubernetes/)