💽

【gcloud】300秒以上かかるCloudSQLのインポート/エクスポートを終わるまで待つ方法

2024/07/26に公開

結論

エクスポート

gcloud sql export sql ${srcInstanceName} gs://${bucketName}/${fileName} \
  --project=${projectID} \
  --database=${srcDatabaseName} \
  --async \
  --quiet \
  --offload

operationID=$(gcloud sql operations list \
  --project=${projectID} \
  --instance=${srcInstanceName} \
  --filter="TYPE:EXPORT AND NOT STATUS:DONE" \
  --format="value(NAME)" \
  --limit=1)
gcloud sql operations wait ${operationID} --project=${projectID} --timeout=unlimited

インポート

gcloud sql import sql ${destInstanceName} gs://${bucketName}/${fileName} \
  --project=${projectID} \
  --database=${destDatabaseName}
  --user=${destDatabaseUserName} \
  --async \
  --quiet \

operationID=$(gcloud sql operations list \
  --project=${projectID} \
  --instance=${destInstanceName} \
  --filter="TYPE:IMPORT AND NOT STATUS:DONE" \
  --format="value(NAME)" \
  --limit=1)
gcloud sql operations wait ${operationID} --project=${projectID} --timeout=unlimited

エクスポート/インポートは一部のフラグが異なるだけです。
ポイントは、以下の手順をちゃんと踏むことです。

  1. gcloud sql export|export--async で非同期実行する
  2. 実行した(まだ完了していない)エクスポート/インポートのオペレーションIDを特定する
  3. gcloud sql operations wait ${operationID} で完了するまで待つ

--asyncをつけない同期実行だとタイムアウトで終了してしまいますが、この方法なら完了するまで待つことができます。

背景

自分がいま所属しているチームは毎週リリースをしていて、リリースの前日に「本番環境と同じ構成・スペックの環境(= ミラー環境)でのE2Eテスト」を実施することで検証を行なっています。

このミラー環境を用意するとき、本番環境のCloudSQLからデータをエクスポート → ミラー環境にインポート…というステップをGitHub Actionsを用いて実行していたのですが、先日このステップが失敗するようになりました。


ミラー環境を用意するGithub Actionsの実行履歴

ログを見ると、同じコマンドを実行しているのに失敗するようになっているのが確認できました。

2024/6/5のexport sqlコマンド:

  op=$(gcloud sql export sql production-db-replica gs://production-mirror-data/sql-data/$(TZ=JST-9 date '+%Y-%m-%d')-my_database.gz \
    --project my-project-production \
    --database=my_database \
    --quiet \
    --async \
    --offload)
  gcloud sql operations wait --project my-project-production --timeout unlimited "${op}"
  shell: /usr/bin/bash -e {0}
  env:
    # -- 環境変数 --
Serverless exports cost extra. See the pricing page for more information: https://cloud.google.com/sql/pricing.
Waiting for [https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project/operations/e2c09062-ec1a-4268-b6cc-08850000002b]...
.....................................................done.
NAME                                  TYPE    START                          END                            ERROR  STATUS
e2c09062-ec1a-4268-b6cc-08850000002b  EXPORT  2024-06-05T05:05:12.719+00:00  2024-06-05T05:22:04.936+00:00  -      DONE

2024/6/12のexport sqlコマンド:

  op=$(gcloud sql export sql production-db-replica gs://production-mirror-data/sql-data/$(TZ=JST-9 date '+%Y-%m-%d')-my_database.gz \
    --project my-project-production \
    --database=my_database \
    --quiet \
    --async \
    --offload)
  gcloud sql operations wait --project my-project-production --timeout unlimited "${op}"
  shell: /usr/bin/bash -e {0}
  env:
    # -- 環境変数 --
Serverless exports cost extra. See the pricing page for more information: https://cloud.google.com/sql/pricing.
ERROR: (gcloud.sql.operations.wait) Invalid value: exportContext:
  databases:
  - my_database
  fileType: SQL
  kind: sql#exportContext
  offload: true
  sqlExportOptions:
    mysqlExportOptions:
      masterData: 0
    schemaOnly: false
  uri: gs://production-mirror-data/sql-data/2024-06-12-my_database.gz
insertTime: '2024-06-12T05:04:51.298Z'
kind: sql#operation
name: 289944ef-3741-42da-8115-82180000002b
operationType: EXPORT
selfLink: https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project-production/operations/289944ef-3741-42da-8115-82180000002b
startTime: '2024-06-12T05:04:53.348Z'
status: RUNNING
targetId: production-db-replica
targetLink: https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project-production/instances/production-db-replica
targetProject: my-project-production
user: production-mirror-setupper@my-project-production-mirror.iam.gserviceaccount.com
Error: Process completed with exit code 1.

Invalid value: exportContext: 以下を見ればわかるのですが、 gcloud sql export sql を実行した時の出力に exportContext が含まれるようになっています。

6/5から6/12の間に仕様変更が入ったのかな?と思ってリリースノートを見てみたのですが、479.0.0 (2024-06-04) から 480.0.0 (2024-06-11) のCloud SQLの内容には関連しそうな記述を見つけられませんでした。

原因探しは一旦やめて、解決するためにどうするか考えることにしました。

status: RUNNING のログを見るに gcloud sql export sql --async 自体は成功しているので、gcloud sql operations wait に本当は何を渡さないといけないのかを調べました。

gcloud sql operations wait  |  Google Cloud CLI Documentation

OPERATION [OPERATION …]

An identifier that uniquely identifies the operation.

オペレーションっていうのは多分エクスポートやインポートのジョブのことかな?と思って実行してみるとこんな感じの結果に。

gcloud sql operations list --project=my-project-production --instance=production-db-replica --filter="TYPE:EXPORT"
NAME                                  TYPE    START                          END                            ERROR        STATUS
d7c2d9fb-b094-4d11-88bb-8bf60000002b  EXPORT  2024-06-19T07:53:15.366+00:00  2024-06-19T08:10:09.761+00:00  -            DONE
a7de7f48-ac7b-467b-a633-70560000002b  EXPORT  2024-06-19T05:05:01.952+00:00  2024-06-19T05:20:25.432+00:00  -            DONE
289944ef-3741-42da-8115-82180000002b  EXPORT  2024-06-12T05:04:53.348+00:00  2024-06-12T05:21:35.085+00:00  -            DONE
...

NAME って書いてるけど、UUIDっぽいしこれだろ!と思い、
TYPE:EXPORTかつ現在実行中のオペレーションのNAME だけを取得するようにlistのコマンドを書き直して、

gcloud sql operations list --project=my-project-production --instance=production-db-replica --filter="TYPE:EXPORT AND NOT STATUS:DONE" --format='value(NAME)'

従来のスクリプトと組み合わせるとうまくいきました。

gcloud sql export sql production-db-replica gs://production-mirror-data/sql-data/$(TZ=JST-9 date '+%Y-%m-%d')-my_database.gz \
  --project my-project-production \
  --database=my_database \
  --quiet \
  --async \
  --offload
operationID=$(gcloud sql operations list --project=my-project-production --instance=production-db-replica --filter="TYPE:EXPORT AND NOT STATUS:DONE" --format='value(NAME)')
gcloud sql operations wait --project my-project-production --timeout unlimited "${operationID}"

Serverless exports cost extra. See the pricing page for more information: https://cloud.google.com/sql/pricing.
exportContext:
  databases:
  - my_database
  fileType: SQL
  kind: sql#exportContext
  offload: true
  sqlExportOptions:
    mysqlExportOptions:
      masterData: 0
    schemaOnly: false
  uri: gs://production-mirror-data/sql-data/2024-06-12-my_database.gz
insertTime: '2024-06-19T08:42:45.241Z'
kind: sql#operation
name: 1e5fc16e-7b70-49e2-8ec7-bfe30000002b
operationType: EXPORT
selfLink: https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project-production/operations/1e5fc16e-7b70-49e2-8ec7-bfe30000002b
startTime: '2024-06-19T08:42:47.050Z'
status: RUNNING
targetId: production-db-replica
targetLink: https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project-production/instances/production-db-replica
targetProject: my-project-production
user: production-mirror-setupper@my-project-production-mirror.iam.gserviceaccount.com
Waiting for [https://sqladmin.googleapis.com/sql/v1beta4/projects/my-project-production/operations/1e5fc16e-7b70-49e2-8ec7-bfe30000002b]...done.
NAME                                  TYPE    START                          END                            ERROR  STATUS
1e5fc16e-7b70-49e2-8ec7-bfe30000002b  EXPORT  2024-06-19T08:42:47.050+00:00  2024-06-19T08:47:15.807+00:00  -      DONE

たらたら書いてきましたが、結局エクスポートのスクリプトのbefore/afterはこんな感じです。

before:

op=$(gcloud sql export sql production-db-replica gs://production-mirror-data/sql-data/$(TZ=JST-9 date '+%Y-%m-%d')-my_database.gz \
  --project my-project-production \
  --database=my_database \
  --quiet \
  --async \
  --offload)
gcloud sql operations wait --project my-project-production --timeout unlimited "${op}"

after:

gcloud sql export sql production-db-replica gs://production-mirror-data/sql-data/$(TZ=JST-9 date '+%Y-%m-%d')-my_database.gz \
  --project my-project-production \
  --database=my_database \
  --quiet \
  --async \
  --offload
operationID=$(gcloud sql operations list --project=my-project-production --instance=production-db-replica --filter="TYPE:EXPORT AND NOT STATUS:DONE" --format='value(NAME)')
gcloud sql operations wait --project my-project-production --timeout unlimited "${operationID}"

別解

この記事を書いているときに改めてCLIのリリースノートを確認してみると、2024.5.29リリースのCLI v478.0.0 にて出力が変わっていたことに気づきました。時系列的に関係ないだろと思って読んでいなかった…。

https://cloud.google.com/sdk/docs/release-notes#47800_2024-05-29

こんな感じの修正でよかったっぽいです。

op=$(gcloud sql export sql ${srcInstanceName} gs://${bucketName}/${fileName} \
  --project=${projectID} \
  --database=${srcDatabaseName} \
  --quiet \
  --async \
  --offload \
  --format="value(selfLink)" # または`--format="value(name)"`
)
gcloud sql operations wait ${op} --project=${projectID} --timeout=unlimited

まぁでも、「エクスポート/インポート開始 → 実行中のオペレーションを取得 → 終わるまで待機」と明示的に書いている方が保守しやすいのではと思います。

おわりに

この内容は所属している会社の社内LT会でも話したのですが、その時に「gcloudは自動化には適さないので(仕様が固まっている)SDKやAPIを使った方がいい」というコメントをいただきました。

確かにな…と思いつつ移行には着手していませんが、
CI/CDパイプラインを組む際には仕様変更があっても壊れにくい作りにしたり、そもそも仕様変更が入りにくいツールを選定して、デリバリーを守るのが大切だなという学びがありました。

参考

https://cloud.google.com/sdk/gcloud/reference/sql/export/sql

https://cloud.google.com/sdk/gcloud/reference/sql/import/sql

https://qiita.com/holysugar/items/d8371257748553cf8b08

Discussion