Google App Engine (GAE) フレキシブル に Rails6 アプリ と非同期処理環境(sidekiq)を構築する

## はじめに
ちょっとした興味からGCPの勉強を初めて3日目にしてやっと Google App Engine(以降GAE)に当初のゴールだった Railsアプリとsidekiqのワーカー両方をデプロイし起動するところまで出来ました。ググってみるとRails アプリ単体をGAEにデプロイする記事はたくさんあったのですが、sidekiq も合わせてGAEにデプロイする手順までまとめた記事は見かけなかったのでここにまとめたいと思います。

## 全体図
- GAEで別サービスで App(Rails), Worker(Sidekiq) の両方をデプロイし起動させる
- DBにはCloud SQLのPostgres を利用する
- Cloud MemorystoreのRedisを利用してSidekiqのジョブ管理を行う

![App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29755/2733a9a3-fe5c-5b57-31d4-a31b8f540c19.png)

## 前提
- 簡単なRailsアプリ、Sidekiqのワーカーは実装済みとする
- GCPでプロジェクをは作成済みとする。今回のプロジェクトIDは (仮) `rails-app-sample` とする。
- 環境
    - Rails 6.1.0
    - Sidekiq 6.1.2

## 説明しないこと
- sidekiq の初期設定

## サンプルアプリケーション
- id, name, created_at, updated_at カラムを保持する items テーブル
- RailsのAPIモード上で scaffold を利用しAPIを実装
    - `GET /items`
    - `POST /items`

**db/schema.rb**

```ruby
  create_table "items", force: :cascade do |t|
    t.string "name", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end
```

**config/routes.rb**

```ruby
Rails.application.routes.draw do
  resources :items
end

```

**app/controllers/items_controller.rb**

```ruby
class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :update, :destroy]

  # GET /items
  def index
    @items = Item.all

    render json: @items
  end

  # POST /items
  def create
    ItemCreatorWorker.perform_in(3.seconds, item_params.to_h)
    render json: { message: 'success' }, status: :accepted
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_item
      @item = Item.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def item_params
      params.fetch(:item, {}).permit(:name)
    end
end
```

**app/workers/item_creator_worker.rb**

```ruby
class ItemCreatorWorker
  include Sidekiq::Worker

  def perform(options = {})
    @item = Item.new(options)
    @item.save
  end
end
```

## 手順

### Cloud SDK をインストールする
GAE のデプロイは gcloud コマンドを利用して行うため Cloud SDKが必要なのでインストールをします。詳しくは [Google Cloud SDK のインストール  \|  Cloud SDK のドキュメント](https://cloud.google.com/sdk/docs/install?hl=JA) を参考にしてください。

### gcloud コマンドの認証

```
# Google アカウントの選択とアクセスの許可を行う認証画面がブラウザ上で開かれるので許可をする
$ gcloud auth login

# プロジェクト一覧を確認する
$ gcloud projects list

# gclould コマンドで操作したいプロジェクトのプロジェクトIDを指定する
$ gcloud config set project [PROJECT ID]
```

### appengine gem のインストール

[appengine gem](https://github.com/GoogleCloudPlatform/appengine-ruby)はDBマイグレーションを App Engine から実行するために必要なので追加してください。

```
gem "appengine"
```

これで以下のようなコマンドが実行できるようになります。

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

### GAE環境の用意

`gcloud app create` を実行し、リージョンを選択することでApp Engineアプリケーションが作成できます。これでいつでもデプロイは可能な状態となりました。

```
$ gcloud app create
You are creating an app for project [my-project2-300306].
WARNING: Creating an App Engine application for a project is irreversible and the region
cannot be changed. More information about regions is at
<https://cloud.google.com/appengine/docs/locations>.

Please choose the region where you want your App Engine application
located:

 [1] asia-east2
 [2] asia-northeast1
 [3] asia-northeast2
 [4] asia-northeast3
 [5] asia-south1
 [6] asia-southeast2
 [7] australia-southeast1
 [8] europe-west
 [9] europe-west2
 [10] europe-west3
 [11] europe-west6
 [12] northamerica-northeast1
 [13] southamerica-east1
 [14] us-central
 [15] us-east1
 [16] us-east4
 [17] us-west2
 [18] us-west3
 [19] us-west4
 [20] cancel
Please enter your numeric choice:  2

Creating App Engine application in project [my-project2-300306] and region [asia-northeast1]....done.
Success! The app is now created. Please use `gcloud app deploy` to deploy your first app.
```

参考: [ロケーション \- リージョンとゾーン  \|  Google Cloud](https://cloud.google.com/about/locations?hl=ja#asia-pacific)


### Cloud SQL で PostgreSQL を用意

1. グローバルメニューより「SQL」を選択し、「インスタンスを作成」を選択する
2. 「PostgreSQL」を選択する
3. インスタンスID, パスワードを入力、ロケーション、データベースバージョンを選択。今回はパブリックIPより接続するように選択します。マシンタイプとストレージ以降はお好みで。
4. 「作成」ボタンを押せば、PostgreSQL が約10分後に作成されます。

#### DBユーザー作成

1. SQL -> ユーザーメニューを選択後、「ユーザーアカウントを追加」ボタンを押す
2. インスタンスに固有のユーザーを作成するため、ユーザー名・パスワードを入力後、「追加」ボタンを押す
3. これでDB接続ユーザーが作成されました

#### データベース作成
1. SQL -> データベースを選択後、「データベースの作成」ボタンを押す
2. データベース名を入力後、「作成」ボタンを押せば終わり

### Cloud Memorystore で Redis を用意

1. `$ gcloud services enable redis.googleapis.com` を実行して `Google Cloud Memorystore for Redis API` を有効にする
2. Memorystore -> Redis から「インスタンスを作成」ボタンを押す
3. インスタンスID, 表示名を入力、リージョン・ゾーン・バージョンなどを選択し、「作成」ボタンを押します。これで Redis が作成されます


### アプリケーション直下に以下の Dockerfile を用意する

```dockerfile
FROM ruby:2.6.6

ENV LANG C.UTF-8

RUN apt-get update -qq && apt-get install -y build-essential

RUN gem install bundler

RUN mkdir /app
WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install

COPY . /app

EXPOSE 8080

# アプリとWorker両方でDockerfileを兼用するため起動スクリプトを別途実行するようにする
CMD ["/bin/sh", "bootstrap.sh"]
```

### app.yaml: web側サービスの構成ファイルを用意する

```yaml
# 独自のDockerイメージを利用するため custom を入力
runtime: custom

# フレキシブル環境を利用するため flex を入力
env: flex

# env_variables で環境変数の設定ができる。このサービス側は web として認識し、bootstrap.sh 内で実行するコマンドを切り替えるため、 `SERVICE_TYPE: 'web'` としました。
# 他にも良い方法があれば知りたいです。
env_variables:
  SERVICE_TYPE: 'web'

# 今回は検証環境として最低限のスペックで構築します。
manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

# 【重要】 GAE から Cloud SQLのDBに接続できるようにするため、 cloud_sql_instances に　CLOUD SQL で作成した該当DBの接続名（例: bamboo-striker-300213:asia-northeast1:sample-app-db )を入力します。
beta_settings:
  cloud_sql_instances: [YOUR_INSTANCE_CONNECTION_NAME]

#  DBパスワードなどの秘匿情報を
includes:
  - secret.yaml
```

### worker.yaml: 非同期処理側サービスの構成ファイルを用意する

```yaml
runtime: custom
env: flex
# 用途がわかるようにサービス名をつけておきます
service: worker

env_variables:
  # bootstrap.shで実行コマンドを切り替えるため、明示的に以下を定義します。
  SERVICE_TYPE: 'worker'

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

beta_settings:
  cloud_sql_instances: [YOUR_INSTANCE_CONNECTION_NAME]

includes:
  - secret.yaml
```

### secret.yaml: 秘匿情報のみを記載した環境変数を定義するファイルを用意する

アプリのサービス構成ファイルでは [includes ディレクティブ](https://cloud.google.com/appengine/docs/standard/python/config/appref#includes) を使用することで、別ファイルを読み込むことが出来ます。今回は秘匿情報を定義した構成ファイルを用意し、以下のように定義ます。

```yaml
env_variables:
  SECRET_KEY_BASE: [SECRET_KEY_BASE]
  DATABASE_USER: 'sample-app-user'
  DATABASE_PASSWORD: 'sample-app-user-password'
  DATABASE_NAME: 'sample-app-db'
  # /cloudsql/[YOUR_INSTANCE_CONNECTION_NAME] の形式で定義することにより、GAE から Cloud SQL へ UNIX ドメインソケットで接続できるようになります。
  DATABASE_HOST: '/cloudsql/[YOUR_INSTANCE_CONNECTION_NAME]'
  RAILS_ENV: 'production'
  # web/workerのサービスから Reis に接続できるようにするため以下も定義します。
  REDIS_URL: 'redis://[REDIS_IP_ADDRESS]:6379'
```

参考: [App Engine から Cloud SQL への接続  \|  Google Cloud](https://cloud.google.com/sql/docs/mysql/connect-app-engine?hl=ja)

### bootstrap.sh: web, worker用の起動スクリプトを用意する

```shell
#!/bin/bash

if [ "$SERVICE_TYPE" = "web" ]; then
  bundle exec rackup -p 8080 -o '0.0.0.0'
elif [ "$SERVICE_TYPE" = "worker" ]; then
  bundle exec sidekiq -C config/sidekiq.yml
fi
```

### ここで一旦web側をデプロイする

この後データベースの作成、マイグレーションを実施するためweb側をデプロイしておきます。

```
$ gcloud app deploy
```

### マイグレーション準備

#### Cloud SQL Admin APIの有効

以下を実行してCloud SQL Admin APIを有効にします。DBの作成・マイグレーションなどが可能になります。

```
$ gcloud services enable sqladmin.googleapis.com
```


#### Cloud Build に Cloud SQL クライアントロールの追加

Cloud Build から Cloud SQL Admin API 経由でアクセスできるように Cloud Build のサービスアカウントに **Cloud SQL クライアント** のロールを追加します。これでアクセスできるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29755/b093aa1d-a622-3287-e7f1-d6a641b1f857.png)

#### DBの接続状態を確認

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

特にCloud Buildや認証関連のエラーが発生しなければOK!

```
---------- CONNECT CLOUDSQL ----------
cloud_sql_proxy is running.
Connections: my-project2-300306:asia-northeast1:sample-app-db.

---------- EXECUTE COMMAND ----------
bundle exec rake db:migrate:status
Schema migrations table does not exist yet.
ERROR
ERROR: build step 0 "gcr.io/google-appengine/exec-wrapper:latest" failed: step exited with non-zero status: 1
--------------------------------------------------------------------------------------------------------------
```

#### マイグレーションを実行!!

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

これで必要なテーブルも無事に作成されました :smile: 

### 最後にweb/worker両方を再度デプロイして終わり

時間はかかりますが待っていればこれでweb, worker構成の完了です！ うおおおお。

```
$ gcloud app deploy app.yaml worker.yaml
```

## 動作検証

Itemデータの登録処理はワーカー側で非同期で登録するようにしており、数秒待ってから登録されるようになりました！ :tada: 

```json
$ curl -s https://my-project2-300306.an.r.appspot.com/items | jq .
[]
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"ageage"}' https://my-project2-300306.an.r.appspot.com/items
{"message":"success"}⏎
$ curl -s https://my-project2-300306.an.r.appspot.com/items | jq .
[
  {
    "id": 1,
    "name": "ageage",
    "created_at": "2020-12-31T09:58:19.770Z",
    "updated_at": "2020-12-31T09:58:19.770Z"
  }
]
```

なんやかんやでここまで来るのに丸３日かかりました... :sweat_smile: 

## トラブルシューティング

### ERROR: (gcloud.app.deploy) NOT_FOUND: Unable to retrieve P4SA: [service-XXXX@gcp-gae-service.iam.gserviceaccount.com] from GAIA. Could be GAIA propagation delay or request from deleted apps.

初回時に `gcloud app deploy` をした際にこのエラーが表示されることがあります。ググっても GAIA が何者か分からず原因不明なのですが、もう一度コマンドを再実行すると発生しなくなります。謎...。

### ERROR: (gcloud.app.deploy) Error Response: [8] The region asia-northeast1 does not have enough resources available to fulfill the request. Please try again later.

これは結構な頻度で発生します。内容としては「該当のリージョンで該当のリクエストを満たす十分なリソースが無い。後でやり直してください」とのこと。文字通り時間をおいてからやり直すとうまくいきます

## おわりに
年末年始の自由研究としてちょっとした興味がてら GAE 上でのRailsアプリケーション環境の構築方法を調査して構築してみました。ざっと触れることでGAE フレキシブルを利用した環境構築方法はざっと把握することができました。GCPから日本語の Rails 環境構築方法、Cloud SQLでのPostgreSQLへの接続方法などの資料が丁寧に提供されておりとても助かりました。一方構築し始めて気づいたことは、意外と非同期処理環境の構築方法は日本語・英語共にあまりなく、本記事は珍しい方の内容になるかと思います。あくまで自分用のメモとして列挙した内容であり読みづらい点は申し訳ないです。

GAEには、その他タスクキュー、cronジョブ、ファイアーウォールルール、Memcacheなどのなどの機能もあるので、全容把握のためざっくりでも良いので触れてみたいです。

ちなみにここまでかかったコストは¥910 でした。（無料枠無し)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29755/ea6b5ef5-7a66-e56b-0443-be57af085fd7.png)


## 参考
- [GoogleCloudPlatform/appengine\-ruby: Optional integration library for the Ruby runtime for Google App Engine](https://github.com/GoogleCloudPlatform/appengine-ruby)
- 今回利用したサンプルRailsアプリケーション [hypermkt/rails\-playground](https://github.com/hypermkt/rails-playground)
- [Rails 5 での Cloud SQL for PostgreSQL の使用  \|  Ruby  \|  Google Cloud](https://cloud.google.com/ruby/rails/using-cloudsql-postgres?hl=ja)
- [app\.yaml によるアプリの構成  \|  App Engine フレキシブル環境用カスタム ランタイム  \|  Google Cloud](https://cloud.google.com/appengine/docs/flexible/custom-runtimes/configuring-your-app-with-app-yaml)
- [App Engine から Cloud SQL への接続  \|  Google Cloud](https://cloud.google.com/sql/docs/mysql/connect-app-engine?hl=ja)