0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GCS バケット更新をトリガーに Filestore へ書き込む Cloud Run 構築 (Terraform)

Posted at

GCS バケット更新をトリガーに Filestore へ書き込む Cloud Run 構築 (Terraform)

前回は、Cloud Run Functions で GCS バケットの更新をトリガーにして Filestore にファイルを保存しようとしたが、イメージのユーザ権限周りでうまくいかなかった。
Cloud Run Functions はデフォルトで root 権限がないユーザーで実行されるから、Filestore への書き込みに必要な権限が足りなかった。

今回は、Cloud Run Functions の代わりに、カスタムイメージを使って Cloud Run をデプロイする方法で、この問題を解決する。
具体的には、GCS バケットに新しいファイルがアップロードされたら、それをトリガーにして、Cloud Run で動くコンテナが Filestore にファイルをコピーする、という仕組みを作る。

全体像

まず、今回構築するシステムの全体像はこんな感じ。
image.png

  1. GCS バケット: ログファイルとか、Filestore に保存したいファイルがアップロードされる場所。
  2. Eventarc トリガー: GCS バケットで「ファイルが作成された」イベントを検知して、Cloud Run に伝える。
  3. Cloud Run:
    • カスタムコンテナイメージ: Filestore に書き込むためのツール (例えば mount.nfs コマンド) とか、GCS からファイルをダウンロードするためのツール (gsutil) が入ってる。
    • VPC Direct Egress: Cloud Run が Filestore と同じネットワーク内で通信できるようにする。
    • サービスアカウント: Filestore への書き込み権限と、GCS からの読み取り権限を持ってる。
  4. Filestore: Cloud Run から書き込まれたファイルが保存される場所。

この仕組みを、今回は Terraform を使って構築していく。

Terraform コード解説

今回使う Terraform コードの主要な部分を解説する。

1. Artifact Registry (コンテナイメージの保管場所)

resource "google_artifact_registry_repository" "repository" {
  project       = var.project_id
  location      = var.region
  repository_id = "cloud-run-images"
  format        = "DOCKER"
  description   = "Docker repository for Cloud Run images"
}

ここでは、google_artifact_registry_repository リソースを定義し、Artifact Registry リポジトリを作成している。このリポジトリに、Cloud Run で使うカスタムコンテナイメージを保存する。

2. Docker イメージのビルドとプッシュ (null_resource)

resource "null_resource" "docker_build_and_push" {
  depends_on = [google_artifact_registry_repository.repository]

  provisioner "local-exec" {
    command = <<EOT
      # 認証
       gcloud auth print-access-token | sudo docker login -u oauth2accesstoken --password-stdin https://asia-northeast1-docker.pkg.dev
      # terraformのmain.tfがあるパスからDockerfileがある相対パスを指定
      DOCKER_FILEPATH=""
      # Docker Build 
      sudo docker build -t $IMAGE_NAME $DOCKER_FILEPATH

      # Docker Push
      sudo docker push $IMAGE_NAME
    EOT
  }
}

null_resource は、Terraform のリソースとしては何も作成しないが、provisioner "local-exec" を使ってローカルでコマンドを実行できる。ここでは、以下の処理を実行してる。

  • 認証: Google Cloud の認証情報を設定して、Artifact Registry にアクセスできるようにする。

  • イメージ名の設定: ビルドする Docker イメージの名前を、Artifact Registry のリポジトリのパスを含む形式で設定する。

  • Docker Build: docker build コマンドで、指定したパスにある Dockerfile からコンテナイメージをビルドする。

  • Docker Push: docker push コマンドで、ビルドしたイメージを Artifact Registry にプッシュする。

3. Cloud Run サービス

resource "google_cloud_run_v2_service" "default" {
  project  = var.project_id
  name     = var.cloud_run_service_name
  location = var.region

  template {
    execution_environment = "EXECUTION_ENVIRONMENT_GEN2" # 第 2 世代の実行環境を指定
    containers {
      
      image = "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.repository.repository_id}/${var.cloud_run_image_name}:latest" # 1.で作成したArtifact Registoryおよびpushしたimageを指定
       env {
         name  = "MOUNT_PATH"
         value = "/mnt/nfs" # Filestore のマウントポイント
       }
      volume_mounts {
        name       = "nfs" # ボリューム名
        mount_path = "/mnt/nfs" # マウントパス
      }
    }
    service_account = var.default_compute_service_account # 今回はデフォルトの Compute Engine サービスアカウントを利用する。
    vpc_access { # Direct VPC egress を設定
        network_interfaces {
          network    = var.network_self_link 
          subnetwork = var.subnet_self_link 
        }
    }

    volumes { 
      name = "nfs" # ボリューム名 (上記 volume_mounts.name と一致させる)
      nfs {
        server    = var.filestore_ip_address # Filestore インスタンスの IP アドレス
        path      = var.filestore_share_name    # Filestore の共有名
        read_only = false
      }
    }
  }
}

google_cloud_run_v2_service リソースで、Cloud Run サービスを定義している。各行の説明はコメントで記載。

4. Eventarc トリガー

resource "google_eventarc_trigger" "trigger" {
  project  = var.project_id
  name     = "${var.cloud_run_service_name}-trigger"
  location = var.region
  service_account = var.default_compute_service_account

  matching_criteria {
    attribute = "type"
    value     = "google.cloud.storage.object.v1.finalized"
  }
  matching_criteria {
    attribute = "bucket"
    value     = var.gcs_bucket_name #GCSのバケット更新をトリガーにCloud runを実行する
  }
  destination {
    cloud_run_service {
      service = google_cloud_run_v2_service.default.name
      path    = "/"
      region  = var.region
    }
  }
}

google_eventarc_trigger リソースで、GCS バケットのイベントをトリガーにして Cloud Run サービスを起動するように設定してる。
type: google.cloud.storage.object.v1.finalized (オブジェクトの作成/更新完了) イベントをトリガーにする。

5. IAM 権限

resource "google_cloud_run_service_iam_member" "invoker" {
  location = google_cloud_run_v2_service.default.location
  project  = google_cloud_run_v2_service.default.project
  service  = google_cloud_run_v2_service.default.name
  role     = "roles/run.invoker"
  member   = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
}

ここでは、Cloud Run サービスを実行するサービスアカウント (今回はデフォルトの Compute Engine サービスアカウント) に、必要な権限を付与してる。

roles/run.invoker: Cloud Run サービスを起動する権限 (Eventarc トリガーに必要)。

補足:Dockerfile

FROM python:3.12-slim
# 環境変数を設定
ENV PORT=8080

# 作業ディレクトリを作成
WORKDIR /app

# 依存関係をコピーしてインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Cloud Functions フレームワークをインストール
RUN pip install functions-framework

# アプリケーションコードをコピー
COPY main.py .

# Root ユーザーで実行
USER root

# Cloud Functions フレームワークを実行 --targetでpythonのエンドポイントととなる関数を指定。
CMD ["functions-framework", "--target=main", "--port=8080"]
  • root ユーザーでの実行について
    この Dockerfile では、USER root でコンテナ内のプロセスを root ユーザーで実行するように設定している。これは、Filestore をマウントするために root 権限が必要という制約があるため。

main.pyの例 

下記プログラムは、GCSバケットに更新もしくは作成されたファイルをFilestore上にコピーするもの。今回の記事で詳細な解説は割愛。

import subprocess
import os
import functions_framework
from google.cloud import storage

# 環境変数から設定を取得
MOUNT_PATH = os.getenv("MOUNT_PATH")

# GCS クライアント
storage_client = storage.Client()

@functions_framework.cloud_event
def main(cloud_event):
    """GCS のファイル変更イベントを処理し、Filestore にコピー"""
    data = cloud_event.data

    file_name = data.get("name")
    source_bucket = data.get("bucket")

    file_path = f"gs://{source_bucket}/{file_name}"
    print(f"Processing file: {file_path}")

    if not source_bucket or not file_name:
        raise ValueError("Missing bucket or file name in event data")

    destination_path = os.path.join(MOUNT_PATH, file_name)
    print(f"Mounting Filestore: {destination_path}...")

    # ディレクトリが存在しない場合は作成
    filestore_dir = os.path.dirname(destination_path)
    if not os.path.exists(filestore_dir):
        try:
            os.makedirs(filestore_dir, exist_ok=True)
            print(f"Created directory: {filestore_dir}")
        except OSError as e:
            print(f"Error creating directory {filestore_dir}: {e}")
            return

    try:
        bucket = storage_client.bucket(source_bucket)
        blob = bucket.blob(file_name)

        # Cloud Filestore にコピー
        blob.download_to_filename(destination_path)

        print(f"Copied {file_name} to {destination_path}")
    except Exception as e:
        print(f"Error copying file: {str(e)}")
        raise
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?