App Engine を布教したくて Go + Datastore の開発環境を Docker Compose でシュッと立ち上げた話

最新[^1]の Google App Engine Goランタイムの開発環境を Docker Compose で簡単に構築してみた話。
あるいはリーズナブルにサーバーを運用できる App Engine を布教する話。

[^1]:  2019/11/05現在
# TR;DR;

* 環境構築
    * https://github.com/pistatium/appgengine-go112-datastore-docker-compose
    * 動くサンプルはこちら。
    * `docker-compose up -d` で開発環境が立ち上がります。
    * プロジェクトのテンプレートにするなりご自由に。
* App Engine + Datastore の構成
    * 非常に安価でのサーバー運用が可能。
    * 第一世代から第二世代の移行はちょっとつらい。
    * 今からやるなら第二世代の `Go 1.12` を使うと良さそう。

--- 

## App Engine よもやま話
最初にちょっとだけ脱線を。ローカルの開発環境のことだけ気になる方は次章まで読み飛ばしてください。

### なぜ App Engine を使うのか

2011年ごろから自前のブログシステムを App Engine(Python) 上に構築して運用しています。
サーバレスでメンテいらず、とか急なアクセス増でも対応しやすい、などなどメリットはありますが、一番の利点は **運用コストの低さ** です。
App Engine はサーバレス構成が一般的になる前から従量課金方式でサーバーを提供していて、用途によっては月額のレンタルサーバを借りるより安く利用することができました。学生の頃から活用していたのでランニングコストを抑えるかは重要でした。

実際どれくらい安いかというとこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25168/087b9894-7de0-fb3f-4f5f-6c9c4782dc5a.png)

[自分のブログ](https://kimihiro-n.appspot.com) の運用にかかっている費用推移なのですが、だいたい 2000PV/月 のリクエストをさばいて数十円/月程度に抑えられています。
レンタルブログサービスでも広告消すだけで数百円掛かってしまうので圧倒的なコスパですね。
PVや機能によっては App Engine が提供する **無料枠内での運用** も可能だったりします。

この金額には

* **Webサーバーリソース** (App Engine)
* **データベース** (Datastore)
* **ドメイン** + SSL (https://〇〇.appspot.com)

が含まれています。
サブドメインとはいえドメインが含まれてるのが強いですね。
Google Cloud Run や Amazon API Gateway が吐き出す機械的なドメインよりも整ったドメイン名を使うことができます。
`appspot.com` というルートドメインも個人の成果物のアプリケーションが詰まってる感じがして悪くないです。

安くブログを運用したいという方は是非 App Engine を検討してみる事をおすすめします。
ブログを自前で構築することで Web アプリケーションの知識も自然に身に付きます。

###  Cloud Datastore (Firestore Datastoreモード)
App Engine を利用する上で欠かせないのが Datastore と呼ばれる NoSQL データベースです。
Cloud SQL などを使って RDB と接続することもできますが、いつアクセスされるかわからない状況で常時インスタンスを起動して置かなければならないのは費用的に不利です。
Datastore であれば読み書きのオペレーションとストレージのみの課金額で済みます。

ちなみに Datastore 、昔は App Engine 専用のデータベースだったのですが、データベースサービスとして独立し、最近では  Cloud Firestore に統合されてしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25168/73221015-05bd-38dc-0a82-b067f6d4c478.png)

GCP 上でプロジェクトを立ち上げた際は Cloud Firestore をネイティブモードで動かすか、Datastoreモードで動かすか聞かれます。この選択肢は排他的なので一度設定してしまうと変更することができません。
すごく雑にいえばユーザーが直接データベースを参照することがなければ Datastore モードを使えばよさそうです。ドキュメント単位のセキュリティルールが使えない代わりに性能面でメリットがあります。

### App Engine ランタイム環境の変化
長く運用していると App Engine の実行環境の変化にも対応しなくてはなりません。
サーバレスだからといってメンテなしでずっと動くわけではないようです。
自分が経験したところでいうと Python 2.5 の環境が停止したり、デプロイのコマンドやオプションが変わったりということが時々発生しています。先日もデプロイを実行しようとしたら10,000ファイルアップロード上限が追加されてて焦りました…。

そして Python2 系は **Python 自体のサポートが今年いっぱいで終了してしまいます**。
これに対する App Engine 側からのアナウンスが見当たらないですが、たとえ利用できても使わない方が賢明です。

となると Python 3系へ乗り換える必要があるのですが、Python 3.7 のランタイムは App Engine の **第二世代** と呼ばれる環境で実行されるようになります。
つまり **仕様がガラッと変わってしまう** わけですね。
今までは App Engine 用にライブラリや実行環境がカスタマイズされていたのですが、第二世代は素の Python に近い形で動くようになります。
App Engine に依存しない状態で開発できるようになるため、Dockerや別のシステムへの移植性があがり嬉しい反面、過去の App Engine のソースを移行しようとするとつらみがあります。
個人的には Memcache と呼ばれるメモリキャッシュ機能が消えてしまうのが厳しいですね。
第一世代のランタイムであればキャッシュ用のサービスが組み込まれてた安価に利用できたのですが、第二世代では SDK 側に組み込まれていないので Cloud Memorystore などのサービスを利用する必要が出てきました。
費用抑えて運用するためにはしばらくはキャッシュ機構持たないほうが良さそうですね…。
一応、App Engine のプログラムが呼ばれたとき、しばらくはインスタンスが生存して使いまわしが行われるので、整合性が不要なキャッシュ目的であればグローバル変数(リクエストのライフサイクルの外側)に入れておくことでキャッシュすることが可能です。

### Go のランタイムへ
最近 Go 言語勉強しているので、どうせ作り直すなら Python ではなく Go で書いてみようと考えてます。
コンパイルが早くてビルド成果物も小さいのでサーバーサイドで使い勝手がいいですね。表現力が乏しいのでちょっと記述が大変だったりもしますが、手に馴染んでくるようなとっつきやすさがあります。

Go のランタイムについても第一世代、第二世代の交代が発生していて 1.9 と 1.11 が第一世代、1.12が第二世代となっているようです。1.9 と1.11 は同じ世代ですが内部的には結構変わっているみたいでドキュメントを参照する時はどのバージョンのランタイムを使っているのかを注意が必要でした。
Go 1.11 は Python みたいに言語自体の消費期限が迫ってるわけではないので当面利用できそうですが、いずれ第二世代に置き換わっていく事を考えると今始めるなら 1.12 一択になりそうです。


## 開発環境を構築する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25168/f606e758-55ff-224d-2087-851fbef92ca6.png)

ということで本題。

App Engine + Go 1.12 + Datastore のローカル開発環境を Docker-compose を使って整えてみます。
昔の App Engine は専用のコマンドに Datastore のエミュレータや管理画面も含まれてましたが、gcloud コマンドに統合されてからはデプロイの機能のみのサポートとなってしまいました。
幸い Datastore のエミュレータ自体はベータとして gcloud コマンドに用意されているので、こちらを使って開発環境を作っていきます。

### 実現したいこと
1. Datastore のエミュレータを起動する
1. Go 1.12 の開発用サーバーを立ち上げる
1. Go から Datastore のエミュレータへアクセスできるようにする
1.  アプリケーションから読み書きができるか確認

ローカルの開発環境としてやりたいことはこの4つです。
これらが `docker-compose up -d` でシュッと起動できれば開発が捗りそうですね。

一つづつ何が必要か見ていきましょう。

### 1. Datastore のエミュレータを起動する

```
bash gcloud beta emulators datastore start
```

gcloud コマンドが使える状態であれば、このコマンドでエミュレータを起動できるみたいです。
公式で `google/cloud-sdk` の Docker が提供されているのでこれを使えば簡単にできそうですね。

### 2. Go 1.12 の開発用サーバーを立ち上げる
Go 1.12 のランタイムでは普通の Webアプリケーションと変わらない形で Go のアプリケーションを作れます。
なので標準の net/http パッケージを使ったり、外部のフレームワークを使ったりしてアプリケーションを組めます。
今回は Gin というフレームワークを使ってみることにしました。
一点注意するところは **`PORT` という環境変数を読み取って、その ポート番号でサーバーを立ち上げる**必要がある点ですね。
CloudRun でも同じ仕様なので GCP 上にアプリケーション立ち上げる際はこの仕様にしておくと便利そうです。


### 3. Go から Datastore のエミュレータへアクセスできるようにする
立ち上げた Web サーバーでエミュレータの Datastore を見に行くように修正をします。
Google の SDK を使う場合、**環境変数に `DATASTORE_EMULATOR_HOST` `DATASTORE_PROJECT_ID` の2つを渡してあげればデータストアへの向き先を変更することができる** みたいです。
アプリケーション側の変更がいらないので本番にデプロイするときも簡単ですね。
ここで設定する `プロジェクトID` はローカルで開発する場合は、エミュレータの起動オプションとして渡したプロジェクトIDと揃えれば何でも良さそうです。


### 4. アプリケーションから読み書きができるか確認

```go
	ctx := context.Background()

	projectID := os.Getenv(EnvKeyDatastoreProjectId)
	client, err = datastore.NewClient(ctx, projectID)

	if err != nil {
            // エラー処理
	}

	// 最新10件取得
	q := datastore.NewQuery("Entry").Order("-created_at").Limit(10)
```

アプリケーションからはこんな形で使えば取得できるみたいです。
詳細は[サンプルソース](https://github.com/pistatium/appgengine-go112-datastore-docker-compose/blob/master/main.go) を参照してください。
環境変数で SDK にプロジェクトID を渡してますが、プログラムからも再度プロジェクトIDを渡す必要がありました。

### Docker Compose にまとめる
必要な情報が揃ったので Docker Compose で開発環境をまとめてしまいましょう。

```yml

version: '3'
services:
  env:
    image: golang:1.12
    environment:
      - GO111MODULE=on
      - DATASTORE_EMULATOR_HOST=datastore:8059  # datastore サービスの `$ホスト名:$ポート` を指定
      - DATASTORE_PROJECT_ID=local-app  # 開発環境用のプロジェクト名
      - PORT=8000
    working_dir: /opt/app
    volumes:
      - .:/opt/app  # カレントディレクトリを同期
    ports:
      - 8000:8000  # ホストマシンから localhost:8000 でアクセスできるように
    links:
      - datastore:datastore  # datastore というホスト名で datastore サービスへアクセスできるように
    command: bash -c "go get github.com/pilu/fresh && fresh -c .fresh.conf"  # ホットリロードできるように Fresh を導入

  datastore:
    image: google/cloud-sdk
    environment:
      - PROJECT_ID=local-app  # 開発環境用のプロジェクト名 (上と同じであれば何でも可)
    command: /bin/bash -c "gcloud beta emulators datastore start --host-port 0.0.0.0:8059 --project $$PROJECT_ID --data-dir /data"
    volumes:
      - datastore_volume:/data
volumes:
  datastore_volume:  # 永続化用にボリュームを定義
```

これが完成した docker-compose.yml です。

Go のアプリケーションを載せる `env` というサービスと Datastore のエミュレータを動かす `datastore` という2つのサービスを定義しています。

datastore サービスの方は Docker Compose 再起動のたびにデータが消えてしまうと困るので、 volumes に専用のボリュームを定義してそこに書き込むよう指定しています。
プロジェクトIDは `local-app` と仮にしています。

Go のアプリケーション側からは links 機能を使ってエミュレータにアクセスできるように内部用のホスト名を設定してます。先程の環境変数にこのホスト名を渡してあげれば通信することが可能です。
あとはソース変更のたびにアプリケーションを再起動し直すのは手間なので、[Fresh](https://github.com/gravityblast/fresh) というホットリロードの仕組みを導入してます。

これで `docker-compose up -d` を実行すれば開発環境がシュッと立ち上がる仕組みが完成しました。
App Engine への実際のデプロイも `gcloud app deploy` コマンドを叩けばそのまま実行できます。
(`DATASTORE_PROJECT_ID` を本番環境でも設定してあげる必要があります。)

**2019/11/06 追記:** 
https://cloud.google.com/datastore/docs/activate?hl=ja#datastore-permissions-for-app-engine
App Engine から Datastore へアクセスする Permission の設定も必要でした。

---

あとはゴリゴリとアプリケーションを書いてくだけですね。
**App Engine 面白いのでぜひ使ってみてください！**
(年内にブログリニューアルしなきゃ…)


## 追記:2019/11/07
[Updating App Engine with more new runtimes: Nodejs 12, Go 1.13, PHP 7.3 and Python 3.8](https://cloud.google.com/blog/products/application-development/updating-app-engine-with-more-new-runtimes)
Go 1.13のランタイムがベータ版で公開されたみたいです。
言語的なアップデートだけで仕様的には大きく変わらない感じですかね。


## 参考
* [Cloud Datastore エミュレータの実行](https://cloud.google.com/datastore/docs/tools/datastore-emulator?hl=ja)
* [GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)](https://qiita.com/br_branch/items/a26480a05ecb97ac20b3)
* [GAE/SE Pythonランタイムアップデートへの道](https://qiita.com/wasnot/items/97cc0e058cecfcc0889d)