こんにちは!
KDDIアイレットの取り組みとして、8月17日から8月28日まで開催中の「Google Cloud 夏休み自由研究ブログリレー」、本日は5日目の投稿です。
今回は「Google Cloudのイベント駆動を体験する」をテーマに、実際に検証してみた内容をご紹介します!

これまでの記事はこちらです。

記事一覧はこちら

なぜこのテーマを選んだのか

Google Cloudでの開発に初めて取り組むにあたり、以下の2つを目的として本テーマに挑戦しました。

  • Google Cloudの管理コンソール操作に慣れること
  • クラウドの強みである「サーバーレス」「イベント駆動」の仕組みを体感すること

今回は定番の「画像をアップロードしたら自動でサムネイル(元画像を軽量化し、読み込みを速くした画像)を生成する」仕組みを通じて、イベント駆動の便利さを学んでいきます!

今回作った仕組み

使用したサービス

サービス名 説明 本検証での役割
Cloud Storage 無制限で高耐久なオブジェクトストレージ 元画像および生成されたサムネイル画像の保存
Cloud Run Functions サーバー管理不要でイベントに応じて処理を実行するFaaS 画像アップロードを検知し、サムネイル生成処理を実行

処理の流れ

  1. ユーザーが「元画像用バケット」に画像をアップロード
  2. アップロードを検知してCloud Run Functionsが起動
  3. Cloud Run Functions内で画像をリサイズ
  4. 生成された画像を「サムネイル用バケット」へ保存

実装手順

1. バケットの作成

元画像をアップロードする用のバケット(thumb-gen-source)と、生成されたサムネイル画像を保存する用のバケット(thumb-gen-resized)の2つを作成します。
[Cloud Storage] > [バケット] へアクセスし、[作成] をクリックします。

設定値は以下のとおりです。

  • 使ってみる
    • バケット名:グローバルに一意なもの(世界中どの名前とも被らない)
    • ラベル:そのまま
  • データの保存場所の選択
    • ロケーション:us-west1(※無料枠を活用するため米国リージョンを選択)
    • ロケーションタイプ:Region
  • データの保存方法を選択する
    • デフォルトのストレージクラス:Standard
    • 階層名前空間:無効
    • Rapid Cache:無効
  • オブジェクトへのアクセスを制御する方法を選択する
    • 公開アクセスの防止:オン
    • アクセス制御:均一性
  • オブジェクトデータを保護する方法を選択する
    • 削除(復元可能ポリシー):デフォルト
    • オブジェクトのバージョニング:無効
    • バケット保持ポリシー:無効
    • オブジェクト保持:無効
    • 暗号化のタイプ:Googleが管理
    • 許可された暗号化ルール:Googleが管理Cloud KMS


2. サービスアカウント・権限の設定

2.1 Cloud Storage サービスアカウントへの権限付与

[Cloud Storage] > [設定] に移動し、[Cloud Storageサービスアカウント] の値を確認します。
[IAMと管理 > IAM] へ移動し、[Google 提供のロール付与を含める] にチェックを入れます。Cloud Storageサービスアカウントを探し、右にある修正マークをクリックして以下のロールを付与します。

  • Pub/Subパブリッシャー

2.2 Pub/Sub サービスアカウントへの権限付与

同じく [IAMと管理 > IAM] で [Google 提供のロール付与を含める] にチェックを入れます。
[名前] が Cloud Pub/Sub Service Account の行を探し、右にある修正マークをクリックして以下のロールを付与します。

  • サービスアカウントトークン作成者

2.3 Cloud Run Functions 用のサービスアカウント作成

[IAMと管理 > サービスアカウント] で [サービスアカウントを作成] をクリックします。
設定値は以下のとおりです。

  • サービスアカウント名:Thumbnail Generator Service Account
  • サービスアカウントID:sa-thumb-generator

作成したサービスアカウントを選択し、[権限] タブにアクセスします。
[アクセスを管理する] から、以下3つのロールを付与します。

  • Eventarcイベント受信者
  • Storageオブジェクト閲覧者
  • Storageオブジェクト作成者

3. Cloud Run Functionsのデプロイ

3.1 サービスの作成

[Cloud Run > サービス] へアクセスし、[関数を作成] をクリックします。
設定値は以下のとおりです。

  • 名前:generate-thumbnail
  • リージョン:us-west1(オレゴン)
  • ランタイム:Python 3.12
  • トリガー:あとで設定
  • 認証:パブリックアクセスを許可(検証のため)
  • 課金:リクエストベース
  • サービスのスケーリング:自動スケーリング(最小値や最大値はデフォルトのまま)
  • Ingress:すべて
  • セキュリティ > サービスアカウント:Thumbnail Generator Service Account​

3.2 トリガーの作成

作成したサービスを選択し、[トリガー] タブにアクセスします。
[トリガーを追加] をクリックし、[Cloud Storageトリガー] を選択します。
設定値は以下のとおりです。

  • 名前:trigger-generate-thumbnail
  • トリガーのタイプ:Googleのリソース
  • イベントプロバイダ:Cloud Storage
  • イベントデータのコンテンツタイプ:google.cloud.storage.object.v1.finalized
  • バケット:thumb-gen-source
  • リージョン:us-west1(オレゴン)
  • サービスアカウント:Cloud Run Functions用に作成したサービスアカウント
  • サービスURLパス:/

3.3 ソースコードの記述

作成したサービスを選択し、[ソース] タブにアクセスします。
[ソースを編集] をクリックし、各ファイルを以下のように修正します。

ソースコード(main.py)を展開する
import io
import os
import functions_framework
from google.cloud import storage
from PIL import Image, ImageOps

# 出力バケット名(環境変数から取得)
DEST_BUCKET_NAME = os.environ.get("DEST_BUCKET_NAME", "your-destination-bucket-name")

# サムネイル画像サイズの最大(幅:200px 高さ:200px)
THUMB_SIZE = (200, 200)

storage_client = storage.Client()

@functions_framework.cloud_event
def generate_thumbnail(cloud_event):
data = cloud_event.data

bucket_name = data["bucket"]
file_path = data["name"]
content_type = data.get("contentType", "")

# 1. 画像ファイル以外はスキップ
if not content_type.startswith("image/"):
print(f"画像ファイル以外はスキップします: {file_path}")
return

file_name = os.path.basename(file_path)

# 2. 既にサムネイル化されたファイルの処理をスキップ(誤って同一バケットに保存した場合の安全対策)
if file_name.startswith("thumb_"):
print(f"既に処理されたファイルはスキップします: {file_path}")
return

print(f"処理中...ファイル: {file_path} バケット: {bucket_name}")

src_bucket = storage_client.bucket(bucket_name)
src_blob = src_bucket.blob(file_path)

# 3. 元画像をメモリ上にダウンロード
image_bytes = src_blob.download_as_bytes()

try:
# Pillowで画像を開く
with Image.open(io.BytesIO(image_bytes)) as img:
# 元のフォーマットを維持(なければJPEG扱い)
original_format = img.format if img.format else "JPEG"

# EXIF回転情報の補正(スマホで撮影した画像の向き補正)
img = ImageOps.exif_transpose(img) if hasattr(ImageOps, "exif_transpose") else img

# 4. アスペクト比を維持してリサイズ
img.thumbnail(THUMB_SIZE)

# 処理後の画像をメモリ上のBytesIOへ保存
output_buffer = io.BytesIO()

# RGBAモードのPNG等をJPEGに変換する場合はRGB変換が必要
if original_format.upper() in ("JPEG", "JPG") and img.mode in ("RGBA", "P"):
img = img.convert("RGB")

img.save(output_buffer, format=original_format)
output_buffer.seek(0)

# 5. 保存先パスを作成して別バケットへアップロード
dest_bucket = storage_client.bucket(DEST_BUCKET_NAME)
dir_name = os.path.dirname(file_path)
dest_file_path = (
os.path.join(dir_name, f"thumb_{file_name}")
if dir_name
else f"thumb_{file_name}"
)

dest_blob = dest_bucket.blob(dest_file_path)
dest_blob.upload_from_file(output_buffer, content_type=content_type)

print(
f"サムネイルの作成に成功!!!: gs://{DEST_BUCKET_NAME}/{dest_file_path}"
)

except Exception as e:
print(f"エラー {file_path}: {e}")
raise e
ソースコード(requirements.txt)を展開する
functions-framework==3.*
google-cloud-storage==2.*
Pillow==10.*


4. 動作確認

  1. thumb-gen-source バケットにテスト画像をアップロードします。
  2. 処理完了後、thumb-gen-resized バケットにサムネイル画像が生成されているか確認できました!

つまずいたポイントと解決策

今回の検証で、個人的に特に勉強になった「つまずいたポイント」が2つありました。

1. サービスアカウントの権限不足によるエラー問題

Cloud Run Functionsのトリガーを作成しようとしたところ、権限不足によりエラーになりました。

原因

Cloud Storage トリガーの Cloud Run Functions(第2世代)は、内部で Pub/Sub を介してイベントを通知する仕組みになっています。
そのため、Cloud Storage サービスアカウントに対して「Pub/Sub への書き込み」、Pub/Sub サービスアカウントに対して「認証用トークンの作成」を行うための権限が不足していると、トリガー作成時やデプロイ時にエラーが発生します。

解決策

以下の手順でCloud Storageのサービスアカウントに必要なロールを付与することで無事に解決しました!

  1. [Cloud Storage > 設定] を開き、画面に表示されているCloud Storageサービスアカウントを確認。
  2. [IAMと管理 > IAM] に移動し、[Google 提供のロール付与を含める] にチェックを入れ、さきほど確認したCloud Storageサービスアカウントに対して、以下のロールを付与。
    • Pub/Sub パブリッシャー
  3. 同じく [IAMと管理 > IAM] で [Google 提供のロール付与を含める] にチェックを入れ、[名前] が Cloud Pub/Sub Service Account のサービスアカウントに対して、以下のロールを付与。
    • サービスアカウントトークン作成者

2. Python と Pillow(PIL)の互換性問題

画像リサイズ処理のために定番ライブラリである「Pillow」を使おうとしたのですが、ビルド時または実行時にエラーが発生してしまいました。

原因

検証時に選択していたバージョンは、Pillowなどの一部外部ライブラリが対応していなかった(またはバイナリパッケージが提供されていなかった)ことが原因でした。

解決策

ランタイムの Python バージョンを安定版である 「Python 3.12」 に下げたところ無事にデプロイが完了し、処理が動くようになりました!

まとめ

今回の検証を通じて、当初の目的であった以下2つを達成することができました。

  • 管理コンソール操作に慣れることができた
    IAMの権限設定やCloud Storage、Cloud Run Functionsの画面を行き来することで、コンソールのどこに何の項目があるかをスムーズに把握できるようになりました。
  • 「サーバーレス」「イベント駆動」の仕組みを体感できた
    「ファイルをアップロードする」というユーザーのアクションをトリガーにして、自動で関数が起動し、処理を完結させて別のバケットへ保存する、という一連の流れを自分の手で構築できたのは大きな糧となりました。

今後はこの経験を糧に、実務でもGoogle Cloudを活用して、クラウドの強みを最大限引き出したシステム構築に貢献できるよう努力していきます。

最後までお読みいただきありがとうございました!