CUBE SUGAR CONTAINER

技術系のこと書きます。

GCP: Cloud Functions で Cloud Storage にオブジェクトを保存する

今回は Google Cloud の Cloud Functions で実行した処理の中で Cloud Storage にオブジェクトを保存する方法について。 Cloud Functions で実行した何らかの処理の成果物を保存する先として Cloud Storage を使うイメージになる。

操作は、基本的に Google Cloud SDK の gcloud コマンドから実施する。 なお、操作の対象となる Google Cloud API が有効化されていない場合には、別途有効化するかを確認する表示が出ることもある。

使った環境は次のとおり。

$ sw_vers              
ProductName:        macOS
ProductVersion:     13.5
BuildVersion:       22G74
$ gcloud version                            
Google Cloud SDK 442.0.0
bq 2.0.96
core 2023.08.04
gcloud-crc32c 1.0.0
gsutil 5.25

下準備

まずは Google Cloud SDK をインストールしておく。

$ brew install google-cloud-sdk

そして、利用したいアカウントにログインする。

$ gcloud auth login

必要に応じて、デフォルトで操作する対象のプロジェクトを指定する。

$ gcloud config set project <project-id>

Cloud Pub/Sub のトピックを作成する

Cloud Functions のトリガーとして Cloud Pub/Sub を使いたい。 そこで、まずはトピックを作成しておく。 ここでは example-pubsub-topic という名前にした。

$ gcloud pubsub topics create example-pubsub-topic

Cloud Storage のバケットを作成する

続いて Cloud Storage にオブジェクトを保存する先となるバケットを作成する。 バケットに付ける名前は、Cloud Storage のシステム全体でユニークな必要がある。

今回はユーザ名や日付などから、ユニークになりやすい組み合わせでサンプルとなるバケットの名前を作った。 別にこれでなくとも誰かが使っているバケットの名前と衝突しなければ何でも構わない。

$ UNIQUE_BUCKET_NAME=$(echo $(whoami)-example-bucket-$(date +%Y%m%d)) 
$ echo $UNIQUE_BUCKET_NAME  
amedama-example-bucket-20230806

バケットは gcloud storage buckets create コマンドで作成する。 基本的に Cloud Storage の操作をするときは、対象を URI で指定する。 その際、スキーマとして先頭に gs:// をつける必要がある。 また、--location オプションを指定しない場合はマルチリージョンで us にバケットが作成される。

$ gcloud storage buckets create \
    gs://${UNIQUE_BUCKET_NAME} \
    --location=asia

Cloud Functions を作成する

続いては Cloud Storage のバケットにオブジェクトを作成する Cloud Functions を作成する。

まずは必要なファイルを入れるディレクトリを用意する。 ここでは helloworld という名前で作成した。

$ mkdir helloworld

続いて Cloud Functions の本体となる処理を Python のモジュールとして作成する。 ポイントは google.cloud.storage パッケージを使って Cloud Storage にオブジェクトを作成しているところ。 バケットの名前は BUCKET_NAME という環境変数から取得するように作ってある。 なお、環境変数が指定されていない場合のエラーハンドリングは省略してある。

$ cat << 'EOF' > helloworld/main.py
import logging
import os
from datetime import datetime

from google.cloud import logging as gcloud_logging
from google.cloud import storage as gcloud_storage


gcloud_logging.Client().setup_logging()
LOG = logging.getLogger(__name__)


def main(event, context):
    gcs_client = gcloud_storage.Client()
    bucket_name = os.environ.get("BUCKET_NAME")
    bucket = gcs_client.bucket(bucket_name)

    now = datetime.now()
    file_name = now.strftime("%Y-%m-%d/%H:%M:%S.log")
    blob = bucket.blob(file_name)

    timestamp = now.strftime("%Y%m%d%H%M%S")
    file_data = f"executed: {timestamp}\n"
    blob.upload_from_string(file_data)

    LOG.info(
        "successfully saved to %s in %s",
        file_name,
        bucket_name
    )

EOF

オブジェクトには Cloud Functions が実行された時刻をタイムスタンプとして書き込んでいる。

動作に必要なパッケージは requirements.txt に記述する。

$ cat << 'EOF' > helloworld/requirements.txt
google-cloud-logging
google-cloud-storage
EOF

上記で次のようなファイル構成が作られる。

$ tree helloworld 
helloworld
├── main.py
└── requirements.txt

1 directory, 2 files

Cloud Functions をデプロイする

先ほどのディレクトリを元に Cloud Functions をデプロイするには gcloud functions deploy コマンドを使う。 このとき --set-env-vars オプションを使って環境変数を設定できる。 バケットの名前として BUCKET_NAME を忘れずに指定しよう。 また --trigger-topic を指定することで、指定した Cloud Pub/Sub のトピックにメッセージが到着した際に処理がトリガーされる。

$ gcloud functions deploy helloworld \
  --gen2 \
  --no-allow-unauthenticated \
  --runtime python310 \
  --memory 128Mi \
  --region asia-northeast1 \
  --trigger-topic example-pubsub-topic \
  --source helloworld \
  --entry-point main \
  --set-env-vars BUCKET_NAME=${UNIQUE_BUCKET_NAME}

うまくいけば gcloud functions list にデプロイした Cloud Functions が表示される。

$ gcloud functions list
NAME        STATE   TRIGGER                      REGION      ENVIRONMENT
helloworld  ACTIVE  topic: example-pubsub-topic  asia-northeast1  2nd gen

また gcloud functions logs read コマンドでエラーなどが出ていないかも確認しておこう。

$ gcloud functions logs read helloworld --region asia-northeast1
LEVEL  NAME        TIME_UTC                 LOG
I      helloworld  2023-08-06 14:56:35.116  Default STARTUP TCP probe succeeded after 1 attempt for container "helloworld-1" on port 8080.

正常にデプロイできたら Cloud Pub/Sub にメッセージを送信して Cloud Functions を起動しよう。

$ gcloud pubsub topics publish projects/$(gcloud config get-value project)/topics/example-pubsub-topic --message "-"

少ししたら、また gcloud functions logs read コマンドを実行してログを確認しよう。 うまくいけば次のようなログが出力される。

$ gcloud functions logs read helloworld --region asia-northeast1                                                         
LEVEL  NAME        TIME_UTC                 LOG
I      helloworld  2023-08-06 14:59:00.192  successfully saved to 2023-08-06/14:58:57.log in amedama-example-bucket-20230806
I      helloworld  2023-08-06 14:59:00.191
I      helloworld  2023-08-06 14:58:57.341
I      helloworld  2023-08-06 14:56:35.116  Default STARTUP TCP probe succeeded after 1 attempt for container "helloworld-1" on port 8080.

Cloud Storage の内容を確認する

Cloud Functions から作成された Cloud Storage のオブジェクトを確認しよう。

gcloud storage ls -r コマンドを使うことでバケットの中身を再帰的に表示できる。

$ gcloud storage ls -r gs://${UNIQUE_BUCKET_NAME}
gs://amedama-example-bucket-20230806/:

gs://amedama-example-bucket-20230806/2023-08-06/:
gs://amedama-example-bucket-20230806/2023-08-06/14:58:57.log

オブジェクトの内容は gcloud storage cat コマンドで確認できる。

$ gcloud storage cat gs://${UNIQUE_BUCKET_NAME}/2023-08-06/14:58:57.log 
executed: 20230806145857

どうやら意図した通りに作成されているようだ。

後片付けする

動作の確認が終わったら後片付けをしよう。

まずは Cloud Storage のオブジェクトとバケットを削除する。 gcloud storage rm -r を使うとバケットの中身とバケット自体が再帰的に削除できる。

$ gcloud storage rm -r gs://${UNIQUE_BUCKET_NAME}

続いて gcloud functions delete コマンドを使って Cloud Functions の関数を削除する。

$ gcloud functions delete helloworld --region asia-northeast1

そして gcloud pubsub topics delete コマンドを使って Cloud Pub/Sub のトピックを削除する。

$ gcloud pubsub topics delete example-pubsub-topic

また、Cloud Functions ではデプロイしたファイル群などを以下のような名前で Cloud Storage に保存している。

gcf-v2-sources-<project-id>-<region>
gcf-v2-uploads-<project-id>-<region>

これらも気になる場合には先ほどと同じ要領で削除しておこう。

$ gcloud storage buckets list | grep name
$ gcloud storage rm -r gs://<bucket-name> 

まとめ

今回は Cloud Functions を使って Cloud Storage にオブジェクトを保存する方法を確認した。