🐈

Firestore(Datastoreモード)のデータベースをPoint-in-time recoveryで復元する

2023/08/25に公開

1. はじめに

はじめまして、クラウドエース データML ディビジョン所属の尾杉です。
クラウドエースのITエンジニアリングを担うシステム開発部の中で、特にデータ基盤構築・分析基盤構築からデータ分析までを含む一貫したデータ課題の解決を専門とするのがデータML ディビジョンです。

データML ディビジョンでは、Google Cloud が提供しているデータ領域のプロダクトについて、新規リリースをキャッチアップするための調査報告会を毎週実施しています。
その中で特に重要と考えるリリースを記事としてまとめ、本ページのように公開しています。

本ページには、コマンドやソースコードが記載されています。
もしご利用の際は、[$$] の中身を適宜変更してください。

2. リリース内容

今回ご紹介するリリースは、2023年7月14日付にプレビュー提供された Datastore モードの Firestore(以下、Datastore)の「Point-in-time recovery」という機能についてです。

Point-in-time recovery を活用することで、偶発的な削除や書き込みに対してデータを保護できるメリットがあります。

3. Datastore の概要

Datastore とは、Google Cloud が提供している NoSQL ドキュメントデータベースサービスの1つです。

4. Point-in-time recovery の概要

Datastore で提供されている Point-in-time recovery(以下、PITR)は、過去の時点(最大7日間)のデータを復元できる機能です。
PITR には、Fine-grained windowCoarse-grained window の2つの機能があり、それぞれで制約が異なるので注意が必要です。

Fine-grained window Coarse-grained window
保持期間 1時間以内のエンティティの履歴を保持 7日前から1時間前までのエンティティの履歴を保持
タイムスタンプ マイクロ秒単位で識別 分単位で識別
※1分間以内に複数回の書き込みがあった場合、最後の書き込みが採用される
費用 なし あり

PITR が無効になっているデフォルト状態では Fine-grained window が適用され、PITR を有効にすると Fine-grained windowCoarse-grained window の両方が適用されます。
また PITR を無効にすると、PITR を有効にしていた間 Coarse-grained window で保持していたデータは完全に削除されます。再度有効にしてもこのデータは復元されません。

4.1 復元の方法

本節では、PITR を利用してデータを復元する方法をまとめます。
また復元方法によって復元可能な期間が異なる点にご注意ください。

復元方法は、データベース全体を復元する方法データベースの一部を復元する方法 の2つがあります。
以下でそれぞれの復元方法について説明します。

4.1.1 データベース全体を復元する方法

復元方法は、復元したい過去の時点を指定してデータベースを Cloud Storage にエクスポートし、それを新しいデータベースとしてインポートするといった方法になります。

注意点が2つあります。
<注意点>
復元できるデータベースは1時間以内に限る
 これは復元に利用されるウィンドウが Fine-grained window になるためです。
 そのためエクスポートできるデータベースは1時間以内の状態になり、復元可能な状態も1時間以内に限定されます。
 もし1時間をこえたタイムスタンプを指定すると以下のようなエラーが出力されます。

ERROR: (gcloud.alpha.firestore.export) INVALID_ARGUMENT: The requested export snapshot timestamp is outside the valid window of 1 hr

タイムスタンプは分単位までしか指定できない
 前述で Fine-grained window が適用されると記載しましたが、指定できるタイムスタンプの粒度は1分単位になります。秒以下を指定してエクスポートすることはできません。

<使用するコマンド一覧>
以下のコマンドでデータベースを Cloud Storage にエクスポートすることができます。

$ gcloud alpha firestore export gs://[$BUKET-NAME$] \
 --snapshot-time=[$'YYYY-MM-DDThh:mm:00.000000Z'$]
  • [$BUKET-NAME$]
    データベースをエクスポートする Cloud Storage のバケット名を入力します。

  • [$'YYYY-MM-DDThh:mm:00.000000Z'$]
    復元したい時点のタイムスタンプを [$'YYYY-MM-DDThh:mm:00.000000Z'$] に入力します。

以下のコマンドで Cloud Storage からデータベースにインポートすることができます。

$ gcloud datastore import gs://[$BUKET-NAME$]/[$FOLDER-NAME$]/[$FOLDER-NAME$].overall_export_metadata --async
  • [$FOLDER-NAME$]
    エクスポートによってCloud Storage のバケットに保存されたフォルダ名に入力します。

4.1.2 データベースの一部を復元する方法

復元方法は、復元したい過去の時点を指定してデータベースを読み取り、その結果を insert や update などで書き込むといった方法になります。
この方法ですと PITR を有効にしている場合、4.1.1節 とは異なり過去7日以内のタイムスタンプを指定することができます。

以下のコードは、ドキュメントのクライアントライブラリを利用したPythonコードを一部変更したものです。
実行すると、実行時点のエンティティの内容とタイムスタンプで指定した時点のエンティティの内容の2行が出力されます。

from datetime import datetime, timezone
from google.cloud import datastore

client = datastore.Client(project = [$PROJECT-NAME$])

# read_time = datetime.now(tz=timezone.utc)
read_time = datetime.strptime([$'YYYY-MM-DDThh:mm:ss.ssssssZ'$],"%Y-%m-%dT%H:%M:%S.%fZ")

key = client.key([$Kind$], [$Key_id$], napespace=[$NAME-SPACE$](※デフォルトなら不要))

# read without PITR read time
entity = client.get(key)
print(entity)

# read with PITR read time
entity = client.get(key, read_time=read_time)
print(entity)

# PITR read using read_only transaction
with client.transaction(read_only=True, read_time=read_time):
    entity = client.get(key)

query = client.query(kind=[$Kind$])

# run query without PITR read time
iterator = query.fetch()

# run query with PITR read time
iterator = query.fetch(read_time=read_time)

# PITR read query using read_only transaction
with client.transaction(read_only=True, read_time=read_time):
    iterator = query.fetch()

5. 検証:バックアップからデータベース全体を復元

ここまでは Datastore と PITR について述べてきました。
本章では、実際に Datastore のデータベースを作成から削除まで行い、その後 PITR を使って復元した手順を記載します。

Step1. データベースの作成

Cloud Shell をアクティブにして、以下のコマンドを実行しDatastore モードのデータベースを作成します。

$ gcloud alpha firestore databases create \
 --location=[$LOCATION$] \
 --type=[$DATABASE-TYPE$] \ # データベースモードを使用するためdatastore-modeと入力する
 --enable-pitr

API を有効していないと「有効にして再実行するか?」聞かれるので、その場合は y を押して Enter を押します。

API [firestore.googleapis.com] not enabled on project [$PROJECT-NAME$]. Would you like to enable and retry (this will take a few minutes)? (y/N)?

すると以下のような実行結果が出力されます。

metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.CreateDatabaseMetadata
name: projects/[$PROJECT-NAME$]/databases/(default)/operations/MXRzYWUtc3UIIgUQAYzzu4gQBqbmorYIDAoaGg
response:
  '@type': type.googleapis.com/google.firestore.admin.v1.Database
  appEngineIntegrationMode: DISABLED
  concurrencyMode: PESSIMISTIC
  createTime: '2023-08-14T02:28:06.295493Z'
  deleteProtectionState: DELETE_PROTECTION_DISABLED
  earliestVersionTime: '2023-08-14T02:28:06.295493Z'
  etag: IKKpi4CN24ADMMX3ioCN24AD
  keyPrefix: p
  locationId: [$LOCATION$]
  name: projects/[$PROJECT-NAME$]/databases/(default)
  pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_ENABLED
  type: DATASTORE_MODE
  uid: 4ebbd4be-afee-4e5f-ae37-52ecd7d59986
  updateTime: '2023-08-14T02:28:06.295493Z'
  versionRetentionPeriod: 604800s

ここで response: の中身について確認します。

  1. earliestVersionTime
    この項目はPITRによって復元できる過去最大の日時を表示しています。
    上記の出力結果では 2023-08-14T02:28:06.295493Z となっており、この日時から現在時刻の間のPITRを読み取れます。また最大保持期間の7日間ルールがあるため、時間が経つにつれて更新されます。

  2. pointInTimeRecoveryEnablement
    この項目はPITRが有効/無効のどちらの状態かを表示しています。
    PITR が有効になっていると末尾が ENABLED に、無効になっていると末尾が DISABLED になります。上記の出力結果では POINT_IN_TIME_RECOVERY_ENABLED となっていることから、PITR が有効になっていることが分かります。

  3. versionRetentionPeriod
    PITR バージョンの保持期間を表示しています。
    上記の出力結果では 604800s となっており、保持期間が7日間となっていることが分かります。

これらより、今回作成したデータベースは PITR が有効になっていることが分かります。

Step2. エンティティの作成

次に検証に使用するエンティティを作成します。
コンソール画面から Datastore のページに遷移します。

エンティティの作成を押下し、空欄を埋めていきます。
今回は以下のようなプロパティで作成しました。

Step3. データベース保存用のバケットを作成

Cloud Storage にデータベースを保存するバケットを作成します。

$ gcloud storage buckets create gs://[$BUKET-NAME$] --location=[$LOCATION$]

Step4. エンティティを削除

コンソール画面からエンティティを削除します。
削除すると先ほど作成したエンティティが消え、データベースが空になっていることが確認できます。

Step5. データベースの格納

次に削除前のデータベースを Step3 で作成したバケットに保存します。
--snapshot-time で復元したいデータベースの日時を指定して以下のコマンドを実行します。

$ gcloud alpha firestore export gs://[$BUKET-NAME$] \
 --snapshot-time=[$'YYYY-MM-DDThh:mm:00.000000Z'$]

実行後、コンソール画面からインポート/エクスポート画面に遷移するとデータベースが Cloud Storage にエクスポートされていることが分かります。

Step6. エンティティを復元

Step5でエクスポートしたデータベースをもとにエンティティの復元を行います。
以下が Cloud Storage からデータベースをインポートするコマンドになります。

$ gcloud datastore import gs://[$BUKET-NAME$]/[$FOLDER-NAME$]/[$FOLDER-NAME$].overall_export_metadata --async

これにより、エクスポートしたファルダの中にある .overall_export_metadata というメタデータファイルをインポートします。


コマンド実行後、コンソール画面からエンティティを確認すると削除前の状態に戻っていることが分かります。

これで無事に復元できていることが確認できました。

6. 気になったところを調べてみた

5章 では、実際にデータベース全体の復元を実施しました。
本章は、検証してみた中で気になったところについて調査した内容を記載します。

6.1. データ保持期間以外のタイムスタンプは本当に無効か?

earliestVersionTime: '2023-08-14T01:34:51.077276Z' のデータベースに対して、これよりも早い時点でバックアップの作成を試みました。すると、以下のようなエラーメッセージが表示され、 earliestVersionTime より早い時間を復元ポイントとして指定することはできないといわれました。
この結果より、データ保持期間以外のタイムスタンプは無効だとわかりました。

ERROR: (gcloud.alpha.firestore.export) INVALID_ARGUMENT: Requested export snapshot timestamp is too old. The snapshot timestamp must be earlier than the allowed Point-in-Time Recovery read time of 2023-08-14T02:28:03.393996Z

6.2. PITR が無効の時、Fine-grained window は利用できるのか?

PITR が無効の時、Fine-grained window は利用できるのかを検証します。

まずはデータベースを作成します。
以下が実行コマンドとレスポンスになります。

gcloud alpha firestore databases create \
 --location=[$LOCATION$] \
 --type=datastore-mode
metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.CreateDatabaseMetadata
name: projects/[$PROJECT-NAME$]/databases/(default)/operations/MXRzYWUtc3UIIgUQA4-LiOgQBqb8r-IIDAoaGg
response:
  '@type': type.googleapis.com/google.firestore.admin.v1.Database
  appEngineIntegrationMode: DISABLED
  concurrencyMode: PESSIMISTIC
  createTime: '2023-08-18T07:04:02.836945Z'
  deleteProtectionState: DELETE_PROTECTION_DISABLED
  earliestVersionTime: '2023-08-18T07:04:02.836945Z'
  etag: IMvE5JHS5YADMNGD5JHS5YAD
  keyPrefix: p
  locationId: [$LOCATION$]
  name: projects/[$PROJECT-NAME$]/databases/(default)
  pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_DISABLED
  type: DATASTORE_MODE
  uid: dfe4bacd-ae08-4a4f-9e74-bc4d05f49ad1
  updateTime: '2023-08-18T07:04:02.836945Z'
  versionRetentionPeriod: 3600s

deleteProtectionState: DELETE_PROTECTION_DISABLED より、PITR が無効の状態で作成できていることがわかります。

ここからはドキュメントを参考にエンティティの作成から削除、追加までを行います。
以下のPythonコードでエンティティを作成します。

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

task = datastore.Entity(client.key("Task"))
task.update(
    {
        "category": "Personal",
        "done": False,
        "priority": 4,
        "description": "Learn Cloud Datastore",
    }
)

client.put(task)

エンティティ作成後、category プロパティを削除します。

次に 4.1.2節 に記載したPythonコードを実行し、削除前後のエンティティの内容を確認します。

<Entity('Task', XXKey_id省略XX) {'description': 'Learn Cloud Datastore', 'priority': 4, 'done': False}>
<Entity('Task', XXKey_id省略XX) {'done': False, 'description': 'Learn Cloud Datastore', 'priority': 4, 'category': 'Personal'}>

1行目が現在のエンティティの内容で、2行目がタイムスタンプで指定した時点のエンティティの内容です。
category プロパティの有無より、削除前後であることがわかります。
この結果より、PITR が無効の時 Fine-grained window は利用できることがわかりました。

最後に2行目のエンティティの内容を以下のPythonコードでinsertし、category プロパティを持つ新しいエンティティを作成します。
新規のエンティティを作成したくない場合は、key id を指定することで回避できます。

from google.cloud import datastore

# For help authenticating your client, visit
# https://cloud.google.com/docs/authentication/getting-started
client = datastore.Client()

with client.transaction():
    incomplete_key = client.key("Task")

    task = datastore.Entity(key=incomplete_key)

    task.update(
        {'done': False, 'description': 'Learn Cloud Datastore', 'priority': 4, 'category': 'Personal'}
    )

    client.put(task)

6.3. 既存のデータベースに対してPITRを有効したとき、過去何日まで有効範囲になるのか?

既存のデータベースに対してPITRを有効したとき、過去何日まで PITR の有効範囲になるのかを検証します。

6.2節 で作成したデータベースを流用します。
まずは、以下のコマンドで作成したデータベースの response: を確認します。

gcloud firestore databases describe
appEngineIntegrationMode: DISABLED
concurrencyMode: PESSIMISTIC
createTime: '2023-08-18T07:04:02.836945Z'
deleteProtectionState: DELETE_PROTECTION_DISABLED
earliestVersionTime: '2023-08-18T07:31:18.608564Z'
etag: IJuNstLl5YADMLOK1PPW5YAD
keyPrefix: p
locationId: [$LOCATION$]
name: projects/[$PROJECT-NAME$]/databases/(default)
pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_DISABLED
type: DATASTORE_MODE
uid: dfe4bacd-ae08-4a4f-9e74-bc4d05f49ad1
updateTime: '2023-08-18T07:04:02.836945Z'
versionRetentionPeriod: 3600s

pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_DISABLED より、PITRが無効なことがわかります。

次に以下のコマンドで PITR を有効にします。

$ gcloud alpha firestore databases update --enable-pitr
Request issued for: [(default)]
Updated database [(default)].
done: true
metadata:
  '@type': type.googleapis.com/google.firestore.admin.v1.UpdateDatabaseMetadata
name: projects/[$PROJECT-NAME$]/databases/(default)/operations/BhADj4uI6BAGpvyv4ggMChAa
response:
  '@type': type.googleapis.com/google.firestore.admin.v1.Database
  appEngineIntegrationMode: DISABLED
  concurrencyMode: PESSIMISTIC
  createTime: '2023-08-18T07:04:02.836945Z'
  deleteProtectionState: DELETE_PROTECTION_DISABLED
  earliestVersionTime: '2023-08-18T07:37:00Z'
  etag: INeO1fLm5YADMKXY1PLm5YAD
  keyPrefix: p
  locationId: [$LOCATION$]
  name: projects/[$PROJECT-NAME$]/databases/(default)
  pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_ENABLED
  type: DATASTORE_MODE
  uid: dfe4bacd-ae08-4a4f-9e74-bc4d05f49ad1
  updateTime: '2023-08-18T07:04:02.836945Z'
  versionRetentionPeriod: 604800s

pointInTimeRecoveryEnablement: POINT_IN_TIME_RECOVERY_ENABLED より、PITR が有効になったことがわかります。

PITR を有効にした時点で、date コマンドで現在の時刻を確認したところ Fri 18 Aug 2023 08:36:02 AM UTC でした。earliestVersionTime:'2023-08-18T07:37:00Z' との差は約1時間という結果より、PITR を有効化した時点より約1時間前までが有効範囲となることが分かりました。
推察になりますが約1時間という値は、デフォルトで Fine-grained window が機能している効果かと思われます。

7. まとめ

Datastore の PITR についてのご紹介は以上になります。
まだプレビュー段階の機能ですが、うまく活用すれば偶発的な削除や書き込みに対してデータを保護できるように感じました。
Datastore を使用されている方はぜひご利用を検討してみてください!

Discussion