💬

Cloudflare R2のオブジェクトをrcloneでCloud Storageに自動バックアップする

2023/10/11に公開3

最近はじめた新しいプロジェクトでは、オブジェクトストレージとしてCloudflare R2を選んでいます。R2の何よりも魅力は良心的な料金設定です。

バージョニングに対応してない問題

R2は2023年10月時点ではS3やGoogle Cloud Storageにあるようなバージョニング機能には対応していません。

要望は挙がっていますが(R2 Object Versioning and Replication - Cloudflare Community)今のところ特に動きはありません。

バージョニング機能なしの場合、コンソールやAPI経由でうっかりオブジェクトを削除/上書きしてしまったようなときに復元ができません。ユーザーのアップロードしたファイルを保管するようなWebサービスであれば、これは大きな不安要素になると思います。

rcloneでCloud Storageへバックアップ

バックアップ無しでサービスを運営したくないので、自分のプロジェクトではCloud Run jobsにより定期的にrcloneを実行し、R2からCloud Storageへとバックアップを取ることにしました。

Cloud Run jobsによりrcloneを定期実行する方法について、探してもほとんど情報が見つからなかったので記事にまとめることにしました。

(ちなみに)S3にしなかった理由

バックアップ用途では、Cloud StorageよりもS3の方が値段が安く抑えられそうでした。しかし、自分はGoogle Cloudの方が使い慣れており、Cloud Run jobsを使えば定期的にrcloneを実行する構成も組みやすそうだと考えて、Cloud Storageを選ぶことにしました。

1. rclone.confを作成

まずrclone.confを作成し、R2とCloud Storageの設定を定義します。

rclone.conf
# Cloud Storage
[gcs]
type = google cloud storage
bucket_acl = private

# R2
[r2]
# s3互換なので`s3`とする
type = s3
provider = Cloudflare
endpoint = https://[CloudflareのアカウントIDをここに].r2.cloudflarestorage.com

アクセスキーの指定について

ここではr2gcsの設定ともに、access_key_idsecret_access_keyを指定していません。

まず、Cloud StorageへのアクセスについてはrcloneをCloud Run上で実行するため、Cloud Runの実行サービスアカウントに適切なロールを付与してあげる(後述)ことでアクセスキーの指定は不要となります。

r2の方は、アクセスキーが必要になりますが、これらをrclone.confにハードコーディングするのは避けたいので、Cloud Runの環境変数として指定するのが良いと思います。具体的には以下の2つの命名で環境変数を設定すればr2に対するアクセスキーとして認識してくれます。

  • RCLONE_CONFIG_R2_SECRET_ACCESS_KEY
  • RCLONE_CONFIG_R2_ACCESS_KEY_ID

(参考: https://rclone.org/docs/#config-file)

この記事では解説を省きますが、Secret Managerにシークレットを登録しておき、Cloud Runのデプロイコマンドを呼び出すときに環境変数として設定するのが安全だと思います。

2. Dockerfileを作成

rclone/rcloneというDockerイメージをベースとすれば、Dockerfileは簡潔に書くことができます。

Dockerfile
FROM rclone/rclone

COPY ./rclone.conf /config/rclone/rclone.conf

# バックアップ目的なので`sync`ではなく`copy`に
CMD ["copy", "r2:バケット名", "gcs:バケット名"]

コマンド/オプションについてはドキュメントを参考に調整してください。

3. Cloud Storageのバケットを用意

※ Google Cloudの設定はTerraformから行ったのでその記述と簡単な解説を載せておきます。

foo.tf
# R2からバックアップするためのCloud Storageバケット
resource "google_storage_bucket" "backup_bucket" {
  name          = "r2-backup"
  location      = "US-CENTRAL1"
  storage_class = "ARCHIVE"
  uniform_bucket_level_access = false # falseでないとエラーになったので
}

最もコストが低くなるようにロケーションはus-central1に、ストレージのクラスはarchiveにしています。

4. Cloud Run jobsの実行用のサービスアカウントを作成

foo.tf
# ジョブ実行用のサービスアカウント
resource "google_service_account" "backup_job_service_account" {
  project      = var.project_id
  account_id   = "rclone-backup-service-account"
  display_name = "バックアップジョブ実行用のサービスアカウント"
}

# サービスアカウントからCloud Storageにアクセスできるようにする
resource "google_project_iam_member" "rclone_backup_service_account_gcs_admin" {
  project = var.project_id
  role    = "roles/storage.admin"
  member  = "serviceAccount:${google_service_account.backup_job_service_account.email}"
}

Cloud Run jobs実行用のサービスアカウントを作成したうえで、Cloud StorageへアクセスできるようにIAMロールを付与しておきます。

5. Cloud Buildからデプロイ

今回はGitHubのmainブランチへpushされたときにCloud Buildにより以下のデプロイが実行されるようにしました。

  • Cloud Run jobs: rcloneを実行するジョブ
  • Cloud Scheduler: ジョブを定期的に呼び出すためのもの

Cloud Buildを使わずにローカルからgcloudコマンドで設定していくのであれば以下のページが参考になるかもしれません。

https://codelabs.developers.google.com/codelabs/cloud-starting-cloudrun-jobs?hl=ja

Cloud Buildのサービスアカウントにロールを付与する

事前にCloud Buildのサービスアカウントに必要なロールを付与しておく

Cloud Buildの実行前に、Cloud Buildのサービスアカウントに必要なロールを付与しておきます。デフォルトのサービスアカウントにロールを追加する場合は以下のようになると思います。

foo.tf
# Cloud BuildのサービスアカウントがArtifact Registryにアクセスできるように
resource "google_project_iam_member" "cloudbuild_artifact_registry_repo_admin" {
  project = var.project_id
  role    = "roles/artifactregistry.repoAdmin"
  member  = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
}

# Cloud BuildのサービスアカウントがCloud Runをデプロイできるように
resource "google_project_iam_member" "cloudbuild_artifact_registry_repo_admin" {
  project = var.project_id
  role    = "roles/run.admin"
  member  = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
}

# Cloud BuildのサービスアカウントがCloud Schedulerを管理できるように
resource "google_project_iam_member" "cloudbuild_scheduler_admin" {
  project = var.project_id
  role    = "roles/cloudscheduler.admin"
  member  = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
}
foo.tf
# Cloud Build: GitHubにプッシュされたときにR2のバックアップジョブ/スケジューラーをデプロイする
resource "google_cloudbuild_trigger" "deploy_backup_job" {
  name        = "deploy-backup-job"
  description = "mainブランチへのプッシュ時にバックアップジョブをデプロイ"

  # GitHubとの連携設定
  github {
    owner = "user_or_org_name"
    name  = "repo_name"
    push {
      branch       = "^main$"
      invert_regex = false
    }
  }

  # 以下のいずれかのファイルに変更があった場合のみトリガーする
  included_files = ["rclone/Dockerfile", "rclone/rclone.conf"]

  substitutions = {
    # さっき作ったサービスアカウントを参照
    "_SERVICE_ACCOUNT" = "${google_service_account.backup_job_service_account.email}"
    # ビルドされたイメージの格納先(Artifact Resistry上のURL)
    "_ARTIFACT_RESISTRY_IMAGE_TAG" = "us-central1-docker.pkg.dev/${var.project_id}/backup-job-images/backup-job:${"$"}{SHORT_SHA}"
  }

  build {
    timeout = "1200s"

    # Dockerfileをビルド
    step {
      id   = "build"
      name = "gcr.io/cloud-builders/docker"
      args = [
        "build",
        "--tag=$_ARTIFACT_RESISTRY_IMAGE_TAG",
        "--file=Dockerfile",
        "."
      ]
    }

    # Artifact Registryにイメージをプッシュ
    step {
      id   = "push"
      name = "gcr.io/cloud-builders/docker"
      args = [
        "push",
        "$_ARTIFACT_RESISTRY_IMAGE_TAG",
      ]
    }

    # Cloud Run jobsにデプロイ
    step {
      id         = "deploy-backup-job"
      name       = "gcr.io/google.com/cloudsdktool/cloud-sdk:slim"
      entrypoint = "gcloud"

      args = [
        "run",
        "jobs",
        "deploy",
        "backup-job",
        "--region=us-central1",
        "--service-account=$_SERVICE_ACCOUNT", 
        "--image=$_ARTIFACT_RESISTRY_IMAGE_TAG",
        "--task-timeout=30m",
      ]
    }

    # Cloud Schedulerでジョブを定期実行する設定
    step {
      id         = "deploy-rclone-backup-scheduler"
      name       = "gcr.io/google.com/cloudsdktool/cloud-sdk:slim"
      entrypoint = "gcloud"

      args = [
        "scheduler",
        "jobs",
        # deployコマンドがないため、createコマンドで作成し、作成後はupdateコマンドしないとエラーになる
	# (そもそもterraformではgcloudで呼び出さない方が良いかもしれない)
        "create",
        "http",
        "backup-job-scheduler",
        "--location=us-central1",
        "--uri=https://us-central1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/$PROJECT_ID/jobs/rclone-backup-job:run",
        "--http-method=POST",
        # 1日に1回、日本時間の午前4時に実行する
        "--schedule=0 4 * * *",
        "--time-zone=Asia/Tokyo",
	# Cloud Run jobsを実行するためのサービスアカウントの指定
        "--oauth-service-account-email=${data.google_project.project.number}-compute@developer.gserviceaccount.com"
      ]
    }
  }
}

これで設定は完了です。コンソールからCloud Schedulerを開き、手動でジョブを実行する等してバックアップが正常にできていることを確認しましょう。

Discussion

melodycluemelodyclue

この記事を元に、自分なりにterraformを使って自動バックアップを構築できました。
ありがとうございます。