🫐

RDBに保存された画像データを安全にGCSへ移行する

2024/10/18に公開

こんにちは。今回はRDBに保存されていたbase64形式の画像データをGCSへ移行した流れをご紹介します。私自身はWebフロントを主軸にエンジニアをしており、最近はチームの状況もあってバックエンドから幅広く対応をする事が増えました。

はじめに

私の開発するサービスはGCPを基本に設計をしておりRDBにCloud SQL、Object StorageにGCSを使用しています。また、GKEを使ったKubernetesによるコンテナ管理をしています。詳細なテーブル定義は省略しますが、従来の実装ではMessage Tableの中にbase64形式の画像データが保存されていました。

ですがサイズの大きいbase64の画像データをそのままDBで管理すると、DBへの負荷が増えパフォーマンス低下の懸念があります。

https://maximorlov.com/why-storing-files-database-bad-practice/

そこでDBに保存されていたbase64形式の画像データをKubernetesのJobを使用してGCSへ移行した流れをご紹介します。また、今回はRDBからGCSへの画像データ移行を主な目的とするためアプリケーションの詳細な実装は割愛します。

https://kubernetes.io/ja/docs/concepts/workloads/controllers/job/

全体像

全体像としてはこのようなイメージです。GitHub ActionsをトリガーにしてKubernetesのJobをApplyし、Privateなネットワークの中でRDBから画像データを取得してGCSへアップロードします。

実行するスクリプトを用意する

私たちのチームはBackendにもTypeScriptを採用しておりORMにはDrizzleを使用しています。まずはGCSへ移行するためのスクリプトを用意します。

https://orm.drizzle.team/

用意したスクリプトは非常に単純なもので、DBからbase64の画像データを取得してGCSへ逐次アップロードするといったものです。コードの詳細は省略して掲載します。

task.ts
const migrateDBToStorage = async () => {
  // ...省略

  const messages = await db.select().from(MessageTable).execute()
  if (messages.length > 0) {
    for (const message of messages) {
      const base64EncodedImageString = message.base64ImageData.replace(
        /^data:image\/\w+;base64,/,
        ''
      )
      const buffer = Buffer.from(base64EncodedImageString, 'base64')
      const fileType = await fileTypeFromBuffer(buffer)

      if (!fileType) {
        logger.error({
          message: `Failed to get file type: ${message.id}`,
        })
        continue
      }

      const result = await storageClient.uploadBlobsInBatch([
        {
          path: `messages/${message.id}.${fileType.ext}`,
          content: buffer,
        },
      ])
      if (!result.isSuccess) {
        logger.error({
          message: 'Failed to upload image',
          stack: result.getError().stack,
        })
      }
    }
  }

  // ...省略
}

Kubernetesの設定

Kubernetesの構成はkustomizeを使用して各環境ごとに設定値を調整できるようにしています。これによりdev, stg, prodの各環境に対して共通のベース設定を使用しつつ、環境ごとの差分だけを管理することができます。

https://github.com/kubernetes-sigs/kustomize

Kubernetesの構成
.
├── base
│   ├── external-secret.yaml
│   ├── job.yaml
│   └── kustomization.yaml
└── overlays
    ├── dev
    │   ├── config.properties
    │   ├── external-secret.yaml
    │   ├── job.yaml
    │   ├── kustomization.yaml
    │   └── serviceaccount.yaml
    ├── prod
    └── stage

環境変数などはconfigMapで管理をしていますが、SecretはExternal Secret Operatorを使用してSecret ManagerのSecretを読み取り、KubernetesのSecretオブジェクトに保存するようにしています。

https://external-secrets.io/main/

そして先ほど用意したスクリプトを実行させる設定をJobに定義し、Artifact RegistryへPushしたコンテナのimage tagを指定します。

job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: db-to-storage-migration
spec:
  template:
    spec:
      containers:
      - name: db-to-storage-migration
        image: { Artifact Registryに登録されたimage }
        command:
          - "sh"
          - "-c"
          - "bun run /app/index.js migrate-db-to-storage"

Service Accountの管理

KubernetesのPodからCloud SQLやGCSへアクセスをするためには適切なService Accountの設定が必要になります。

https://cloud.google.com/iam/docs/service-account-overview?hl=ja

今回はそれぞれに対して最低限以下の権限を与える必要がありました。

これらのアクセス権を定義したGoogle Service Accountを用意し、Workload IdentityによってKubernetes側のService Accountと紐づけて権限を借用するようにします。

serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: db-to-storage-migration-sa
  annotations:
    iam.gke.io/gcp-service-account: 用意したService Account

GitHub ActionsでTriggerする

最後に設定したKubernetesのJobをGKE上にApplyします。その際にGitHub ActionsからApplyをするようにしました。その理由は以下の通りです。

  • 手元からApplyすることによるオペレーションミスを防ぐ
  • 既にGitHub ActionsからWorkload Identityを使用したGKEへのアクセス制御がされていたため追加の権限管理が不要

また、Workload IdentityとOIDCの認証を組み合わせることによりサービスアカウントキーを使用せずに権限を借用できます。

db-to-storage-migration-dev.yaml
name: Database to Storage Migration

on:
  workflow_dispatch:

env:
  WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
  GHA_SERVICE_ACCOUNT: ${{ secrets.GHA_SERVICE_ACCOUNT }}
  REGION: { region名 }
  GCP_PROJECT: { project名 }
  GKE_CLUSTER: { cluster名 }

jobs:
  db-to-storage-migration:
    name: Database to Storage Migration
    runs-on: ubuntu-latest
    permissions:
      contents: 'read'
      id-token: 'write'
    environment: Development
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.ref_name || 'development' }}
      - uses: 'google-github-actions/auth@v2'
        with:
          WORKLOAD_IDENTITY_PROVIDER: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
          SERVICE_ACCOUNT: ${{ env.GHA_SERVICE_ACCOUNT }}
        
      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v2'
        with:
          version: '>= 363.0.0'

      - name: Database to Storage Migration
        run: |
          gcloud components install gke-gcloud-auth-plugin
          gcloud container clusters get-credentials $GKE_CLUSTER --region $REGION --project $GCP_PROJECT
          kubectl apply -k deployment/k8s/manifests/db-to-storage-migration/overlays/dev

まとめ

最後に用意したGitHub Actionsを各環境ごとに走らせれば完了です。

この対応によってRDBで管理されていた画像データをGCSへ移行することができました。あとはアプリケーションコードを修正してGCSに対して画像を追加・参照するように変更します。

そして今回は一時的な対応だったため、用意したKubernetesのリソースやMigrationスクリプトなどは必要がなくなったタイミングで削除しました。

慣れない対応だったため手探りでの実装となりましたが、非常に学びがあるタスクとなりました。

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion