Datastoreのバックアップを取得する

GAEを利用してDatastoreのバックアップを取得する
===

単にバックアップを取得するのであれば特にプログラムを書く必要はなく、こちら(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
```

ただ、 `cron.yaml` にbucket名まで記述することになるため、1つのコードの記述で `本番環境` `ステージング環境` とbucketを切り替えることができない。
そのため、goのコードからDatastoreのバックアップを取得を試みた。

## 早速実装

処理に必要なファイルと実装は以下。

goのアプリケーションをデプロイするための記述と、出力先バケット名とバックアップの出力のprefixを記述している。
あと、バックアップ対象から除外したいKind名はカンマ区切りで入力できるように。

```app.yaml
application: hogehoge-backup-test
version: 1
runtime: go
api_version: go1

handlers:
- url: /.*
  script: _go_app
  login: admin

env_variables:
  TARGET_BUCKET_NAME: "hogehoge-backup-bucket"
  BACKUP_PREFIX: "backup-prefix"
#  IGNORE_KINDS: "b,c" # backupから除外したいkind名をカンマ区切りで
```

cron（定時実行の設定）を記述。

```cron.yaml
cron:
- description: Daily Backup
  url: /backup
  schedule: every day 02:00
  timezone: Asia/Tokyo
  target: default
```

queue設定。このqueueのtargetが重要

```queue.yaml
queue:
- name: backupQueue
  rate: 1/s
  target: ah-builtin-python-bundle
```

メインのプログラム。

```backup.go
import (
	"fmt"
	"golang.org/x/net/context"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/taskqueue"
	"net/http"
	u "net/url"
	"os"
	"strings"
)

func init() {
	http.HandleFunc("/backup", handler)
}

const (
	BackupQueueName = "backupQueue"
	BackupPath      = "/_ah/datastore_admin/backup.create"
)

func handler(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	bucketName := os.Getenv("TARGET_BUCKET_NAME")
	backupPrefix := os.Getenv("BACKUP_PREFIX")
	ignoreKindsString := os.Getenv("IGNORE_KINDS")

	kinds, err := getKinds(c)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
		return
	}

	q := u.Values{
		"name":           {fmt.Sprintf("%s-", backupPrefix)},
		"filesystem":     {"gs"},
		"gs_bucket_name": {bucketName},
	}

	ignoreMap := map[string]bool{}
	if ignoreKindsString != "" {
		ignoreKinds := strings.Split(ignoreKindsString, ",")
		for _, kind := range ignoreKinds {
			ignoreMap[kind] = true
		}
	}

	for _, kind := range kinds {
		if _, ok := ignoreMap[kind]; !ok {
			q.Add("kind", kind)
		}
	}

	backupTask := taskqueue.NewPOSTTask(BackupPath, q)

	if _, err := taskqueue.Add(c, backupTask, BackupQueueName); err != nil {
		// fail
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))
	} else {
		// success
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, http.StatusText(http.StatusOK))
	}
}

func getKinds(c context.Context) ([]string, error) {
	t := datastore.NewQuery("__kind__").KeysOnly().Run(c)
	var kinds []string
	for {
		key, err := t.Next(nil)
		if err == datastore.Done {
			break
		}
		if err != nil {
			return nil, err
		}
		if strings.HasPrefix(key.StringID(), "_") {
			continue
		}
		kinds = append(kinds, key.StringID())
	}
	return kinds, nil
}
```

だいたいこんな感じ。
ディレクトリに上記のファイルを配置して `goapp deploy ./` と実行するだけで(app.yamlの内容は各自環境に合わせる必要はありますが） appengine環境にデプロイして動作します。

上のコードでは最初に言っていたBucket名をプロジェクトごとに切り替えることができていないが、Bucket名にプロジェクト名を含めるなどとし、 `appengine.AppID(context)` でappengineのプロジェクト名を取得してBucket名の切り替えなどを行うことで対応できます。

## 上記のコードでできていること

- Datastore内の　`_ prefix` のついていないKind一覧を取ってバックアップ（prefixがついているものは各種メタ的な情報(?)なので）
- 全部取られたくない場合のため、無視させたいKindの一覧をわたせるようにした。

一般的なバックアップしたいという要求はみたせるのではないかと。

## ソースコード

githubに置きました。

https://github.com/chidakiyo/datastore-backup

## 謝辞

@sinmetal さん、 @soundTricker さん、Datastoreのバックアップに関する情報ありがとうございました。
@vvakame さん一般的なHandlerに書き直してみました。ありがとうございます。
m(_ _)m