初投稿です。Rails で本格的なWebアプリを作れるようになろうと独学中です。
アイデアが一定程度Railsアプリ(Rails7+MySQL)の形になってきたのでいざ動かさん、と
構築が簡単なようで費用負担も許容範囲に収まりそうな Google App Engine のスタンダード環境であれやこれや調べながら試行錯誤したのですが、
動くようになるまでかなり苦労した――元々インフラ周りが特に苦手なのが大きいですが――ので顛末を書き残しておきます。
ローカル環境
ローカル環境はWindows 10 Pro、Ruby3.2.2、Rails7.1.2。
Docker 等の仮想環境や rbenv1 等のバージョン切替などは特に入れていません。
基本手順
概ね公式ドキュメントや下記参考記事に則ったものなので詳細は割愛します。
その通りやっただけでは解決しなかった問題とその対処を次章以降で詳細に記します。
※Cloud SDK はインストール・セットアップ済前提
参考記事
① Google Cloud の管理画面から以下を作成
「➡」以下の値を全て控えておく
- (Google Cloud のアカウント自体を新規に作成した場合のみ) 請求先アカウント2
- プロジェクト(「リソースの管理」より) ➡プロジェクトID
-
Cloud SQLのインスタンス&ユーザー&データベース ➡ユーザ名/パス/接続名
接続は「プライベートIP」(ネットワーク:default)を選択、その他設定項目は適当に3 -
サービスアカウント&keyfile(IAMと管理>サービスアカウント)
ロールは Cloud Storage>ストレージ管理者、Cloud SQL>Cloud SQLクライアント(phpMyAdmin用)
サービスアカウントが作れたら選択して『キー』タブ>『鍵を追加』>『新しい鍵を作成』>『JSON』選択して『作成』を実行、
DLしたjsonファイル=keyfileをプロジェクト内に適宜配置 ➡そのファイルパス -
Cloud Storage のバケット
設定項目は適当に ➡バケット名 -
VPCコネクタ(「サーバーレス VPC アクセス」より)
カスタムIP範囲:10.8.0.0(/28)、ネットワークは「default」でOK ➡コネクタ名
SQLインスタンス/VPCコネクタ作成時にAPI有効化が複数要求されるので全て有効化する
リージョンは後述の AppEngine($ app create
で作成時に入力)含め全て揃える
安くて比較的近いのは台湾(asia-east1) レイテンシ重視なら日本国内(asia-northeast1/2)、価格重視ならオレゴン(us-west1)等でも
2024/7/8追記:
Cloud SQLの接続を「プライベートIP」とする前提で書いていますが、それに必要なVPCコネクタが少々金食い虫4なので
セキュリティ面等に問題がなければ VPCコネクタが不要な「パブリックIP」の方が費用面ではよさそうです。
現時点でまだ試していないので、試した後に別途記事にしたいと思います。
どうしてもプライベートIPでないといけない場合、開発中の費用を抑えるには利用(終了)時にVPCコネクタを都度作成(・削除)する必要があります。(参考記事は下記)
これとセットでCloud SQLインスタンスの開始/停止も利用(終了)時に都度行うようにすれば、最低限のコスト(非利用時¥10/日程度)で開発用環境の維持が可能です。
② credentails を作成
①で取得したDBのユーザー/PW/接続名や、シークレットキー($ rails secret
で取得)といった秘匿情報を
下記コマンドで本番環境用 credentials ファイルに登録します。
$ EDITOR="vi" rails credentials:edit -e production
VScodeで編集したい場合は EDITOR="vi"
のところを EDITOR="code --wait"
とします。(参考記事は下記)
編集結果はこんな感じになると思います。
db:
user: your_db_user_name
pass: your_db_password
socket: /cloudsql/your-project-name:region-name:your-db-name
secret_key_base: xxxxxxxx
# メール送信が必要ならその設定等も
2024/7/8追記:
当初記載では秘匿情報の管理手法が曖昧だったため、rails7.1以降で推奨の credentials での管理を前提とした記載に置き換えました
※この辺の詳細はGAEの本筋からは外れるので詳細は credentials が主題の記事を参照願います。
③ app.yaml を作成
インスタンスの種類・性能、entrypoint
(起動時コマンド)、VPCコネクタ名 等を指定
# だいたいこんな感じになる
runtime: ruby32
env: standard
instance_class: F1
entrypoint:
bundle exec rackup --port $PORT
env_variables: # 詳細後述
RAILS_SERVE_STATIC_FILES: true
PIDFILE: "/tmp/puma.pid"
vpc_access_connector:
name: <VPCコネクタ名>
④ Cloud Storage 設定記述
config/storage.yml
に プロジェクト名・バケット名・①で取得した keyfile のパス を記載
gcp:
service: GCS
project: "Your Project Name"
credentials: <%= Rails.root.join("your_keyfile_path.json") %>
bucket: your-storage-bucket-name
使用ストレージ(config/environments/production.rb
の active_storage.service
)を
storage.yml で指定した名称(上記の例だと :gcp)に設定
config.active_storage.service = :gcp
keyfile は秘匿情報なので .gitignore
へも登録
⑤ Gemfile に必要なgemを追加
gem "google-cloud-storage"
gem "appengine" # migration を app.yaml から行わせればなくても事足りる(詳細後述)
⑥ .gcloudignore を作成
デプロイ対象/対象外ファイルを指定
.gcloudignore
#!include:.gitignore # .gitignore の内容取込み
.git
.gitignore
test/*
# 以下git管理外だがデプロイには必要なファイル/ディレクトリ(「!」で .gitignore 設定を打消し)
!/config/credentials/*.key # ②で生成した credentials のキーファイル
!storage_credentials.json # ①で取得・④で設定した Cloud Storage の keyfile
!/public/assets # 詳細後述
⑦ 下記コマンドを逐次実行
# Gemfile に追加したパッケージのインストール
$ bundle install
# assets のプリコンパイル
# config.assets.compile = true なら不要だが本番では非推奨(詳細後述)
$ rails assets:precompile
# gcloud プロジェクトのセット
$ gcloud auth application-default set-quota-project <PROJECT ID>
$ gcloud config set project <PROJECT ID>
# 上記の手順中では有効化されない必要APIの有効化
$ gcloud services enable sqladmin.googleapis.com
$ gcloud services enable serviceusage.googleapis.com
# AppEngine のサービス(アプリ)を作成し初回デプロイ
$ gcloud app create
$ gcloud app deploy
# IAMポリシーの追加 ※コロン後に空白入れないこと
$ gcloud projects add-iam-policy-binding <PROJECT ID> --member=serviceAccount:<PROJECT番号>@cloudbuild.gserviceaccount.com --role=roles/editor
# <PROJECT番号>は $ gcloud projects list で確認可能
API有効化やリージョン選択等要求されたら応えること
IAMの設定はデプロイ前だとエラーになるのでこの順番で
⑧ phpMyAdmin5 セットアップ
公式チュートリアルは MySQL5.7以前・php5.5以前・phpMyAdmin4.9 以前の場合しかないので下記記事を参考に(これで MySQL8.0 でも行けました)
マイグレーションが実行できない(500エラーになる)
デプロイ=ソースを転送後マイグレーションを appegine gem の機能を利用して
ローカルから下記コマンドで行う――というのはどの手順書にもありますが、
$ bundle exec rake appengine:exec -- bundle exec rake db:migrate
これが通らず下記のような感じの500エラーが返ってきてしまいます。
---------- CLEANUP ----------
Deleting the following versions:
- your-project-id/default/appengine-exec-YYYYMMDDHHmmss
Deleting [default/appengine-exec-YYYYMMDDHHmmss]...done.
rake aborted!
JSON::ParserError: unexpected token at '<html><head> (JSON::ParserError)
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>500 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>
'
インスタンスの性能を絞っているせいもあるのか、コールドスタート時(=非スタンバイ状態でリクエストが投げられた際)は
アプリの起動(=app.yaml
の entrypoint
に指定したコマンドの実行)から行われるためレスポンスが大幅に遅くなります。
マイグレーション直後もアプリの再起動が必要なため同様で、マイグレーションコマンドの後処理でサイトにアクセスする際に時間がかかって500を吐いているのかな…と想像しています。(全然見当違いかもしれませんが)
上記の想像が間違っていなければ、起動速度の大幅な改善しか解決の手立てがなくインスタンスの性能を上げるほかなさそうな気がしますが試していません6。
対症療法としてマイグレーションは $ rake appengine:exec
から行うのでなく
app.yaml
の entrypoint
を以下のように指定して起動時の前処理で実行することで
ひとまず見えている範囲ではエラーや警告はなく動かせています。
entrypoint:
bundle exec rake db:migrate && bundle exec rackup --port $PORT
参考
pidファイルが作れず 502 Bad Gateway になる
デプロイしても Nginx の 502 Bad Gateway 表示になりアプリのページが表示できないのでログを確認したところ
bundler: failed to load command: rackup (/workspace/.bundle/gems/ruby/3.2.0/bin/rackup)
/layers/google.ruby.bundle/gems/.bundle/gems/ruby/3.2.0/gems/puma-6.4.0/lib/puma/launcher.rb:312:in `write': No such file or directory @ rb_sysopen - tmp/pids/server.pid (Errno::ENOENT)
と tmp/pids/server.pid
ファイルがなく書き込みができないと怒られていました。
AppEngine でも Puma サーバで Rails アプリを動かす場合 pidファイルは config/puma.rb
で指定したパスに作成されます。
$ rails new
以降特に弄っていなければ下記のように
PIDFILE
環境変数が設定されていればその値、未設定ならアプリケーションディレクトリ配下の tmp/pids/server.pid
になっているかと思います。
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } # ◆
AppEngine ではアプリケーションにファイル・フォルダ作成権限があるのはルートの /tmp
のみと何かで見た気がするので
その通りならば ちゃんと裏は取れていないのですが アプリケーションディレクトリ配下の tmp/pids/
が作成できないため生じていると思われます。
対処法として考えられるのは以下3つ(1、3は動作確認済)。
後の方ほど強引度が上がっていきます… アリなのかタブーなのかはよく分かりません🙄
-
PIDFILE
環境変数の設定
環境変数の設定(app.yaml
のenv_variables
)でPIDFILE
に書込み可能なパス7を指定 -
config/puma.rb
のpidfile
を書き換える
PIDFILE
未設定時の値(上記◆印の{ }
内)を書込み可能なパス7に変更 -
アプリケーション配下の
tmp/pids
をデプロイ時にmkdir
してしまう
前述の migration の件と同じ考え方で、app.yaml
のentrypoint
にmkdir -p tmp/pids &&
を前付けしてもフォルダは作れて動かせます。
アプリから作れないならデプロイ時のコマンドで作ってしまえ、という(
参考
assets が読み込めず 503(Service Temporarily Unavailable)になる
rails の標準出力に下記、もしくは類するエラーが出ている場合は
ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.)
precompile された assets、即ち public
配下の静的ファイルを参照できていないので
参照する設定になっているかどうか、そしてその対象が正しく配置できているか確認が必要です。
前者:設定については config/environment/production.rb
内の config.public_file_server.enabled
が true
になるような対処が必要です。
デフォルトでは ENV["RAILS_SERVE_STATIC_FILES"].present?
になっているはずで、それをそのまま true に書き換えてももちろん動きはしますが
見ての通り RAILS_SERVE_STATIC_FILES
環境変数が存在していれば true
になる仕組みなので app.yaml
の env_variables
で設定してやります(存在さえしていれば present?
が true
になるので値は何でも可)。
env_variables:
RAILS_SERVE_STATIC_FILES: true
ちなみにHerokuや、AppEngineでもフレキシブル環境なら明示的に設定しなくともランタイムで既に設定されているそうです。
ここまでは言及している記事が上記以外にも多数ありますが、私の場合もう一点ハマりまして
.gcloudignore
の設定不備で public/assets
がデプロイ時に転送されていませんでした。
public/assets
は通常git管理しないので $ rails new
時点から .gitignore
に列挙されていますが
その内容を .gcloudignore
にもまるっと取り込んでいる場合は、デプロイの対象にはなってくれないと困るので除外を打ち消す設定を追加する必要があります。
それになかなか気付きませんでした。。(assets周りの理解も浅かったことを痛感)
具体的には .gcloudignore を下記のように記述します。
#!include:.gitignore # ←これで .gitignore の内容をまるっと取り込んでいる場合は
!/public/assets # ←この記載を入れる
$ gcloud meta list-files-for-upload
でデプロイ対象ファイルのリストが出せるので
その中に public/assets も含まれていることを確認するとよいんじゃないかと思います。
なお .gcloudignore
の設定は正当で public/assets
がデプロイ対象になっていても
$ rails assets:precompile
をローカルで行っていないとそもそも静的ファイルが作られず同様にエラーになります。
実験のため一旦ローカルの precompile した assets を $ rails assets:clobber && rails assets:clean
で削除し再デプロイしたらエラーが再現できました。
またプリコンパイルをデプロイ後 AppEngine 上で行えないか、$ bundle exec rake appengine:exec -- bundle exec rake assets:precompile
とやってみたり
前述の migration 出来ない問題と同じ発想で entrypoint
に追加してみたりといった実験も行いましたがいずれもダメでした。
ちなみに assets が存在しなかったら動的に生成(コンパイル)するという設定が config.assets.compile = true
にすればできてしまうので
上記2点の設定や assets:precompile
の実施有無など関係なくこれでエラー回避できることも確認済です。
ただ基本的にこの設定は開発環境用で、assets をキャッシュするので余計なメモリを食ったりそのキャッシュを当てに行く処理も走ったりでパフォーマンスが落ちるので本番環境ではタブーとのこと。
AppEngine だと費用にも影響があるかもしれないので、よほどパフォーマンスも出費も気にしない場合以外はやめておいた方がよさそうです。
その他こまごま
Gemfile の Ruby バージョン指定
例えば Ruby3.2.2 で開発していて、Gemfile にもそのバージョンを
下記のようにマイナーバージョンまで細かく指定していると
ruby "3.2.2"
$ gcloud app deploy
時に以下のように怒られて失敗します。
Your Gemfile cannot restrict the Ruby version to "3.2.2". Consider instead: ruby '~> 3.2.0'
これはもうこの警告にある通り、下記のように書くことで通せます。
ruby "~> 3.2.0"
何もその程度のことでエラーで落とさなくても・・・と思いましたが、
恐らく背景としては AppEngine runtime のラインナップがマイナーver.ごとに1つ(最新版?)でパッチver.全部を取り揃えてはおらず
厳格に指定されるとその通りのバージョンの runtime がない、ということになり得るので厳格なチェックにしているものと想像します。
CloudSQL の接続がプライベートIPのみの場合の phpMyAdmin 設定
CloudSQL インスタンスの接続をプライベートIPのみとしている場合、phpMyAdmin のデプロイ設定ファイル(app.yaml
)にもVPCコネクタの設定を入れる必要があります。
設定は rails アプリ側のそれと全く同じ記述でOKです(下記)。
vpc_access_connector:
name: <コネクタ名>
この設定がない場合、phpMyAdmin のログイン画面は出ますが正しいユーザーID/パスワードを入れても mysqli::real_connect(): (HY000/2002): Connection refused
でログインできません(下記)。
当たり前といえば当たり前なのですが忘れがちなのでもしこの症状があれば要確認です。
以下調査不能
改めてナレッジをまとめる際に再現等確認できたものは以上ですが、初回試行時は他にもかなーりあれこれ躓いてました。。 (インフラ苦t
以下記録が残ってなく裏も取れてないのであやふやな記述しかできず恐縮ですが記憶にある範囲で躓いた内容とその対処を記しておきます。
gcloud CLI(Cloud SDK) が古い問題
そんな人はあまりいないとは思うのですが、私の手元では gcloud CLI がだいぶ前にインストール・セットアップしたままでかなり古かったようで最初のうちの失敗はそれも原因にあったようです。
当てはまるなら下記をまず実行しておきましょう。
$ gcloud components update
※必要な状況なら実行するよう促されるので、エラーや警告をマメに見ている方なら適用済だとは思います
appengine gem のバージョン
最初 Gemfile
にバージョン指定を記載せず $ bundle install
したところ依存関係の影響?かどうやらかなり古いバージョンが入ってしまったようで
それ起因っぽいエラーもあったので下記のようにバージョン指定して $ bundle install
しなおした記憶があります。
gem "appengine", "~> 0.7.0" # 実践時点の最新バージョン
ただそもそもこのgem、AppEngine にデプロイしたアプリのrakeタスクをローカルから実行するときに必要なだけなので、
前述のようにマイグレーションの実行コマンドを app.yaml
に仕込んでしまえばなくても困ることはあまりなさそうです。
tmp/sockets
, app/assets/images
がないと怒られる
デプロイのトライ&エラーを繰り返している最中、前述のpidファイルの問題と同じような事象がこれらのディレクトリでも起きたことがありました。(但し詳細なエラー・事象は記録がなく再現もできず不明)
pidfile
問題の対処法3.のように一度 entrypoint
に mkdir -p
を入れて強引に解決?しましたが
その後その mkdir
を削除しても(そしてデプロイファイルの中間保存を削除し再度全アップロードしても:その手法は後述)問題なくデプロイできています。
当初の失敗はその際 実は assets:precompile
に失敗していてそれが原因だったとか?などと考えたりもしますが assets/image
はともかく sockets
は関係ないでしょうし詳細は謎のままです。。8
もし何がしか知見をお持ちの方おられましたら補足頂けますと幸いです。
おまけ
ファイル名間違え
デプロイ対象外ファイルの指定を .gcloudignore
ファイルで行いますが、なんか 'ignore' をtypoする手癖があるらしく(gitignoreもよくgitig 'o' noreになってる)
ファイル名が .gcloudig 'o' nore になっていたのに全然気付かなかったおかげで対象除外が全然効かないばかりか .gitignore
の反映(取込み)すらできていないため莫大な数のファイルをアップロードする羽目になっていました。。
それでも Rails7 はファイル数が抑えられているので何千ファイルで収まりますが、アレコレ試している最中に一度 Rails6 に落とたりもしてみた際は AppEngine のデプロイファイル数上限(一万)に抵触してしまいデプロイできませんでした。😂
実際には .gitignore
で無視するようなファイルの内転送が必要なのは前述の public/assets
くらいで
それ以外は殆ど不要(動かすのに必要なものでも元々 AppEngine の runtime に入っているか、コンテナ立ち上げ時に bundler で構築される)だし
テストコードとかも普通は不要なはずなので test
ディレクトリなんかもまるっと除外してしまえば 大規模なシステムでない限り数百のオーダーで収まるはずです。
デプロイするファイルの転送経路
ちなみにデプロイされるファイルは、AppEngine とは別にこれ用に自動で作られる Cloud Storage の専用バケットに格納してからデプロイし
2回目以降はそれと比較して変更が生じたファイルだけ専用バケットにアップロード➡デプロイ、という仕組みになっているようです。
バケットの中身を削除してまるまるアップロードしなおしたい場合は下記で削除できます。
$ gcloud storage rm -r gs://staging.<project ID>.appspot.com/*
なお中身でなくバケット自体を削除してしまうと悲惨なことになるので絶対にやらないようにしましょう。
運がよければ $ gcloud beta app repair
で修復できますが(経験済)、ダメだったらプロジェクトから全て作り直すしかない・・・と思います9。
エラーの詳細把握
以上のようにいろいろ躓きポイントがありかなり時間がかかりましたが、地味に一番時間を溶かしたのはそもそもエラーの詳細がどこに出るのか分かってなく
Google Cloud コンソールのログエクスプローラに全部出るもんだと思っていて(実際には $ gcloud app logs tail
で標準出力を追わないと詳細が掴めない場合が多い)
具体的な情報がさっぱり得られない10まま当てずっぽうで試行錯誤するしかなかった当初の状況でした。。 どれくらい溶かしたか恥ずかしくて書けないレベルです()
いや、確かに $ gcloud app deploy
した後にログを追うには
$ gcloud app logs tail -s <サービス名:通常は default>
してね、って言われるんですが、思い込みで試しもしないでログエクスプローラと同じ内容しか出てこないもんだと思い込んでいたので。。 思い込みホントヨクナイ
ただ逆に、場合によっては標準出力だけ追っても有用な情報に乏しくログエクスプローラの方を見ないと状況が把握できないこともあるようなので(どういう時だとそうなのかまだしっかり理解できていません。。)
勘所が掴めて来るまでは両方、というか手がかりがありそうなフシは片っ端から確認するのが大事なようです。(まあこれは言語とか環境とか関係なく全てについて言えることですね😅)
-
そもそも rbenv は Windows には対応していないので仮想環境を入れない限り使えない ↩
-
先に作っておかないと次のプロジェクト作成の際に課金が自動で有効にならない ↩
-
最安構成にするなら Enterprise/サンドボックス/シングルゾーン/共有コア/HDD10GB ↩
-
バックで実質的にGCEを常時動かすため。当方の設定ではおよそ¥60/日かかっていた ↩
-
普通 Rails はローカル SQLite、本番 PostgreSQL が定番ですが使い慣れたGUIクライアントのこいつを使いたかったので MySQL です。でもそういう需要って意外とあるのでは? ↩
-
お金もかかる?ちょこっと試すだけならそんなにかからない? ↩
-
前述の assets 絡み
.gcloudignore
不備も怪しいっちゃ怪しい ↩ -
自動生成バケットの名が特殊で Cloud Storage のコンソールからは同じ名前のバケットが作れないため(どうにか作れる方法 or AppEngine 側のデプロイ用バケットを変更する方法がもしかしたらあるのかもしれませんが) ↩
-
The request failed because the instance failed the readiness check.(503)
とか
The request failed because the instance could not start successfully(500)
とか ↩