Rails 5.1 on Google App Engine

## はじめに
「Railsを動かすならHeroku」の方が圧倒的に多いかと思いますが、Google App Engine（以下：GAE）でもFlexible Environmentにはなりますが、動かせます。

Docker化しているアプリケーションであれば、後々はk8s等の恩恵も受けたいはずで、それを考慮に入れてくると、GCP上に載せておくことは良いことかもしれません。
また、BigQueryやStackdriverなど、魅力的なプロダクトに繋げやすいのもGCPの利点です。

あまり、RailsをGoogle App Engineに載せている記事を見かけなかったので、簡単にまとめてみました。

Herokuデプロイ時に比べ、いくつかハマるポイントがありました……。
ここでは、Rails v5.1以上を対象に記載します。

## GAEのRuby Base ImageのDockerfileを覗いてみる
まずは、GAEが使用するRuby Base ImageのDockerfileを覗いてみましょう。
~~https://github.com/GoogleCloudPlatform/ruby-docker/blob/master/appengine/image_files/Dockerfile~~ (リンク切れ)
**（2017-10-06 修正）**
https://github.com/GoogleCloudPlatform/ruby-docker/blob/master/ruby-base/Dockerfile

大事な部分だけ、切り出します。

```dockerfile:Dockerfile(BaseImage)
FROM gcr.io/google-appengine/debian8

---(snip)---

# (1)
RUN mkdir /nodejs && curl -s https://nodejs.org/dist/v6.11.1/node-v6.11.1-linux-x64.tar.gz | tar xvzf - -C /nodejs --strip-components=1

---(snip)---

# (2)
ENV DEFAULT_RUBY_VERSION 2.3.4
ENV BUNDLER_VERSION 1.15.1

---(snip)---

# (3)
ENV RACK_ENV=production \
    RAILS_ENV=production \
    APP_ENV=production \
    RAILS_SERVE_STATIC_FILES=true \
    RAILS_LOG_TO_STDOUT=true

# (4)
WORKDIR /app
EXPOSE 8080
ENV PORT=8080
ENTRYPOINT []
CMD []
```

(1)(2) デフォルトのRubyバージョンは、`2.3.3`、Nodeは`6.11.1`です。（このあたりはよく更新されます。）

(3) Heroku同様、`RAILS_ENV`や、`RAILS_SERVE_STATIC_FILES`など必要な環境変数は、コンテナビルド時に既にセットされます。

(4) ポイントとしては、`8080`番ポートをリッスンしているところでしょうか。
GAEは、言語に限らず`8080`番しかエントリーポイントとして許可しないのが習慣です。Railsサーバの起動は`8080`番で行いましょう。

## gcloud beta app gen-config で作られるDockerfileを覗いてみる
先ほどのBase Imageを使って、デプロイ用のコンテナがビルドされます。
`app.yaml`で`runtime: ruby`を指定した場合のDockerfileは、以下のコマンドで確認することができます。

```bash
$ gcloud beta app gen-config --custom

This looks like a Ruby application. Please confirm the command to run
as the entrypoint: [bundle exec rackup -p $PORT]:
Writing [app.yaml] to [/Users/takamario/qiita-appengine-rails-5.1].
Writing [Dockerfile] to [/Users/takamario/qiita-appengine-rails-5.1].
Writing [.dockerignore] to [/Users/takamario/qiita-appengine-rails-5.1].
```

```dockerfile:Dockerfile(GAEデプロイ用)
FROM gcr.io/google-appengine/ruby:latest

# (1)
ARG REQUESTED_RUBY_VERSION=""

RUN if test -n "$REQUESTED_RUBY_VERSION" -a \
        ! -x /rbenv/versions/$REQUESTED_RUBY_VERSION/bin/ruby; then \
      (apt-get update -y \
        && apt-get install -y -q gcp-ruby-$REQUESTED_RUBY_VERSION) \
      || (cd /rbenv/plugins/ruby-build \
        && git pull \
        && rbenv install -s $REQUESTED_RUBY_VERSION) \
      && rbenv global $REQUESTED_RUBY_VERSION \
      && gem install -q --no-rdoc --no-ri bundler --version $BUNDLER_VERSION \
      && apt-get clean \
      && rm -f /var/lib/apt/lists/*_*; \
    fi
ENV RBENV_VERSION=${REQUESTED_RUBY_VERSION:-$RBENV_VERSION}

COPY . /app/

RUN if test -f Gemfile.lock; then \
      bundle install --deployment --without="development test" \
      && rbenv rehash; \
    fi

# (2)
ENV RACK_ENV=production \
    RAILS_ENV=production \
    RAILS_SERVE_STATIC_FILES=true

RUN if test -d app/assets -a -f config/application.rb; then \
      bundle exec rake assets:precompile || true; \
    fi

# (3) BUG: Reset entrypoint to override base image.
ENTRYPOINT []

# (4)
CMD bundle exec rackup -p $PORT

```
(1) ここで、使用するRubyのバージョンを設定できます。
(2) 念のため、ここでも環境変数はセットされています。
(3) BUGと書いてますね……。Base Imageでセットされてはいないですが、明示的に書いて置く必要がありました。
(4) bundle exec rails s -p $PORT でも問題ないです。


### Rails 5.1以上用のDockerfile例
上で見てきたDockerfileは、あくまでもRubyアプリ全般、Railsに関しては、v5.1未満のものでした。
v5.1から導入された`yarn`などを使用するとなると、もうひと工夫必要になります。
（もちろん、そのうち修正されるとは思います。）

**（2017-10-06 追記）**
2017-09-20のコミットで、yarnのインストールが加わっていました。
https://github.com/GoogleCloudPlatform/ruby-docker/commit/8032c26cd3028176d0760b4f435c79463e8e1e30#diff-91e4c715bfb701f20104580d2fcdc94eR29

以下は、`yarn`のインストールを含めたDockerfile例です。

```dockerfile:Dockerfile(yarnのインストールを含む)
FROM gcr.io/google-appengine/ruby:latest

# (1)
ARG REQUESTED_RUBY_VERSION=""

RUN if test -n "$REQUESTED_RUBY_VERSION" -a \
        ! -x /rbenv/versions/$REQUESTED_RUBY_VERSION/bin/ruby; then \
      (apt-get update -y \
        && apt-get install -y -q gcp-ruby-$REQUESTED_RUBY_VERSION) \
      || (cd /rbenv/plugins/ruby-build \
        && git pull \
        && rbenv install -s $REQUESTED_RUBY_VERSION) \
      && rbenv global $REQUESTED_RUBY_VERSION \
      && gem install -q --no-rdoc --no-ri bundler --version $BUNDLER_VERSION \
      && apt-get clean \
      && rm -f /var/lib/apt/lists/*_*; \
    fi
ENV RBENV_VERSION=${REQUESTED_RUBY_VERSION:-$RBENV_VERSION}

### 追加ここから
RUN apt-get update -qq && apt-get install -y apt-transport-https && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update -qq && apt-get install -y yarn
COPY . /app/
### 追加ここまで

RUN if test -f Gemfile.lock; then \
      bundle install --deployment --without="development test" \
      && rbenv rehash; \
    fi

ENV RACK_ENV=production \
    RAILS_ENV=production \
    RAILS_SERVE_STATIC_FILES=true

RUN if test -d app/assets -a -f config/application.rb; then \
      bundle exec rake assets:precompile || true; \
    fi

ENTRYPOINT []

CMD bundle exec rackup -p $PORT
```

### app.yaml

最後に`app.yaml`ですが、動かすだけであれば、それほど複雑な記載は必要ありません。
GAEと一緒にCloudSQLを使うことが多いかと思いますので、その形で記載します。

また、Heroku同様、production環境では`DATABASE_URL`の環境変数で、DBの接続情報を渡すものとします。

（{}の中身は、適宜置き換えて下さい。）

```yaml:app.yaml
runtime: custom
env: flex

env_variables:
  DATABASE_URL: mysql2://{USERNAME}:{PASSWORD}@localhost/{DATABASE}?socket=/cloudsql/{INSTANCE_CONNECTION_NAME}

beta_settings:
  cloud_sql_instances: {INSTANCE_CONNECTION_NAME}
```

## ハマるポイント

### deviseを使っている場合、assets:precompileで落ちる
deviseに限らず発生する可能性はある事象ですが、GAEの設定ファイルである`app.yaml`で環境変数は設定できますが、それは**コンテナ起動時にのみ**セットされ、コンテナビルド時には設定されません。

回避策としては、以下のように適当にセットしておくか、

```ruby:config/initializers/devise.rb
config.secret_key = ENV.fetch('SECRET_KEY_BASE', 'hogehoge')
```

Rails v5.1以上であれば、[Encrypted Secrets](http://weblog.rubyonrails.org/2017/4/27/Rails-5-1-final/#encrypted-secrets)を使って、ソースコードに含めてしまうのが良いかと思います。

```ruby:config/initializers/devise.rb
config.secret_key = Rails.application.secrets.secret_key_base
```

### Stackdriverでのログが見づらい

Base Imageで`RAILS_LOG_TO_STDOUT=true`の環境変数をセットしていますので、Heroku同様、エラー等は標準出力され、GCPではStackdriverで確認可能なのですが、Railsのデフォルトのログフォーマットだと、見づらくなってしまいます。

以下のgemを入れるだけで、フォーマットをStackdriver用に整えてくれます。
[google-cloud-ruby/stackdriver](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/stackdriver)

また、RailsのSQLログは、色付き（ANSI Color）で出力されますが、Stackdriverは対応していないので、以下を設定しておくと良いかもしれません。

```ruby:config/environments/production.rb
config.colorize_logging = false
```

### Healthcheckのログがうざい、service-inされない
上のgemをインストールすると、healthcheckのログファイルを分けてくれるようになります。
ただ、事前にhealthcheckを返すアクションを作成しておく必要があります。

```ruby:app/controllers/gae_healthcheck_controller.rb
class GaeHealthcheckController < ApplicationController
  def ok
    render plain: 'ok'
  end
end
```

```ruby:config/routes.rb
if Rails.env.production?
  get '/_ah/start',  to: 'gae_healthcheck#ok'   
  get '/_ah/stop',   to: 'gae_healthcheck#ok'
  get '/_ah/health', to: 'gae_healthcheck#ok'
end
```

### デプロイに時間がかかる
ログを見てもらうとわかりますが、デプロイ用のコンテナを作るまではそこまで時間がかからないのですが、GAE内部でserviceやversionの設定をしている部分に時間がかかっております。。

これからに期待ですね。。

### CloudSQLへのDBマイグレーションは？
ローカルから、cloud_sql_proxyを用いて、マイグレーションを行うことも可能ですが、以下のgemで可能です。（上記の `stackdriver` も依存gemとして含まれます。）
[GoogleCloudPlatform/appengine-ruby](https://github.com/GoogleCloudPlatform/appengine-ruby)

```bash
$ bundle exec rake appengine:exec -- bundle exec rake db:migrate
```

## 特にコンテナの中身にこだわりがなければ
2017-10-06現在、デフォルトのRuby用のapp.yamlを使用しても、yarnが既にインストールされているイメージが使われるので問題ないです。

```yaml:app.yaml
entrypoint: bundle exec rackup --port $PORT
env: flex
runtime: ruby

beta_settings:
  cloud_sql_instances: [YOUR_INSTANCE_CONNECTION_NAME]
```

この際、注意すべきところは、cloud_sql_proxy用に[プロジェクトのサービスアカウントに`Editor`のロールを付与してあげる](https://cloud.google.com/ruby/rails/using-cloudsql-mysql#grant_required_permission_for_the_appengine_gem)ことです。

```bash:ロール付与
$ gcloud projects add-iam-policy-binding [YOUR-PROJECT-ID] \
  --member=serviceAccount:[PROJECT_NUMBER]@cloudbuild.gserviceaccount.com \
  --role=roles/editor
```

## さいごに

Herokuと違って、準備しておかなければいけない部分は多少ありますが、Google Cloud Platformの他プロダクトの恩恵を受けやすいGoogle App Engineはとても魅力的だと思います。

コンテナ管理が流行っていくことも考慮に入れると、GAEは良い選択肢ではないでしょうか。

## 参考
- [Building Custom Runtimes | Custom runtimes for the App Engine flexible environment](https://cloud.google.com/appengine/docs/flexible/custom-runtimes/build)
- [GoogleCloudPlatform/ruby-docker](https://github.com/GoogleCloudPlatform/ruby-docker)
- [GoogleCloudPlatform/appengine-custom-runtimes-samples](https://github.com/GoogleCloudPlatform/appengine-custom-runtimes-samples)
- [First Impressions: Rails 5 on Google App Engine](http://marcgg.com/blog/2016/05/09/rails-app-engine/)
- [Rails on App Engine: Tips and Tricks](http://www.thagomizer.com/blog/2017/03/09/rails-on-app-engine.html)