docker-compose で Cloud Datastore Emulatorを立ち上げpythonアプリと連携する

# 背景

サービスでdatastoreを使う事になり、開発環境やCIでのテストをどうするか試行錯誤した。 :thinking: 
AWSと比較して情報が少ないので、まとめた。 :grinning: 

# 要点

- flaskアプリとCloud Datastore Emulatorをコンテナを作る
- docker-composeでサービス連携を行う
- datastoreの初期データ投入
- flaskアプリから、Cloud Datastore Emulatorを操作する

# ディレクトリ構成

```
.
├── app
│   ├── Dockerfile
│   └── src
│       ├── main.py
│       └── requirements.txt
├── datastore
│   ├── Dockerfile
│   ├── entrypoint
│   └── import
│       ├── 2020-01-21.overall_export_metadata
│       ├── default_namespace
│       │   └── kind_test_data
│       │       ├── default_namespace_kind_test_data.export_metadata
│       │       └── output-0
│       └── run.sh
└──  docker-compose.yaml
```

# Cloud Datastore Emulator コンテナ構築

## datastore/Dockerfile

公式のSDKイメージから最小限で作成。
エミュレータ起動用shell、データ投入用shellに実行権限付与。

```Dockerfile
FROM google/cloud-sdk:alpine

RUN apk add --update --no-cache openjdk8-jre \
  && gcloud components install cloud-datastore-emulator beta --quiet

COPY . /datastore/

WORKDIR /datastore

RUN chmod +x ./entrypoint
RUN chmod +x ./import/run.sh

ENTRYPOINT ["./entrypoint"]
```

## datastore/entrypoint

`docker-compose down`した際にも、データを維持するために、`/datastore/.data/`ディレクトリにデータをためる。

オプションなしで起動すると、コンテナ内でしかアクセスできないので、`--host-port=0.0.0.0:8081` として起動する。
環境変数から、プロジェクト名と一緒に流し込む。

```bash
#!/usr/bin/env bash

gcloud config set project ${DATASTORE_PROJECT_ID}

gcloud beta emulators datastore start \
  --data-dir=/datastore/.data \
  --host-port=${DATASTORE_LISTEN_ADDRESS}
```

## データ投入のdatastore/import/run.sh

サーバー起動後、下記のエンドポイントにdump済みのデータの保存パスを投げるとデータがインポートできる。

```bash
export DATASTORE_PROJECT_ID

curl -X POST localhost:8081/v1/projects/${DATASTORE_PROJECT_ID}:import \
    -H 'Content-Type: application/json' \
    -d '{"input_url":"/datastore/import/2020-01-21.overall_export_metadata"}'
```

## datastore/importのメタデータ

今回は、gcp のコンソールから、gcsにダンプしたデータをディレクトリ丸ごと`datastore/import`配下に持ってきた。
sdkデータを直接生成してもよい。

# python アプリ コンテナ構築

サンプル用にペラペラのアプリ構築する。

## app/Dockerfile

```Dockerfile
FROM python:3.7-slim-buster

ENV HOME /api/

ADD ./ ${HOME}

WORKDIR ${HOME}

RUN pip install --upgrade pip \
    && pip install --no-cache-dir -r ${HOME}src/requirements.txt

ENTRYPOINT ["python", "src/main.py"]
```

## app/main.py

ちょっと適当すぎる気がするが、データを保存して取り出すだけのエンドポイントを作成。 :thinking: 

認証情報は、ダミーの認証をかませる。

```python
from flask import Flask, jsonify
from google.auth.credentials import AnonymousCredentials
from google.cloud import datastore
from os import getenv

client = datastore.Client(
    credentials=AnonymousCredentials(),
    project=getenv('PROJECT_ID')
)

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False


@app.route('/')
def index():
    key = client.key('EntityKind', 1234)
    entity = datastore.Entity(key=key)
    entity.update({
        'foo': u'bar'
    })
    client.put(entity)
    result = client.get(key)
    return jsonify(result)


if __name__ == '__main__':
    app.run(host='0.0.0.0')

```

## app/requirements.txt

```
Flask==1.1.1
google-auth==1.6.2
google-cloud-datastore==1.8.0
```

# docker-compose.yaml

datastoreのポートは、ホスト側にも開けておくと、GUIツールで見ることができて便利

https://github.com/GabiAxel/google-cloud-gui

```yaml
version: '3.7'

x-custom:
  gcp:
    - &gcp_project_id "dummy"

services:
  app:
    build: "./app/"
    volumes:
      - "./app/:/app/"
    environment:
      FLASK_APP: dev
      DATASTORE_HOST: "http://datastore:8081"
      DATASTORE_EMULATOR_HOST: "datastore:8081"
      PROJECT_ID: *gcp_project_id
      TZ: Asia/Tokyo
    ports:
      - "5000:5000"
    depends_on:
      - datastore
  datastore:
    build: "./datastore"
    volumes:
      - "./datastore/.data:/datastore/.data"
    environment:
      DATASTORE_PROJECT_ID: *gcp_project_id
      DATASTORE_LISTEN_ADDRESS: 0.0.0.0:8081
    ports:
      - "18081:8081"
```

# 起動

```
docker-compose up 
```

ブラウザやcurlで、http://localhost:5000にアクセスすると{"foo": "bar"}と表示されるはず。

## ログ

下記にデータが貯められてるのがわかる
```
/datastore/.data/WEB-INF/appengine-generated/local_db.bin
```

```
datastore_1  | Updated property [core/project].
datastore_1  | WARNING: Reusing existing data in [/datastore/.data].
datastore_1  | Executing: /google-cloud-sdk/platform/cloud-datastore-emulator/cloud_datastore_emulator start --host=0.0.0.0 --port=8081 --store_on_disk=True --consistency=0.9 --allow_remote_shutdown /datastore/.data
app_1        |  * Serving Flask app "main" (lazy loading)
app_1        |  * Environment: production
app_1        |    WARNING: This is a development server. Do not use it in a production deployment.
app_1        |    Use a production WSGI server instead.
app_1        |  * Debug mode: off
app_1        |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
datastore_1  | [datastore] Jan 22, 2020 6:22:00 AM com.google.cloud.datastore.emulator.CloudDatastore$FakeDatastoreAction$9 apply
datastore_1  | [datastore] INFO: Provided --allow_remote_shutdown to start command which is no longer necessary.
datastore_1  | [datastore] Jan 22, 2020 6:22:01 AM com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub <init>
datastore_1  | [datastore] INFO: Local Datastore initialized:
datastore_1  | [datastore]      Type: High Replication
datastore_1  | [datastore]      Storage: /datastore/.data/WEB-INF/appengine-generated/local_db.bin
datastore_1  | [datastore] Jan 22, 2020 6:22:02 AM com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub load
datastore_1  | [datastore] INFO: Time to load datastore: 218 ms
datastore_1  | [datastore] API endpoint: http://0.0.0.0:8081
datastore_1  | [datastore] If you are using a library that supports the DATASTORE_EMULATOR_HOST environment variable, run:
datastore_1  | [datastore] 
datastore_1  | [datastore]   export DATASTORE_EMULATOR_HOST=0.0.0.0:8081
datastore_1  | [datastore] 
datastore_1  | [datastore] Dev App Server is now running.
datastore_1  | [datastore] 
datastore_1  | [datastore] The previous line was printed for backwards compatibility only.
datastore_1  | [datastore] If your tests rely on it to confirm emulator startup,
datastore_1  | [datastore] please migrate to the emulator health check endpoint (/). Thank you!
datastore_1  | [datastore] The health check endpoint for this emulator instance is http://0.0.0.0:8081/Jan 22, 2020 6:22:11 AM io.gapi.emulators.grpc.GrpcServer$3 operationComplete
datastore_1  | [datastore] INFO: Adding handler(s) to newly registered Channel.
datastore_1  | [datastore] Jan 22, 2020 6:22:11 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
datastore_1  | [datastore] INFO: Detected HTTP/2 connection.
```

# データ投入

エミュレータのエンドポイントに、リクエストを投げてあげれば、ダンプデータも取り込める

```
docker-compose exec datastore bash ./import/run.sh
```

# 参考
下記のサイトを参考にさせていただきました。

- [Cloud Datastore Emulator を Node.js から Docker コンテナ間で接続利用する - Qiita](https://qiita.com/ideodora/items/a292953a80940ea06d11)
- [Python Client for Google Cloud Datastore — google-cloud-datastore 0.1.0 documentation](https://googleapis.dev/python/datastore/latest/index.html)