Cloud Spanner を定期的に export する

定期的に Cloud Spanner のデータを外部へ書き出したい場合 [^1] は、Cloud Scheduler から Cloud Dataflow を起動させるることで実現できる。

## 全体像まとめ

![Screenshot 2021-07-28 111612.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32329/87463215-a2e5-3cfa-adc7-c5fab0bfa03a.png)

- Spanner のデータを Avro 形式で GCS に書き出す
- Cloud Scheduler から定期的に export 用の Dataflow Job を起動させる

Terraform 設定例 

https://github.com/komazarari/terraform-export-google-spanner

--------------------
以下詳細

## Spanner の export 方法

export を行うには主に、

- GCP コンソールから行う
- Google 提供のテンプレートを使って Dataflow job を作成する

方法がある。Dataflow のトリガーは gcloud CLI や HTTP API などから行えるので、これを使って定期実行させる。

### export 用 Dataflow テンプレートを使う

Dataflow 向けに、典型的な操作を行うためのテンプレートが Google から提供されている
([Google 提供のテンプレートを使ってみる](https://cloud.google.com/dataflow/docs/guides/templates/provided-templates?hl=ja))。この中に Spanner からの export を行うテンプレートも用意されている。今回は [`Cloud Spanner to Cloud Storage Avro`](https://cloud.google.com/dataflow/docs/guides/templates/provided-batch?hl=ja#cloudspannertogcsavro) を使う。


このテンプレートは、次のような必須パラメータを取る。最低限これらを渡して Dataflow を起動させるだけでよい。

- `instanceId`
- `databaseId`
- `outputDir` (Cloud Storage のパス)

その他のパラメータや gcloud での実行例等、詳細についてはドキュメントを参照のこと

https://cloud.google.com/dataflow/docs/guides/templates/provided-batch?hl=ja#cloudspannertogcsavro

## Cloud Scheduler から Dataflow job を起動する
本記事とは別のテンプレートだが、こちらに例がある。

https://cloud.google.com/community/tutorials/schedule-dataflow-jobs-with-cloud-scheduler

ざっくり言えば

- Dataflow job の作成権限をもったサービスアカウントを使う
- HTTP ターゲットで `https://dataflow.googleapis.com/v1b3/projects/<プロジェクト名>/locations/<リージョン>/templates` を叩く
- リクエストボディに Dataflow テンプレートのパラメータを書く

な Scheduler job を作ればよい。

## Terraform 設定例

- export 先 Cloud Storage
- 実行用サービスアカウント
- Scheduler job

を作るための Terraform 例。全ファイルを置いたレポジトリは [こちら( `komazarari/terraform-export-google-spanner` )](https://github.com/komazarari/terraform-export-google-spanner)

### パラメータ

```hcl:variables.tf
variable "project_id" {
  type = string
}
variable "spanner_instance" {
  type = string
}
variable "spanner_db" {
  type = string
}
variable "bucket_name" {
  type = string
}
variable "region" {
  type        = string
  default     = "asia-northeast1"
  description = "GCS と Dataflow job の実行リージョン. Spanner インスタンスと同じにする"
}
variable "export_schedule" {
  type    = string
  default = "0 0 * * *"
}
variable "days_buckups" {
  type        = number
  default     = 7
  description = "export したデータを保持する日数"
}
variable "dataflow_template_version" {
  type    = string
  default = "2021-07-05-00_RC01"
}
```

### Cloud Storage (期限付き)
今回は定期的に export して溜まっていく想定なので、一定期間を過ぎたものを lifecycle で削除するようにして Cloud Storage を作成する。

```hcl:gcs.tf
resource "google_storage_bucket" "bucket" {
  name     = var.bucket_name
  location = var.region

  lifecycle_rule {
    condition {
      age = var.days_buckups
    }
    action {
      type = "Delete"
    }
  }
}
```

### 実行用サービスアカウント
GCP 管理ロールの `dataflow.developer` を付与する。アカウント名はなんでもよい。

```hcl:sa.tf
resource "google_service_account" "export_user" {
  account_id   = "${var.spanner_instance}-${var.spanner_db}-export"
  display_name = "${var.spanner_instance}-${var.spanner_db}-export"
}
resource "google_project_iam_member" "export_user_dataflow_role" {
  role   = "roles/dataflow.developer"
  member = "serviceAccount:${google_service_account.export_user.email}"
}
```

### Cloud Scheduler
テンプレートに必要なパラメータを base64エンコードした JSON で渡す。サービスアカウントの認証のために `oauth_token` ブロックを書く。

```hcl:scheduler.tf
resource "google_cloud_scheduler_job" "export_job" {
  name     = "${var.spanner_instance}-${var.spanner_db}-export-job"
  schedule = var.export_schedule
  region   = var.region

  http_target {
    http_method = "POST"
    uri         = "https://dataflow.googleapis.com/v1b3/projects/${var.project_id}/locations/${var.region}/templates"

    oauth_token {
      service_account_email = google_service_account.export_user.email
    }

    body = base64encode(<<-EOT
    {
      "jobName": "${var.spanner_instance}-${var.spanner_db}-export-job",
      "parameters": {
        "instanceId": "${var.spanner_instance}",
        "databaseId": "${var.spanner_db}",
        "outputDir": "${google_storage_bucket.bucket.url}/"
      },
      "gcsPath": "gs://dataflow-templates/${var.dataflow_template_version}/Cloud_Spanner_to_GCS_Avro",
      "environment": {
        "tempLocation": "${google_storage_bucket.bucket.url}/_staging"
      }
    }
EOT
    )
  }
}
```

### 動作確認
Cloud Scheduler を手動実行して Cloud Storage にファイルが生成されていることを確認する。

## 参考
- [Exporting databases from Cloud Spanner to Avro](https://cloud.google.com/spanner/docs/export)
- [Google 提供のバッチ テンプレート](https://cloud.google.com/dataflow/docs/guides/templates/provided-batch?hl=ja#cloudspannertogcsavro)
- [Schedule Dataflow batch jobs with Cloud Scheduler](https://cloud.google.com/community/tutorials/schedule-dataflow-jobs-with-cloud-scheduler)
- [google_cloud_scheduler_job | Resources | hashicorp/google | Terraform Registry](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_scheduler_job)

[^1]: Spanner のデータ保護の主な仕組みとして **バックアップ**、**ポイントインタイムリカバリ**、それと本記事で言及している export がある。それぞれ別の目的で使える (https://cloud.google.com/spanner/docs/backup) ので、そもそも定期的な export が必要かどうか、から要検討