Cloud RunからCloud Storageをファイルシステムとしてマウントする

記事タイトルとURLをコピーする

G-gen の佐々木です。当記事では Cloud RunCloud Storage FUSE を使用して、オブジェクトストレージである Cloud Storage のバケットをコンテナ内のディレクトリにマウントしてみます。

前提知識

Cloud Run とは

Cloud Run は Google Cloud のマネージドなコンテナ実行環境を使用してアプリケーションを実行することができるサーバレス コンテナコンピューティング サービスです。
HTTP リクエストを処理のトリガーとする Cloud Run services と、手動もしくはスケジュール、ワークフローをトリガーとする Cloud Run jobs の 2種類があります。
それぞれ以下の記事で詳細を解説しています。

blog.g-gen.co.jp

blog.g-gen.co.jp

Cloud Storage(GCS)とは

Cloud Storage は容量無制限のオブジェクトストレージサービスであり、99.999999999% (イレブンナイン) の堅牢性を持つオブジェクトストレージを安価に利用することができます。
Cloud Storage の詳細は以下の記事で解説しています。

blog.g-gen.co.jp

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 は 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 の利用料として料金が発生します。

考慮事項

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 Run で Cloud Storage FUSE を使用する

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-environmentgen2 を指定しています。
環境変数として 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 の内容がブラウザに表示されます。

Cloud Storage にアップロードしたファイルの内容が表示される

佐々木 駿太 (記事一覧)

G-gen最北端、北海道在住のクラウドソリューション部エンジニア

2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2024に選出。好きなGoogle CloudプロダクトはCloud Run。

趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。