G-gen の佐々木です。当記事では Cloud Run で Cloud Storage FUSE を使用して、オブジェクトストレージである Cloud Storage のバケットをコンテナ内のディレクトリにマウントしてみます。
前提知識
Cloud Run とは
Cloud Run は Google Cloud のマネージドなコンテナ実行環境を使用してアプリケーションを実行することができるサーバレス コンテナコンピューティング サービスです。
HTTP リクエストを処理のトリガーとする Cloud Run services と、手動もしくはスケジュール、ワークフローをトリガーとする Cloud Run jobs の 2種類があります。
それぞれ以下の記事で詳細を解説しています。
Cloud Storage(GCS)とは
Cloud Storage は容量無制限のオブジェクトストレージサービスであり、99.999999999% (イレブンナイン) の堅牢性を持つオブジェクトストレージを安価に利用することができます。
Cloud Storage の詳細は以下の記事で解説しています。
Cloud Storage FUSE について
Cloud Storage FUSE とは
Cloud Storage FUSE は Cloud Storage バケットをローカルファイルシステムとしてマウントしてアクセスできるようにする FUSE (Filesystem in Userspace) アダプタであり、Google がサポートするオープンソースのプロダクトです。これを使用することで、アプリケーションから通常のファイルシステム同様にバケット内のオブジェクトを読み書きすることができるようになります。
Cloud Storage FUSE は、Cloud Storage バケットに格納されたオブジェクトの名前をファイルとディレクトリに変換します。オブジェクトの名前に含まれているスラッシュ(/)をディレクトリの区切りとすることで、仮想的なディレクトリ構成を使用してアプリケーションからファイルにアクセスできるようにします(以下の例を参照)。
これにより、アプリケーションは Cloud Storage の無制限のストレージをファイルシステムと同様に利用することができます。
# Cloud Storage バケットの状態
bucket
├ aaa/aaa-0.txt
├ aaa/aaa-1.txt
├ bbb/bbb-0.txt
└ bbb/bbb-1.txt
# アプリケーションからの見え方
bucket/
├ aaa/
│ ├ aaa-0.txt
│ └ aaa-1.txt
└ bbb/
├ bbb-0.txt
└ bbb-1.txt
- 参考 : Cloud Storage FUSE
制限事項
Cloud Storage FUSE によってマウントされるファイルシステムの制限事項をいくつか記載します。 その他の制限事項については ドキュメント をご一読ください。
- Cloud Storage FUSE は POSIX に準拠していません。POSIX 準拠のファイルシステムとしては、Google Cloud からは Filestore が提供されています。
- Cloud Storage FUSE が Cloud Storage にファイルをアップロードする際、オブジェクトのメタデータ を設定することはできません。メタデータを設定する必要がある場合は、コンソールや CLI、API を使用して Cloud Stroage にオブジェクトを直接アップロードします。
- Cloud Storage FUSE を使用して同じファイルに対して同時に複数の書き込みを行った場合、最後の書き込みだけが有効となり、それ以前の書き込みはすべて失われます(同時実行制御ができない)。
- Cloud Storage FUSE ではハードリンクを使用することができません。
- マウントされたファイルシステムへのアクセスには Cloud Storage バケットのアクセス権(
roles/storage.objectAdmin
など)が必要となります。
料金
Cloud Storage FUSE は無料で利用できますが、これを利用することによって Cloud Storage バケットに保存されるオブジェクトやネットワーク I/O は、通常の Cloud Storage の利用料として料金が発生します。
ネイティブ機能によるマウント
アップデートにより、FUSE の設定を意識することなく Cloud Storage バケットをマウントできるようになったため、基本的にはこちらを使用することを推奨します。
ネイティブ機能によるマウント方法については以下の記事で解説しています。
考慮事項
Cloud Run 実行環境
Cloud Storage FUSE は Cloud Run services、Cloud Run jobs の両方で使用できます。
Cloud Run services では第1世代と第2世代の実行環境があり(Cloud Run jobs は第2世代のみ)、ネットワークファイルシステムがサポートされるのは第2世代のみとなっています。
そのため、Cloud Storage FUSE を使用する場合は第2世代の Cloud Run を使用します。
マルチプロセス化による PID 1 問題への対処
通常、コンテナでは単一のプロセス(アプリケーション)が実行されますが、Cloud Storage FUSE を使用する場合、アプリケーションのほかにファイルシステムへのマウントプロセスが実行されます。このようなマルチプロセスのコンテナでは、コンテナのエントリポイントとして実行されるアプリケーションに PID 1が割り当てられることで、その他のプロセス(ここではファイルシステムへのマウント)の終了処理が適切に行われない問題(PID 1問題)が発生してしまいます。
この問題を適切に処理するために、コンテナのエントリポイントとして複数のプロセスを管理するプロセスマネージャーを実行し、そこからアプリケーションやファイルシステムのマウントプロセスを実行します。
このブログでは公式ドキュメントの チュートリアル に従い、プロセスマネージャーとして Tini を使用するコンテナを作成していきます。
PID 1 問題については以下の記事でわかりやすく解説されています。
- 参考 : コンテナ内で大量のゾンビプロセスが発生していた話(※外部ブログ)
Cloud Run で Cloud Storage FUSE を使用してみる
基本的には 公式ドキュメントのチュートリアル に従って進めていきます。チュートリアルには Node.js、Python、Java のアプリケーションの例がありますが、当ブログでは Go を使用していきます。
構成図
当記事では、Cloud Run にデプロイするコンテナに /mnt/gcs/
ディレクトリを作成し、Cloud Storage FUSE を使用して Cloud Storage バケットをマウントします。
Cloud Storage バケットの作成
ファイルシステムとして使用する Cloud Storage バケットを作成します。
バケット名はグローバルに一意の値を指定する必要があります。
# Cloud Storage バケットを作成する $ gcloud storage buckets create gs://mybucket --location asia-northeast1
Artifact Registry リポジトリの作成
Cloud Run にデプロイするコンテナイメージを格納するための Artifact Registry リポジトリを作成します。
# Artifact Registry リポジトリを作成する $ gcloud artifacts repositories create myrepo \ --repository-format=docker \ --location=asia-northeast1
使用するコード
main.go
Cloud Storage バケットをマウントしたディレクトリからテキストファイルを読み取り、ブラウザに表示するだけの簡単な Web アプリケーションを作成します。
package main import ( "fmt" "log" "net/http" "os" ) func readGCS() (string, error) { // Cloud Storage FUSE をマウントしたディレクトリからテキストファイルを読み取る f, err := os.Open("/mnt/gcs/" + "test.txt") if err != nil { return "", err } defer f.Close() text := make([]byte, 1024) count, err := f.Read(text) if err != nil { return "", err } return string(text[:count]), nil } func readGCSHandler(w http.ResponseWriter, r *http.Request) { text, err := readGCS() if err != nil { log.Print(err) http.Error(w, err.Error(), 500) } else { // 読み取ったテキストファイルの中身を表示 fmt.Fprintf(w, "%s", text) } } func main() { log.Print("starting server...") http.HandleFunc("/", readGCSHandler) port := os.Getenv("PORT") if port == "" { port = "8080" log.Printf("defaulting to port %s", port) } log.Printf("listening on port %s", port) if err := http.ListenAndServe(":"+port, nil); err != nil { log.Fatal(err) } }
gcsfuse_run.sh
Cloud Storage FUSE を使用してコンテナインスタンスに Cloud Storage バケットをマウントしたあと、アプリケーションを実行するスクリプトです。
マウント対象の /mnt/gcs/
ディレクトリもここで作成しています。
#!/usr/bin/env bash set -eo pipefail # コンテナインスタンスに Cloud Storage バケットをマウントするディレクトリを作成 mkdir -p $MNT_DIR # Cloud Storage FUSE を使用してバケットをマウント echo "Mounting GCS Fuse." gcsfuse --debug_gcs --debug_fuse $BUCKET $MNT_DIR echo "Mounting completed." # Web アプリケーションの実行 /app/main
Dockerfile
Dockerfile ではアプリケーションの資材の配置のほか、Cloud Storage FUSE と Tini をインストールしています。
先述の PID 1 問題に対処するため、Tini を使用してマウントとアプリケーション、それぞれのプロセスを実行、管理するようにします。
FROM golang:1.21 WORKDIR /app # tini、gcsfuse をインストール RUN set -e; \ apt-get update -y && apt-get install -y \ tini \ lsb-release; \ gcsFuseRepo=gcsfuse-`lsb_release -c -s`; \ echo "deb http://packages.cloud.google.com/apt $gcsFuseRepo main" | \ tee /etc/apt/sources.list.d/gcsfuse.list; \ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ apt-key add -; \ apt-get update; \ apt-get install -y gcsfuse \ && apt-get clean COPY main.go gcsfuse_run.sh ./ RUN go build -o main /app/main.go RUN chmod +x /app/gcsfuse_run.sh # Cloud Storage バケットをマウントするディレクトリをコンテナの環境変数に設定 ENV MNT_DIR /mnt/gcs # コンテナ実行時に Tini を実行 # https://github.com/krallin/tini ENTRYPOINT ["/usr/bin/tini", "--"] # ENTRYPOINT である Tini の引数としてスタートアップスクリプト(gcsfuse_run.sh)を指定 CMD ["/app/gcsfuse_run.sh"]
コンテナイメージのビルド・プッシュ
Cloud Build を使用してコンテナイメージをビルドし、Artifact Registry のリポジトリにプッシュします。
# Cloud Build を使用してコンテナイメージをビルド、プッシュする $ gcloud builds submit --region asia-northeast1 --tag asia-northeast1-docker.pkg.dev/myproject/myrepo/run-fuse
Cloud Run サービスのデプロイ
ビルドしたコンテナイメージを使用して Cloud Run サービスを作成します。
第 2世代の実行環境を使用するために --execution-environment
で gen2
を指定しています。
環境変数として Cloud Storage のバケット名を指定します。この変数はコンテナインスタンスが立ち上がって gcsfuse_run.sh
が実行された際、バケットをマウントするために使用されます。
# Cloud Run サービスをデプロイする $ gcloud run deploy run-fuse \ --image asia-northeast1-docker.pkg.dev/myproject/myrepo/run-fuse \ --execution-environment gen2 \ --region asia-northeast1 \ --allow-unauthenticated \ --set-env-vars BUCKET=mybucket
動作確認
Cloud Run 上のアプリケーションが、コンテナインスタンスのディレクトリにマウントされた Cloud Stroage バケットにあるファイルを読み込むことができるか確認します。
以下の内容のテキストファイル test.txt
を Cloud Storage FUSE に設定したバケットに配置します。
Hello, FUSE!
以下のコマンドを使用してファイルをアップロードします。
# Cloud Storage バケットに test.txt をアップロードする $ gcloud storage cp test.txt gs://mybucket
Cloud Run サービスの URL にアクセスすると、test.txt
の内容がブラウザに表示されます。
佐々木 駿太 (記事一覧)
G-gen最北端、北海道在住のクラウドソリューション部エンジニア
2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2024に選出。好きなGoogle CloudプロダクトはCloud Run。
趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。
Follow @sasashun0805