GCS バケット更新をトリガーに Filestore へ書き込む Cloud Run 構築 (Terraform)
前回は、Cloud Run Functions で GCS バケットの更新をトリガーにして Filestore にファイルを保存しようとしたが、イメージのユーザ権限周りでうまくいかなかった。
Cloud Run Functions はデフォルトで root 権限がないユーザーで実行されるから、Filestore への書き込みに必要な権限が足りなかった。
今回は、Cloud Run Functions の代わりに、カスタムイメージを使って Cloud Run をデプロイする方法で、この問題を解決する。
具体的には、GCS バケットに新しいファイルがアップロードされたら、それをトリガーにして、Cloud Run で動くコンテナが Filestore にファイルをコピーする、という仕組みを作る。
全体像
- GCS バケット: ログファイルとか、Filestore に保存したいファイルがアップロードされる場所。
- Eventarc トリガー: GCS バケットで「ファイルが作成された」イベントを検知して、Cloud Run に伝える。
-
Cloud Run:
-
カスタムコンテナイメージ: Filestore に書き込むためのツール (例えば
mount.nfs
コマンド) とか、GCS からファイルをダウンロードするためのツール (gsutil) が入ってる。 - VPC Direct Egress: Cloud Run が Filestore と同じネットワーク内で通信できるようにする。
- サービスアカウント: Filestore への書き込み権限と、GCS からの読み取り権限を持ってる。
-
カスタムコンテナイメージ: Filestore に書き込むためのツール (例えば
- 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