GCP移行でハマった10件総まとめ(GAE Rails+Postgres編)

### はじめに
Google Cloud Next '17で発表されたGCPの大きな改良によって、GCPで利用できるプロダクトの幅が広がりました。注目すべきは以下の2点です：

1. Cloud SQLでPostgreSQLのサポート
2. GAE(GoogleAppEngine) Flexible EnvironmentによるRuby, Node.js, .NETのサポート

従来からGCPでは、MySQL+(Python|Java|PHP|Go)という構成のアプリが動かせましたが、これに加えて上述の様々なアプリが動作するようになりました。
私達のチームではRails+Postgresでアプリを開発していたのですが、この変更によってGCPでやっていく機運が高まり移行しました。しかし出たての機能ということもありドキュメントが手薄で、ハマりどころが多かったのも事実。そこで、ハマったところをまとめて共有します。

### ハマりどころ1: GAEデプロイまでのお作法がちょっとむずかしい

git push すればデプロイというわけではなく、GAEではいくつかのお作法があります。一通りお作法を学ぶために、Google謹製のチュートリアルがあるのでまずは眺めてみましょう。
[Ruby Bookshelf App](https://cloud.google.com/ruby/getting-started/tutorial-app)
かんたんに手順をまとめると、

- GCP操作の起点となる、gcloudコマンドをローカルマシンにインストール
- GAEの設定ファイルとして、app.yamlを用意(数行程度のかんたんなもの)
- DBの設定(ただしチュートリアルはMySQLで書かれています)
- gcloudコマンドによるデプロイ(gcloud app deploy)

以上により、https://プロジェクト名.appspot.com というURLでアプリにアクセスできます。
チュートリアルはあっさりした情報ですので、残念ながらハマりどころを掘り下げているわけではありません。本記事のハマりどころもあわせてご覧ください。

### ハマりどころ2: Cloud SQL上のPostgresと接続する方法がよくわからない
前述のチュートリアルでは、Postgresの事例がGCP上にVMでサーバーを立てるやり方が載っています。しかし2017年12月時点では、Cloud SQLを使えばボタン一つで立ち上がるので、そっちを使いたいです。なので以下の設定を行います。

Cloud SQLには様々な接続方法があり、proxyを使う方法、IP制限を緩める方法、認証キーの入ったファイルを指定する方法などがありますが、GAEとつなげるなら、

[Using Cloud SQL for PostgreSQL](https://cloud.google.com/appengine/docs/flexible/ruby/using-cloud-sql-postgres)

に記載されている`app.yaml` / `database.yml`で定義する方法が良さそうです。以下のとおり2箇所に設定を入れます。

1. app.yamlに、beta_settings: cloud_sql_instances: 接続名を設定する

```yaml:app.yaml
entrypoint: bundle exec rackup -p $PORT
env: flex
runtime: ruby
beta_settings:　←コレ追記！
  cloud_sql_instances: career-marketing:us-west1:sandbox17　←コレ追記！
```

2. DB のhostとして、/cloudsql/接続名 を設定する。

```yaml:config/database.yml
production:
  <<: *default
  host: /cloudsql/project-name:us-west1:sandbox17 ←コレ！
```

以上です。app.yamlにbeta_settingsという変な名前の項目を作るところに注目。これはCloud SQLのPostgresがベータリリースのため、今後変更が入る可能性があることを表しているようです。(DB設定を同じ内容を複数箇所に書かなくてはいけないのも気になりますが、改善されるといいですね)

なおそれぞれ、接続名は「プロジェクト名:リージョン名:DB名」ですので、各自の環境ごとに読み替えてください。（DB名だけ書いてしまい、ハマるところの一つです。）

### ハマりどころ３: ローカルからDBに接続できない
ローカルのGUIクライアントなどを使ってDBの中身を確認したい、というのはよくあるケースだと思います。
しかしCloud SQLでは、セキュリティ上の理由から外部からのアクセスがキツめに制限されています。結果、DBサーバのIPを指定して接続することがデフォルトではできません。
特定のIPからのみ接続を許可することはできますが、接続側に固定IPを用意できない場合もあります。

そこでCloud SQLでは、Cloud SQL Proxyと呼ばれるSSHトンネリング(ポートフォワーディング)プログラムを用いた接続方法が用意されています。
SSHトンネリングとは、ローカルの特定のポートへのアクセスを、SSH接続先の(GCP上の)サーバーへ接続する技術です。
言葉だとわかりにくいので、図をご覧ください。

![Cloud SQL via SSH](https://qiita-image-store.s3.amazonaws.com/0/8893/53b981ff-2c67-2735-5da2-c7f572acffe3.png)

- Cloud SQL Proxyは、内部でsshを使ってローカルとCloud SQLを接続する。
- 接続にあたっては、「サービスアカウント」と呼ばれるアクセス管理用アカウントから出力した認証用JSONファイルを参照し、アクセスコントロールされる。
- 認証用JSONファイルに付与される権限は、サービスアカウントに付与された権限によって決まる。
- プログラム起動時には、引数でトンネリングするポートを指定する。
- クライアントアプリは、localhostの指定したポートにアクセスすることで、GCP側のCloud SQLと接続できる。

こう書くと簡単そうですが、接続までに幾つもハマりどころがあります：

- Cloud SQLのAPIがデフォルトでは無効化されており、有効化する必要あり(DBごとに有効化が必要で、忘れていると接続できない←**よく忘れる**)
- `Client`以上の権限でサービスアカウントを用意する必要がある(←**Viewer権限では駄目でした！！**)
- cloud_sql_proxyで接続する際には、、DBを”アカウント:リージョン:DB名”のように指定する（←**コマンドラインのヘルプにはその旨書かれてない**><）

残念ながら、執筆時点ではPostgresで上記の手順をStep-by-stepで書かれたドキュメントが存在しないため、MySQLのものを参考にしながら進めます。

参考: [Cloud SQL Proxy を使用して MySQL クライアントを接続する](https://cloud.google.com/sql/docs/mysql/connect-admin-proxy)
(MySQLと書いてありますがそのまま流用できます！)

具体的な手順は以下のとおりです。
まず、サービスアカウントを作成します。
![service accounts](https://qiita-image-store.s3.amazonaws.com/0/8893/613d3e4b-7f65-d458-4b8c-820b981be29a.png)
https://console.cloud.google.com/iam-admin/serviceaccounts

IAM & adminメニューのServiceAccountから(1)、CREATE SERVICE ACCOUNTを選び(2)、出てきたポップアップから、[Cloud SQL] => [Cloud SQL Client] を選択(3)
(※SELECTしかしないとしても、Viewer権限では上手く動かないのでClientを選択してください！)

次に認証用JSONを作成します。
サービスアカウントを作成したら、一覧画面に秘密鍵を生成するボタンがあるのでクリックします。キーの種類はJSONを選択します。
ダウンロードしたJSONは大事に保管してください。(と言われても、いつも困りますよね。。今回は、credential.jsonという名前でローカルに保存します)

以上で事前準備が整ったので、ローカルマシンでの作業に移ります。
cloud_proxyをインストールします。

```bash:cloud_sql_proxyのインストール(ローカル)
$ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64
$ chmod +x cloud_sql_proxy
```
そして、cloud_sql_proxyを起動します。今回は、localhost:5432にアクセスするとCloud SQLに接続する設定にしていますが、必要に応じて`tcp:5432`の部分を読み替えてください。

```bash:ローカル
$ ./cloud_sql_proxy -instances=project-name:us-west1:sandbox17=tcp:5432 -credential_file=./credential.json
```

以上で、ローカルのマシンから接続できるようになりました。
(結構たいへんですね><。まだまだハマりどころは続きます><)

### ハマりどころ4: ログが流れてこない
GCPでは、StackDriverと呼ばれるモニタリング統合環境でログを確認します。
しかしRailsのデフォルトではHTTPステータスぐらいしか流れません。これではデバッグしようがないので、RailsのログがStackDriverのログに流れるようにします。

```ruby:Gemfile
group :production do
  gem 'appengine'
end
```

```ruby:config/application.rb
require 'stackdriver'
```

appengine gemは、googleが提供するGCP用の以下のGemをまとめてインストールしてくれるラッパーgemです。以下のGemがインストールされます：

- [google-cloud-debugger](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-debugger)
- [google-cloud-error_reporting](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-error_reporting)
- [google-cloud-logging](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-logging)
- [google-cloud-trace](https://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-trace)

これらをインストールすることで、ログを収集するだけでなくパフォーマンス測定やエラーレポーティングにも対応できます。

### ハマりどころ5: ログをtailできない
前述の通り、ログはStackDriverという基盤に統合されてしまっているがゆえ、純粋にログをtailしたいときにもお作法が必要です。

```bash:ローカル
$ gcloud app logs tail -s　アプリ名
```

のようにgcloudコマンド経由でログをtailします。Stack Driver 上ではログが構造化されて表示されて見やすい反面、正規表現が使えなかったり痒いところに手がとどかない場合もあるので、覚えておいて損はありません。

### ハマりどころ6: Rakeタスクを実行する(ローカルから)
GAEを使ってRailsを運用する際、特に問題になるのはタスク実行周りです。
動作しているサーバーにsshでログインしてコマンド実行、、のような操作は行えず、
Googleが提供しているインタフェース経由での実行となります。(後述するCron実行もトリッキーです。)

Rakeタスクを叩く場合には、前述のappengine gemを入れておけば、ローカルコマンドラインから実行できます。事前準備として、Rakeファイルに1行追記します。

```ruby:Rakefile
require File.expand_path('../config/application', __FILE__)
require 'appengine/tasks' ←コレ追記！
Rails.application.load_tasks
```

すると、rake appengine:exec というRakeタスクがが有効化され、そこからリモートコマンド実行ができるようになります。
実行時には「rake appengine:exec – 実行したいコマンド」です。

```bash:ローカル
$ bundle exec rake appengine:exec -- bundle exec rake db:migrate
```

### ハマりどころ7: 定期実行するのがむずかしい
GAEではCronサービスが用意されていますが、通常のcronと異なりシェルコマンドを実行できません。実行ファイルをポコッと置いただけでは動かすことができないのです。
かわりにCronサービスは、指定した時間に指定したURLにアクセスしてくれます。
ポイントは以下のとおりです。

- cron実行にはcron.yamlを用意します。(ここでURLを指定します。)
- cron.yamlは通常の"gcloud app deploy" コマンドではデプロイされず、 "gcloud app deploy cron.yaml" のように別途、明示的にデプロイする必要があります。
- CronサービスはURLへのアクセスによって定期実行がなされます。ですからRakeタスクはそのままコールできません。コントローラからコールできるように変更しないといけません。
- CronサービスはHTTPヘッダに `X-Appengine-Cron: true`  を付与してアクセスしてくるので、これがついていない場合はアクセスをはじきます。
- なお、外部から `X-Appengine-Cron: true` を付与してアクセスした場合にはGoogle側で勝手に外すので、いたずらされるリスクはありません。
- タイムアウト時間は、手動で立ち上げたインスタンスでは２４時間以内、オートスケールさせたものでは10分です。

（24時間「以内」とか、スケール方法によってタイムアウト時間が違うとか引っかかりますが、公式ドキュメントにはそのように書いてあります→ [cron.yamlリファレンス](https://cloud.google.com/appengine/docs/standard/python/config/cronref)）

私は定期実行専用のコントローラとメソッドを用意して対応しました。https://アプリ名/tasks/some_task で5分ごとにタスク実行させるときは、以下のようにコードを追加します。

```yaml:cron.yaml
cron:
- description: some_task
  url: /tasks/some_task #←作った実装と合わせる
  schedule: every 5 minutes
```

```ruby:config/routes.rb
scope :tasks do
  get 'some_task', to: 'tasks#some_task'
end
```

```ruby:app/controllers/taskscontroller.rb
class TasksController < ApplicationController
  before_filter :fail_unless_appengine_cron
  def some_task
    # なにか定期実行させたい処理をここに書く
    render json: {
      status: 200,
      message: 'successfully finished',
    }.to_json
  end
 
  def fail_unless_appengine_cron
    unless request.headers['X-Appengine-Cron'].present?
      fail
    end
  end
end
```
以上により、`X-Appengine-Cron:true`のHTTPヘッダーがついているときのみ成功ステータス200を返却できます。ローカルでテストする際は、curlで`-H`オプションを付与することでヘッダーの有無による挙動の違いを確認できます。

```bash:ローカル
$ curl -LI localhost:3000/tasks/some_task -o /dev/null  -w '%{http_code}\n' -s
500　←ヘッダーがないとエラー
$ curl -H 'X-Appengine-Cron:true' -LI localhost:3000/tasks/some_task -o /dev/null  -w '%{http_code}\n' -s
200 ←ヘッダーがあると成功
```

デプロイには以下のコマンドで行います。

```bash:ローカル
$ gcloud app deploy cron.yaml
```
デプロイ後はGUIから確認でき、GUIから[Run now]ボタンを押して実行することもできます。
![task queues](https://qiita-image-store.s3.amazonaws.com/0/8893/627b0acd-dcc3-5875-75a6-aa10c33c709a.png)

参考: [Scheduling Tasks With Cron for Python](https://cloud.google.com/appengine/docs/standard/python/config/cron)

### ハマりどころ8: 認証系まわりがユニーク（慣れると便利）
(ハマりどころと呼ぶには少し語弊があるかもしれませんが、)GCPではGoogleアカウントに紐付いた形でアクセス権限の制御を行えます。

従来は小さなアプリであっても、認証GemであるDeviseなりbasic認証なりで認証を行う必要がありました。しかし特に、社内ツールやユーザーが限られたアプリではDeviseでは大げさ、basic認証ではガバガバで嫌、というので困るケースがありました。
GCPでは、Identity-Aware Proxyと呼ばれる機能により、Googleアカウントに紐付けてアクセス制限を行えますので、ライトかつセキュアにアプリを作っていけます。さらにGSuiteを導入している企業では、組織レベルで権限設定を行えます。

![Identity-Aware Proxy](https://qiita-image-store.s3.amazonaws.com/0/8893/e7d5f282-5965-5422-e2c0-09dfb5cb155b.png)


部署異動や退職などに伴う権限設定は地味にめんどくさいですが、アプリケーションに手を入れず権限変更できるのはすごい便利で、Deviseでは実現できない機能です。

なお、非ログインユーザーにはGoogleアカウントへのログインが促されます。

![login](https://qiita-image-store.s3.amazonaws.com/0/8893/d226879c-415e-c3f0-d756-75c2b1c650df.png)

### ハマりどころ9: Cloud SQLのPostgresでは使えないExtensionがある
私の携わるアプリでは、Postgres-fdwという外部DBのデータをPostgres上で扱えるExtensionを必要とするものがありましたが、
Cloud SQLでは対応しておらず、別途VMでPostgreSQLを立てる必要がありました。
現在ベータバージョンということもあり今後変わると思いますが、どの順で対応が進むのかは気になるところ。

GCPの公式ドキュメントで [機能のリクエスト](https://cloud.google.com/support/docs/issue-trackers#feature_requests) を見ると、

>あるプロジェクトの新機能リクエストに付けられた星の数がしきい値に達すると、Google は作業に取りかかります。

とのことなので、★をつけるなどのリアクションを取れば、対応される可能性が上がるようです。
（ただしissue trackerを見ていると、作業者がアサインされても数ヶ月動きのないものもあるようです、、。）

参考: [Cloud SQL for PostgreSQL updated with new extensions](https://cloudplatform.googleblog.com/2017/08/Cloud-SQL-for-PostgreSQL-updated-with-new-extensions.html)

### ハマりどころ10： GAEのデプロイが遅い
herokuで1分で終わるところが8分とかかかってがっかりしています、、、(´・ω・`)
RailsはFlexible Environment上で動作するので、Flexible Environmentを展開する分遅い模様です。
でも中の人が頑張ってくれてるはず＞＜。(もしくはkubernetesを使っていく気持ち！)

参考: [GAリリース記念！！今更だけどGAE Flexible Environmentのチュートリアルを試してみた](http://www.apps-gcp.com/gae-flexibile-environment/)

### まとめ - ハマりどころを回避するために
GAE Rails+Postgres、ハマりどころがおもったよりありました><

Googleが提供するドキュメントは少し暗黙知的というか、そのドキュメントだけでは理解しきれない部分があります。チュートリアルとリファレンスなど、似たようなドキュメントを相互に見比べながら情報を補完していくのが重要だと感じました。
多言語対応がされていそうで意外とされておらず、リンク切れした日本語ドキュメントが英語版ならある、といったケースも時折見かけました。

GCPではLinux Boxを触るような気軽さはなく、手順を踏んで各リソースにアクセスしなくてはならないため、初見ではお作法が多く怠く感じます。
しかしその引き換えとしてセキュリティ面の安心感と、ユーザー管理の利便性と、統合的なモニタリング環境が手に入るのが大きなメリットに感じました。
本記事がGCP移行に興味がある方の参考になればさいわいです。