DatastoreのExport機能でバックアップの自動化

SENSY株式会社のwasnotです。

今までpythonでのサーバサイド開発を行っていましたが、
最近エンジニアチームからAI開発チームに異動しました。
まだまだ勉強ばかりです。。

今日はGCPのDatastoreをバックアップ自動化してみた話を書きます。

# Datastore
DatastoreはGCP、特にAppEngineを使っていたら大体の人が使っているKVSなDBです。
SENSYのサービスでも使っています。

この内容はDBの機能として

ユーザの情報をバックアップしたり、
バックアップデータをBigQueryに入れてデータ数の増加傾向などを把握する目的で
Datastoreの情報をバックアップしています。

以前は定期的に手動で行っていたんですが、煩雑になるので

# 既存のバックアップツール

Datastoreにはそもそも管理機能があり、そこでバックアップやインポートなどができました。

- [Datastore Admin](https://console.cloud.google.com/project/_/datastore/settings)


`https://ah-builtin-python-bundle-dot-[PROJECT_ID].appspot.com/_ah/datastore_admin`
Console->Datastore->管理->データストア管理
の項目を有効にすると、pythonの管理ツールが`ah-buildin-python-bundle`モジュールにデプロイされるようです。

ツールはUIがあり、こんな感じです。

![スクリーンショット 2017-12-01 19.00.43_2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/34d120f5-2831-28bf-3722-9441adad726c.png)

選択してGCSにバックアップやGCSからimportなどもできます。

## 自動バックアップ

自動バックアップについてもドキュメントが用意されており、使うことができました。

- [Scheduled Backups](https://cloud.google.com/appengine/articles/scheduled_backups)

以下のようなcronを設定するだけで、簡単にできるようです。

```yaml:cron.yaml
cron:
- description: My Daily Backup
  url: /_ah/datastore_admin/backup.create?name=BackupToCloud&kind=LogTitle&kind=EventLog&filesystem=gs&gs_bucket_name=whitsend
  schedule: every 12 hours
  target: ah-builtin-python-bundle
```

この解説記事がオススメです。

- [Datastoreのバックアップを取得する](https://qiita.com/chidakiyo/items/bb2e49c6f99b7210fa0f)

ただ、なぜか任意のバケットに保存しようとしても上手くいかないケースもありました。
（理由はベータ版ツールのドキュメントに書いてあった通り、Regionが異なるためだったようです。）

# β版のExport/Import機能

[AppEngine ja night #2 ](https://gaeja.connpass.com/event/68702/)に参加して気づいたんですが、
DatastoreのExport/Importツールがベータで公開されていました。
上のスクリーンショットでも
`A new Cloud Datastore service has launched to beta for Exporting and Importing. `
との記載がありますね。

- [エンティティのエクスポートとインポート ](https://cloud.google.com/datastore/docs/export-import-entities)

さらに自動エクスポートのチュートリアルまであります！
pythonのサンプル付きです。

- [エクスポートのスケジューリング ](https://cloud.google.com/datastore/docs/schedule-export)

すでに触っている人も多そうですが、今回はこれを動かしてみました。

## 手順

断っておくと以下はチュートリアルに若干手を加えて解説した程度です。コピペですみません。

### 前準備
1. GCSを使うので課金ステータスをチェック.  
AppEngine defaultのbucket(`[PROJECT_ID].appspot.com`)なら無料5GBで使えるのかもしれません
2. GCSのバケット作成  
Datastoreと同一リージョン、デフォルトクラスはMultiRegional/Regionalにする。
3. AppEngineのService Accountに`roles/datastore.importExportAdmin`を付与する
IAMで設定できます。また、コマンドラインで行う場合は以下のようにしてください。
    
    ```
    gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
    --member serviceAccount:YOUR_PROJECT_ID@appspot.gserviceaccount.com \
    --role roles/datastore.importExportAdmin
    ```

4. AppEngineのService Accountに2.で作ったバケットへの書き込み権限を付与します。
こちらはStorageの管理画面からバケットの権限編集で追加できます。
もしくはIAMでプロジェクトのストレージ権限と、バケットの権限を組み合わせて設定します。
コマンドラインでやる場合は以下のようにします。

    ```
    gsutil iam ch serviceAccount:YOUR_PROJECT_ID@appspot.gserviceaccount.com:objectCreator \
    gs://BUCKET_NAME
    ```

権限は今回はこのようにしました。

![スクリーンショット 2017-12-01 19.32.57_2.png](https://qiita-image-store.s3.amazonaws.com/0/42594/a61eecd3-11ad-d78b-0c11-e99b343da1a0.png)

プロジェクト全体の編集権限はあまり付与しない方がいいかもしれません。

### スクリプトの追加

インストラクションでは新規のアプリ作成を前提に作成されていますが、
今回は既存のpythonのAppEngine/SEアプリに追加する想定で書きます。

#### app.yaml
まずはapp.yamlにendpointを追加してください。

```yaml:app.yaml
handlers:
- url: /cloud-datastore-export
  script: cloud_datastore_admin.app
  login: admin
```

すでにadmin権限のツールやcron/taskqueue用のendpointでadminフィルタしている場合はそのendpointに一緒に登録してもいいと思います。

#### pythonスクリプト
メインのスクリプトです。

実際にはappの初期化等は他のエンドポイントとまとめて書いたりしています。
あと、サンプルではcronのファイル自体にbucketやkindの設定を書いていますが、
それだと管理しづらいのでscript自体で取得したりするように修正しています。

```py:cloud_datastore_admin.py
import datetime
import httplib
import json
import logging
import webapp2

from google.appengine.api import app_identity
from google.appengine.api import urlfetch

from models import User, Item


BUCKET = 'backup-{}'

class Export(webapp2.RequestHandler):

  def get(self):
    access_token, _ = app_identity.get_access_token(
        'https://www.googleapis.com/auth/datastore')
    app_id = app_identity.get_application_id()
    timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%S')
    # bucketはapp_idに応じて変更されるようにしています。
    bucket_name = BUCKET.format(app_id)

    # bucket名の指定は`gs://`から始まる必要があります。
    output_url_prefix = 'gs://{}/{}'.format(bucket_name, timestamp)

    # 保存するentityの条件をrequest bodyに含めます。
    # 今回はAppEngineアプリ内のModel定義からkind名を取得しています。
    entity_filter = {
        'kinds': [
            User.__name__, Item.__name__,
        ],
        'namespace_ids': []
    }
    # 以下はチュートリアルのまま
    request = {
        'project_id': app_id,
        'output_url_prefix': output_url_prefix,
        'entity_filter': entity_filter
    }
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + access_token
    }
    url = 'https://datastore.googleapis.com/v1beta1/projects/{}:export'.format(app_id)
    try:
      result = urlfetch.fetch(
          url=url,
          payload=json.dumps(request),
          method=urlfetch.POST,
          deadline=60,
          headers=headers)
      if result.status_code == httplib.OK:
        logging.info(result.content)
      elif result.status_code >= 500:
        logging.error(result.content)
      else:
        logging.warning(result.content)
      self.response.status_int = result.status_code
    except urlfetch.Error:
      logging.exception('Failed to initiate export.')
      self.response.status_int = httplib.INTERNAL_SERVER_ERROR


app = webapp2.WSGIApplication(
    [
        ('/cloud-datastore-export', Export),
    ], debug=True)
```

見ればわかりますが、
`https://datastore.googleapis.com/v1beta1/projects/[PROJECT_ID]:export`
というREST APIにトークン付きでリクエストしているだけのシンプルなスクリプトです。
python以外に変更することも簡単ですね。


#### cronの設定

最後に自動実行用にcron.yamlを作成します。
今回はスクリプト内で動的にバックアップ対象を決めることにしたので、オプションは無しにしています。

```yaml:cron.yaml
cron:
- description: "Daily Cloud Datastore Export"
  url: /cloud-datastore-export
  target: cloud-datastore-admin
  schedule: every day 03:00
  timezone: Asia/Tokyo
  target: SERVICE.VERSION
```

targetの項目はdefaultサービス・defaultバージョンじゃない場合のみ指定します。
今回はサービスが使われていなそうな、日本時間の3時にバックアップ開始するように設定してみました。


# 注意点
今回の記事のメインです。

## バケットとAppEngine(Datastore)のリージョンが異なると動かない
既存のバックアップツールでも同じかと思いますが、リージョンが異なると動きません。

> Note: You must use the same location for your Cloud Storage bucket and Cloud Datastore. For information about determining your Cloud Datastore location, see Finding the location you chose.

と親切に注意書きがありました。

## ストレージクラスはcoldline/nearlineでは動かない
バケットを作るときにデフォルトのストレージクラスを選ぶと思います。
今まではBigqueryで読み出したらほぼ使わないので、バックアップ用のバケットはColdlineに設定していました。
それでも手動でぽちぽちやる場合には動いていたのですが、
今回のExportツールを使うときはcoldline/nearlineでは動きません。

```
 "message": 
    "Bucket backup-sensycloset-us has storage class NEARLINE which is not supported. 
     Must be one of regional, multi_regional, standard, durable_reduced_availability."
```

オブジェクト単位で指定できると思うので、書き出しが終わったら休眠させる方がコスト削減には繋がるかもしれません。

## 保存のされ方が違う。
ツールが違うので微妙にパスが変わっています。
他のツールで参照している場合は注意が必要ですね。

- 今まで(手動でやった場合)  
`gs://[BUCKET]/[PREFIX]/datastore_backup_datastore_backup_2017_11_20_[KIND]/`
- 新しいツール  
`gs://[BUCKET]/[PREFIX]/all_namespaces/kind_[KIND]`


# まとめ

やっていること自体は以前のバックアップツールとさほど変わらなそうですが、
以前のツールはAppEngine上でpythonスクリプトがtaskqueue上で実行されていました。
なのでインスタンス料金とかかかったり、時間がかかる場合があったりという課題があったかと思います。

もし今から自動化したい、変更したい、という場合はこのベータ版のExport/Importツールを使ってみてもいいかもしれません！
βから明けたら、使わない理由は無くなりますね！

今回は以上です。