App Engine + Cloud SQL + Cloud Storage で脱 Heroku をした

Heroku から離れる経緯

使っていて良い点

  • 簡単に deploy できてとても良い
  • DB との接続も簡単

気になった点

  • 東京リージョンが個人だと使えない(?)エンタープライズ契約だと使えるらしいけど個人では…。
  • 独自ドメインでルートドメインがだいたい使えない (https://devcenter.heroku.com/articles/apex-domains)
    一応 ANAME とかが使えるところだと大丈夫らしい

ルートドメインが使えなかったのが最大の要因で GCP に移動することにした。


以下、Heroku で動かしていた Rails アプリを App Engine に持ってくるときの試行錯誤

Cloud SQL

データベースは PostgreSQL を使う
ちゃちゃっとブラウザ経由で作った。

あとで使うので、以下の項目をメモしておく

  • ユーザー名
  • パスワード
  • インスタンス接続名

ローカルから Cloud SQL のデータを確認する

MySQL のだけど、これを見ながらやるとよい。
https://cloud.google.com/sql/docs/mysql-connect-proxy?hl=ja

IAMと管理 > サービスアカウント から新しいサービスアカウントを作成して認証キーファイルを保存する

サービスアカウント作成キャプチャ

↓のコマンドは実行ファイルをコマンド叩いたときのカレントディレクトリに保存するので、保存先変えたい場合はなんとかしてくれ。

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

# TCP ソケットの場合 (だいたいこっちを使うと思う)
./cloud_sql_proxy -instances=[INSTANCE_CONNECTION_NAME]=tcp:5432 \
                  -credential_file=[PATH_TO_KEY_FILE] &

# UNIXソケットの場合
sudo mkdir /cloudsql
sudo chmod 777 /cloudsql
./cloud_sql_proxy -dir=/cloudsql -instances=<INSTANCE_CONNECTION_NAME> \
                  -credential_file=<PATH_TO_KEY_FILE> &

エラーになった。

$ 2018/07/22 22:48:26 using credential file for authentication; email=cloud-sql-editor@xxxxxxx.iam.gserviceaccount.com
2018/07/22 22:48:27 errors parsing config:
        googleapi: Error 403: Access Not Configured. Cloud SQL Administration API has not been used in project xxxxxxx before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/sqladmin.googleapis.com/overview?project=xxxxxxx then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry., accessNotConfigured

Cloud SQL Admin API というのを有効化すればOK
https://console.cloud.google.com/apis/library/sqladmin.googleapis.com

再度 cloud_sql_proxy を実行
これを動かしているときに DataGrip などでローカルにつなぐノリで設定すればOK

データベース接続手順

App Engine

Rails を動かす準備
以下のリンクを参考にした。
https://cloud.google.com/ruby/rails/appengine

app.yaml

entrypoint: bundle exec rails server -p $PORT
env: flex
runtime: ruby
skip_files: 👈 deploy 時に持っていかないファイル。正規表現で指定する
  - ^vendor
automatic_scaling: 👈 オートスケーリングの設定
  min_num_instances: 1
  max_num_instances: 5
  cool_down_period_sec: 120
  cpu_utilization:
    target_utilization: 0.6
env_variables:
  SECRET_KEY_BASE: xxxxx
  POSTGRES_USER: xxxxx
  POSTGRES_PASSWORD: xxxxx
  POSTGRES_DATABASE: xxxxx
  POSTGRES_HOST: /cloudsql/[INSTANCE_CONNECTION_NAME] 👈 Cloud SQL との接続で必要
  TZ: Asia/Tokyo
beta_settings:
  cloud_sql_instances: [INSTANCE_CONNECTION_NAME] 👈 Cloud SQL との接続で必要

マニュアルだと rackup でサーバーを起動しているけど Heroku だと rails server で動かしているし、 rails server にした。

以下の記事によると

Rails::Serverでは、opt_parserをオーバーライドしてrails s特有のオプションを定義したり、デフォルトポートを3000にしたり、RAILS_ENVに応じた処理が定義されている。
Railsアプリにもconfig.ruはおいてあるが、rackupを叩いてしまうとRails::ServerではなくRack::Serverがstartされるので、これらの恩恵を受けることができなくなる。

https://qiita.com/k0kubun/items/248395f68164b52aec4a

とのこと。

Gemfile

appengine を追加した。

appengine gem には Rake タスク appengine:exec が備わっており、App Engine 本番環境における最新のデプロイ済みバージョンのアプリケーションに対してコマンドを実行します。

とのこと。

https://cloud.google.com/ruby/rails/using-cloudsql-postgres?hl=ja

config/database.yml

production:
  <<: *default
  username: <%= ENV['POSTGRES_USER'] %>
  password: <%= ENV['POSTGRES_PASSWORD'] %>
  database: <%= ENV['POSTGRES_DATABASE'] %>
  host: <%= ENV['POSTGRES_HOST'] %>

deploy 準備

App Engine は Heroku と違い、サーバー側で assets:precompile してくれないので自分で行う。

RAILS_ENV=production bundle exec rails assets:precompile

デプロイする

# 初回のみ実行
gcloud app create --project PROJECT_NAME

# デプロイ
gcloud app deploy --project PROJECT_NAME

デプロイするとバージョンが増えるので古いバージョンを忘れずに削除すること
うっかりするととんでもなくお金かかる。
ちなみにすぐ削除しようとするとエラーになるっぽいので、数分待ってからやると良い。

appengine gem に必要な権限を付与する

appengine gem 経由で DB アクセスするには Cloudbuild サービスアカウントにアクセス権を付与する必要があるらしい。
以下のコマンドでプロジェクト一覧を取得し、使うプロジェクトの PROJECT_ID と PROJECT_NUMBER をメモしておく

gcloud projects list

以下のコマンドを叩いて権限を付与する

gcloud projects add-iam-policy-binding [PROJECT_ID] \
  --member=serviceAccount:[PROJECT_NUMBER]@cloudbuild.gserviceaccount.com \
  --role=roles/editor

DB構築

CLOUDSDK_CORE_PROJECT は gcloud config set project していない場合のプロジェクト指定用環境変数。

CLOUDSDK_CORE_PROJECT=[PROJECT_NAME] bundle exec rake appengine:exec -- bundle exec rake db:create
CLOUDSDK_CORE_PROJECT=[PROJECT_NAME] bundle exec rake appengine:exec -- bundle exec rake db:migrate
CLOUDSDK_CORE_PROJECT=[PROJECT_NAME] bundle exec rake appengine:exec -- bundle exec rake db:seed

こんな感じのログが出ればちゃんと成功している

---------- CONNECT CLOUDSQL ----------
cloud_sql_proxy is running.

---------- EXECUTE COMMAND ----------
bundle exec rake db:create
Created database 'xxxxx'

---------- CLEANUP ----------
PUSH
DONE

Cloud Storage

CarrierWave を使って Cloud Storage にアップロードをしているので、ついでに書いておく。

CarrierWave.configure do |config|
  config.cache_dir = "#{Rails.root}/tmp/uploads"
  # プロバイダ
  config.fog_provider = 'fog/google'
  # 認証情報
  config.fog_credentials = {
      provider:                         'Google',
      google_storage_access_key_id:     Rails.application.secrets.gcp[:google_storage_access_key_id],
      google_storage_secret_access_key: Rails.application.secrets.gcp[:google_storage_secret_access_key]
  }
  # バケット名
  config.fog_directory = 'bucket_name'
  # 一般公開するかどうか
  config.fog_public = true
  # fog_public = false の場合の署名URLの有効時間(s)
  # config.fog_authenticated_url_expiration = 600
  # config.fog_attributes = {
  #     cache_control: "public, max-age=#{365.days.to_i}"
  # }
  config.storage = :fog
end if Rails.env.production?

google_storage_access_key_id と google_storage_secret_access_key は GCP の Storage > 設定 > 相互運用性 から作成できる。

Cloud Storage 画面キャプチャ

fog_public による挙動は以下の通り

  • 共通:
    • Cloud Storage 側の設定である デフォルト ALC は機能しないっぽい
      多分どこかで ALC 情報をいれて保存しにいっていてデフォルト ALC が使われていないと予想
  • true:
    • Cloud Storage にアップロードされた画像は 一般公開 で保存される
    • https で配信される
  • false:
    • Cloud Storage にアップロードされた画像は 非公開 で保存される
    • fog_authenticated_url_expiration で指定した秒数で署名付き URL を生成する
    • 設定があるかどうかわからないが、 http で配信されてしまう

署名付き URLについては https://cloud.google.com/storage/docs/access-control/create-signed-urls-gsutil を参照すると良い。

今回は一般公開するようにした。

ACL の設定

現在はブラウザからだとファイル単位でしか一般公開できないようなので、コマンドで指定バケット内のファイルをすべてを一般公開にする

ちなみに ACL はアクセス制御リストのことらしい。
ACL の詳細は https://cloud.google.com/storage/docs/access-control/lists?hl=ja

すでに保存されているファイルに対し一般公開の設定を行う。

※ ACL はバケット単位か、オブジェクト(ファイル)単位のみで設定される。(途中のディレクトリには ACL などは付与できない)
これは確かディレクトリの概念は実際にはなく、バケット直下に全部保存されているみたいな感じだったからだと思う。

gsutil -m acl set -R -a public-read gs://bucket_name
  • -m: 一部のサポートしているコマンドを並列で実行する
  • -R: 再帰的に処理を行う
  • -a: すべてのオブジェクトのバージョンに対し処理を行う

確認してみる。

$ gsutil -m acl get gs://xxxxx/
[
  {
    "entity": "project-owners-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "owners"
    },
    "role": "OWNER"
  },
  {
    "entity": "project-editors-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "editors"
    },
    "role": "OWNER"
  },
  {
    "entity": "project-viewers-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "viewers"
    },
    "role": "READER"
  }
]

バケットはデフォルトのまま。

$ gsutil -m acl get gs://xxxxx/uploads/123456789.png
[
  {
    "email": "xxxxx@example.com",
    "entity": "user-xxxxx@example.com",
    "role": "OWNER"
  },
  {
    "entity": "project-owners-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "owners"
    },
    "role": "OWNER"
  },
  {
    "entity": "project-editors-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "editors"
    },
    "role": "OWNER"
  },
  {
    "entity": "project-viewers-xxxxx",
    "projectTeam": {
      "projectNumber": "xxxxx",
      "team": "viewers"
    },
    "role": "READER"
  },
  {
    "entity": "allUsers",
    "role": "READER"
  }
]

ファイルには

{
    "entity": "allUsers",
    "role": "READER"
}

という ACL が付与された。これが一般公開の ACL らしい。(もうひとつユーザーの ACL 出てるけど関係ないので無視)

デフォルト ACL の設定

メモがてら残しておくけど、以下はやらなくて良い。

この設定は CarrierWave によるアップロードでは意味をなしませんでした。
ブラウザ上から手動でアップロードしたときに適用されていました

https://cloud.google.com/storage/docs/access-control/create-manage-lists#defaultobjects

既存のデフォルト ALC に allUsers を追加

gsutil defacl ch -u allUsers:READ gs://bucket_name

注意

安易に ↓ をやると

gsutil defacl set public-read gs://bucket_name

デフォルト ACL が allUsers だけになってしまう。

$ gsutil defacl get gs://bucket_name
[
  {
    "entity": "allUsers",
    "role": "READER"
  }
]

やらかしてしまったら以下のコマンドでデフォルト値に戻しておいたほうが良いと思う。

gsutil defacl set project-private gs://bucket_name