M1 MacでGCP Cloud RunへRails6デプロイするtutorial試したので詰まったポイントをメモ

前回[M1 MacでGCP Cloud RunのQuickstart(ruby版)試したので詰まったポイントをメモ](https://qiita.com/ryan5500/items/ddb74f0b0bb5751647d6)で、sinatraアプリをCloud Runにデプロイした。

次はRailsを試してみる。

* GCP Cloud Runに
* Ruby v3.0.3で Rails 6のアプリを
* M1 Macの環境から

デプロイしてみる。

ちょうどRails6を利用したチュートリアルが用意されているので、それをやっていく。

[Cloud Run 環境での Rails の実行](https://cloud.google.com/ruby/rails/run)

# 準備

今回実験に使うプロジェクトをgcloudのデフォルトプロジェクトとして設定したい。
プロジェクトIDを探しにブラウザを動かすのがめんどくさい場合、CLIでも探すことができる。

```bash
$ gcloud projects list
PROJECT_ID                NAME                     PROJECT_NUMBER
test-project-41231412  Test Project              412314......
```

その上で、見つけたプロジェクトIDをデフォルトプロジェクトに設定する。

```bash
# この例ではプロジェクトIDはrails-cloud-run-test
$ gcloud config set project rails-cloud-run-test
```


# Railsのサンプルアプリの取得

```bash
$ git clone https://github.com/GoogleCloudPlatform/ruby-docs-samples.git
$ cd ruby-docs-samples/run/rails
$ bundle install

# 前回の学びで、Gemfile.lockにx86 platformを追加しないとCloud Buildのビルド時にコケるため、対応
$ bundle lock --add-platform x86_64-linux
```

## bundle installでsasscのインストールエラー

`bundle install`中、`sassc`のインストールでエラーが出る。

```
An error occurred while installing sassc (2.1.0), and Bundler cannot continue.

In Gemfile:
  sass-rails was resolved to 6.0.0, which depends on
    sassc-rails was resolved to 2.1.2, which depends on
      sassc
```

どこで落ちてるか。もう少し上のほうのログを確認する。

```
compiling ./libsass/src/ast.cpp
clang: error: the clang compiler does not support '-march=native'
make: *** [ast.o] Error 1

make failed, exit code 2
```

### 解決策

解決策はここに書かれていた。

https://stackoverflow.com/questions/66456656/error-installing-sassc-clang-compiler-does-not-support-march-native-m1

要するに以下を実行すればよい。

```bash
$ gem install sassc -- --disable-march-tune-native
```

ただ、せっかくGemfileがあるのに別個にgem installするのは面倒である。
これをbundleで実行する方法は以下。

https://stackoverflow.com/questions/9166146/rails-options-for-build-bundler-gemfile

```bash
$ bundle config build.sassc --disable-march-tune-native
```

これを実行後、`bundle install`するとすんなり通る。


# DB(CloudSQLインスタンス)の作成

```bash
# インスタンス名はrails-cloud-run-devとする
$ gcloud sql instances create rails-cloud-run-dev \
    --database-version POSTGRES_14 \
    --tier db-f1-micro \
    --region asia-northeast1
```

インスタンス作成には結構時間がかかる。上記コマンドは12分完了までに必要だった。

## CloudSQLインスタンス内へのデータベースの作成

```bash
# インスタンス名はrails-cloud-run-dev
# データベース名はmain
$ gcloud sql databases create main --instance rails-cloud-run-dev
```

## CloudSQLインスタンスへ接続できるユーザーの作成

```bash
# パスワードを生成し、dbpasswordというファイルに書き込む
$ cat /dev/urandom | LC_ALL=C tr -dc '[:alpha:]'| fold -w 50 | head -n1 > dbpassword
# インスタンス名はrails-cloud-run-dev
# ユーザー名はdevuser
$ gcloud sql users create devuser \
   --instance=rails-cloud-run-dev --password=$(cat dbpassword)
```


# Cloud Storageバケットの作成

`gsutil`コマンドを使う。

https://cloud.google.com/storage/docs/gsutil

> gsutil は、コマンドラインから Cloud Storage にアクセスできる Python アプリケーションです。

awscliでいうaws s3と同じか。GCPではCloud Storageに特化したコマンドとしてgsutilがあるらしい。


```bash
# 最後の引数はgs://[PROJECT_ID]-[MEDIA_BUCKET]
# この例のPROJECT_IDはrails-cloud-run-test
# MEDIA_BUCKETはimage-storage
$ gsutil mb -l asia-northeast1 gs://rails-cloud-run-test-image-storage
```

サブコマンドの`mb`は`Make buckets`の略。


このバケットへは、誰でも見られる画像ファイルを配置する予定のため、バケット内を閲覧可能にする。

```bash
$ gsutil iam ch allUsers:objectViewer gs://rails-cloud-run-test-image-storage
```

`ch` は `change`の略。gsutil iamはサブコマンドとして`get`, `set`, `ch`を持つ。



# Secret Managerにシークレット値を保存する

```bash
$ bin/rails credentials:edit
```

## bin/rails credentials:editでbootsnapのエラー発生

上記コマンドを実行すると以下のエラーが出た。

```
/Users/ryan5500/.anyenv/envs/rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/bootsnap-1.7.5/lib/bootsnap/compile_cache/iseq.rb:13:in `to_binary': wrong argument type false (expected Symbol) (TypeError)
```

Ruby3.0.3で発生するバグらしく、bootsnapの1.9.2で修正されている。

https://github.com/Shopify/bootsnap/blob/master/CHANGELOG.md#192

### 解決策

Gemfileの該当箇所を修正。

```
gem "bootsnap", ">= 1.9.2", require: false
```

```bash
$ bundle update bootsnap
```

これで解決する。

Secret Managerにsecretを登録する。

```bash
# RAILS_SECRET_NAMEはここではrails-secret
$ gcloud secrets create rails-secret --data-file config/master.key
```


Cloud Runサービスにsecretへのアクセス権を付与する。

```bash
# RAILS_SECRET_NAMEはここではrails-secret
# ここでのプロジェクト番号は565343841115
$ gcloud secrets add-iam-policy-binding rails-secret \
    --member serviceAccount:565343841115-compute@developer.gserviceaccount.com \
    --role roles/secretmanager.secretAccessor
```

Cloud Buildサービスアカウントにシークレットへのアクセス権を付与する

```bash
# RAILS_SECRET_NAMEはここではrails-secret
# ここでのプロジェクト番号は565343841115
$ gcloud secrets add-iam-policy-binding rails-secret \
    --member serviceAccount:565343841115@cloudbuild.gserviceaccount.com \
    --role roles/secretmanager.secretAccessor
```


# Rails アプリを本番環境のデータベースとストレージに接続する

.envを作成する。

```
PRODUCTION_DB_NAME: main
PRODUCTION_DB_USERNAME: devuser
CLOUD_SQL_CONNECTION_NAME: rails-cloud-run-test:asia-northeast1:rails-cloud-run-dev
GOOGLE_PROJECT_ID: rails-cloud-run-test
STORAGE_BUCKET_NAME: rails-cloud-run-test-image-storage
```


# Cloud BuildにCloud SQLへのアクセス権を付与する

これは、Cloud Buildでアプリケーションイメージをビルドした後、DBマイグレーションを実行するので、予めCloud BuildにCloud SQLへのアクセス権を付与している。

```bash
# この例ではプロジェクトIDはrails-cloud-run-test
# ここでのプロジェクト番号は565343841115
$ gcloud projects add-iam-policy-binding rails-cloud-run-test \
    --member serviceAccount:565343841115@cloudbuild.gserviceaccount.com \
    --role roles/cloudsql.client
```

Cloud Buildで何を実行するかの指定は、`gcloud builds submit`する際に設定ファイルを指定できて、今回の例では、railsのフォルダ内の`cloudbuild.yml`にて行っている。


# Cloud Buildでビルドを行う

以下で、cloudbuild.ymlを使ってビルドを依頼する。

```bash
# 今回の例ではSERVICE_NAMEはrails-test-service
# INSTANCE_NAMEは、上で決めたrails-cloud-run-dev
# REGIONは、asia-northeast1
# RAILS_SECRET_NAMEは、rails-secret
$ gcloud builds submit --config cloudbuild.yaml \
    --substitutions _SERVICE_NAME=rails-test-service,_INSTANCE_NAME=rails-cloud-run-dev,_REGION=asia-northeast1,_SECRET_NAME=rails-secret
```

このコマンドを打つと、何かアップロードしているログが出る。
CloudBuildのログやストレージの内容を見る限り、以下の処理を行っている。

1. `gcloud builds submit`を実行しているパスのファイル群をtarファイルにまとめ、Cloud Buildの実行用に作成されたCloud Storageにアップロードしている。
2. Cloud Buildの実行用に作成されたCloud Storageから、tgzファイルを取得し、cloudbuild.ymlで指定されたdocker build等のstepを順次実行している。


# ビルドされたRailsアプリイメージをCloud Runへデプロイする

```bash
$ gcloud run deploy rails-test-service \
     --platform managed \
     --region asia-northeast1 \
     --image gcr.io/rails-cloud-run-test/rails-test-service \
     --add-cloudsql-instances rails-cloud-run-test:asia-northeast1:rails-cloud-run-dev \
     --allow-unauthenticated
```

デプロイ完了後、アクセスできるURLが表示される。

URLにアクセスしたところ、レスポンスが戻るのに5秒ぐらいはかかる。(初回起動かつRailsであるため)
前回Sinatraだと3秒程度だったので、倍ほどかかる。


# 気になった点

## database.ymlのhost設定が謎

```yaml
production:
  <<: *default
  database: <%= ENV["PRODUCTION_DB_NAME"] %>
  username: <%= ENV["PRODUCTION_DB_USERNAME"] %>
  password: <%= Rails.application.credentials.gcp[:db_password] %>
  host: "<%= ENV.fetch("DB_SOCKET_DIR") { '/cloudsql' } %>/<%= ENV["CLOUD_SQL_CONNECTION_NAME"] %>"
```

hostが`/cloudsql/rails-cloud-run-test:asia-northeast1:rails-cloud-run-dev`これになるのだが、どうやって接続しているのか。

https://stackoverflow.com/questions/27036414/rails-postgresql-socket-path

adapterがmysqlだとsocketという設定値でソケットを使うが、adapterがpostgresqlだとhostの設定値でソケットを指定できるらしい。つまりhostの値はパスである。

ここにも記載があった。

https://cloud.google.com/ruby/rails/run#database_connection

> アプリは、Cloud Run（フルマネージド）で実行されると、Cloud Run 環境から提供されるソケットを使用して PostgreSQL インスタンスに接続されます。アプリは、ローカルマシンで実行されると、Cloud SQL Auth プロキシを使用して PostgreSQL インスタンスに接続されます。