💿

Cloud Run と Cloud Storage FUSE (GCS FUSE) の基本

2023/12/20に公開

2023年は「Cloud Run を触って覚える」をテーマとした ひとりアドベントカレンダー を開催しており、Cloud Run のさまざまな機能や Cloud Run でよく使う構成などをご紹介しています。

20日目は Cloud Run と Cloud Storage FUSE の基本的な使い方についてご紹介します。

Cloud Run の概要は「gihyo.jp」で解説していますので、こちらもぜひご覧ください。

https://gihyo.jp/article/2023/10/modern-app-development-on-google-cloud-03

Cloud Storage FUSE (GCS FUSE) とは

オープンソースの FUSE アダプタを使用した、Cloud Storage バケットをファイルシステムとしてマウントする方法です。Cloud Run からも利用することができ、Cloud Storage を複数のコンテナ間、サービス間、ジョブ間の共有ストレージとして利用することができます。

https://github.com/GoogleCloudPlatform/gcsfuse

Cloud Storage FUSE を使うには、Cloud Run サービスを第2世代の実行環境で実行する必要があります。第2世代ではネットワーク ファイル システムをディレクトリにマウントする機能をサポートしており、Cloud Storage FUSE を使うことで Cloud Storage バケットをディレクトリのように扱うことができます。Cloud Run ジョブは第2世代の実行環境が自動的に設定されているため、特別意識する必要がありません。

クイックスタート

構成を理解する

Cloud Run は通常、コンテナごとに 1 つのプロセスを実行しますが、Cloud Storage FUSE を使用する場合はアプリケーション用のプロセスとマウント用のプロセスの両方を実行するマルチプロセス コンテナを使用する必要があります。

マルチプロセス コンテナを使用する場合は単一のコンテナでは考える必要がなかった PID 1 問題 などを回避する必要が出てきます。回避しなければ、シャットダウンのためのシグナル (SIGTERM) がアプリケーションに送信されず正常に終了処理が行えない、マウント用のプロセスがゾンビプロセスとして立ち上がってしまうなどの問題が発生してしまいます。

次のサンプルではプロセスマネージャーの tini を使って、複数のプロセスを管理しています。tini ではゾンビプロセスをクリーンアップしたり、SIGTERM を明示的にハンドルしない場合でもプロセスを終了させることができ、かつ init の代替として動作します。

https://cloud.google.com/run/docs/tutorials/network-filesystems-fuse?hl=ja

マルチプロセス コンテナに対応した Dockerfile を書くには高度な知識が必要でしたが、現在は マネージドにマウントする機能 をプレビューで提供しています。これを使うことで Dockerfile に特別な対応を記述する必要なく Cloud Storage FUSE を扱うことができます。

https://cloud.google.com/sdk/gcloud/reference/alpha/run/deploy#--add-volume

Cloud Storage バケットを作成する

マウント元となる Cloud Storage バケットを作成します。

作業は Cloud Shell 上で行いますので、まずは Cloud Shell を開きます。

Cloud Shell の起動
Cloud Shell の起動

次のコマンドで Cloud Storage バケットを作成します。BUCKET_NAME は適宜修正してください。

gcloud storage buckets create gs://<BUCKET_NAME> \
  --location asia-northeast1

サンプル プロジェクトをクローンする

サンプル プロジェクトをクローンします。nodejs-docs-samples をすでにクローン済みの方はクローンのコマンドはスキップしてください。

git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git
cd nodejs-docs-samples/run/filesystem/

Cloud Run サービスをデプロイする

サンプル プロジェクトの Dockerfile はマルチプロセス コンテナを動かすための実装になっているため、シンプルな Dockerfile に変更します。

まず Dockerfile は Filestore 用の Dockerfile なので、ファイル名を変更して退避します。

mv Dockerfile filestore.Dockerfile

次に Dockerfile を新規に作成します。

Dockerfile
FROM node:20-slim

ENV APP_HOME /app
WORKDIR $APP_HOME
COPY package*.json ./

RUN npm install --only=production
COPY . ./

CMD ["npm", "start"]

Cloud Run サービスが使用するサービス アカウントを作成し、GCS バケットの操作権限を付与します。

gcloud iam service-accounts create fs-identity
gcloud projects add-iam-policy-binding <PROJECT_ID> \
     --member "serviceAccount:fs-identity@<PROJECT_ID>.iam.gserviceaccount.com" \
     --role "roles/storage.objectAdmin"

次のコマンドで Cloud Run サービスをデプロイします。

gcloud alpha run deploy filesystem-app \
  --region asia-northeast1 \
  --source . \
  --execution-environment gen2 \
  --service-account fs-identity \
  --allow-unauthenticated \
  --add-volume=name=gcs,type=cloud-storage,bucket=<BUCKET_NAME> \
  --add-volume-mount=volume=gcs,mount-path=/mnt/gcs \
  --update-env-vars MNT_DIR=/mnt/gcs

ポイントは --add-volume--add-volume-mount です。

--add-volume では typecloud-storage にすることで Cloud Storage FUSE を使ってボリュームを追加しています。bucket でマウントしたいバケットを指定します。

--add-volume-mount では、追加したボリュームを mount-path で指定したディレクトリパスにマウントしています。MNT_DIR はアプリケーション内から参照しています。

動作確認

Cloud Run サービスのデプロイが完了したら、エンドポイントにブラウザでアクセスします。このアプリケーションはアクセスのたびにテキストファイルを生成し、保存する動作になっています。保存先は Cloud Storage FUSE でマウントしているディレクトリになっています。

Cloud Run サービスの動作確認
Cloud Run サービスの動作確認

Cloud Storage バケットを見てみると、同じファイルが作成されていることが確認できます。

Cloud Storage バケットの確認
Cloud Storage バケットの確認

ファイルアップローダーにカスタマイズする

サンプルコードを修正し、アップローダーを作ってみましょう。ここでは Express の Multer モジュールを使ってアップローダーを作ってみます。

まず Multer をインストールします。

npm install multer

index.js を次のコードに修正します。基本的には Multer のお作法に則ったアップローダーにしているだけで、Multer が使うディレクトリを Cloud Storage FUSE でマウントしたディレクトリに設定しているだけです。

index.js
const express = require('express')
const multer = require('multer')
const app = express()
const mntDir = process.env.MNT_DIR
const port = parseInt(process.env.PORT) || 8080

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, mntDir)
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname)
  }
})
const upload = multer({ storage })

app.use(mntDir, express.static(mntDir))
app.set('trust proxy', 1)

app.get('/', (req, res) => {
  const html = '\
<!DOCTYPE html>\
<html lang="ja"></html>\
<head>\
  <meta charset="UTF-8">\
  <title>File Uploader</title>\
</head>\
<body>\
  <form action="/upload" method="POST" enctype="multipart/form-data">\
    <input type="file" name="file">\
    <button type="submit">アップロードする</button>\
  </form>\
</body>\
</html>'
  res.send(html) 
})

app.post('/upload', upload.single('file'), (req, res) => {
  res.send(`${req.file.originalname} のアップロードが完了しました。`)
})

app.listen(port, () => {
  console.log(`Listening on port ${port}`)
})

process.on('SIGTERM', () => {
  console.log('Received SIGTERM signal. Exiting.')
})

module.exports = app

あとは gcloud run deploy コマンドを実行し、更新するだけです。

gcloud alpha run deploy filesystem-app \
  --region asia-northeast1 \
  --source . \
  --execution-environment gen2 \
  --service-account fs-identity \
  --allow-unauthenticated \
  --add-volume=name=gcs,type=cloud-storage,bucket=<BUCKET_NAME> \
  --add-volume-mount=volume=gcs,mount-path=/mnt/gcs \
  --update-env-vars MNT_DIR=/mnt/gcs

デプロイ後、ブラウザで開いてみるとフォームが表示されます。好きなファイルをアップロードできます(ここでは Google_Logo.png をアップロードしています)。

アップローダー
アップローダー

Cloud Storage バケットを見てみると、アップロードが無事できていることが確認できます。

Cloud Storage バケットの確認
Cloud Storage バケットの確認

注意事項

Cloud Storage FUSE の使用にあたっての注意点がいくつかあります。これらの注意点に留意した上で採用してください。

  • Cloud Storage FUSE は POSIX に準拠していません。そのため、同じファイルへの複数の書き込みに対して同時実行制御を行うことはできないなど、いくつかの制限事項があります。ユースケースが当てはまらない場合は Filestore の利用を検討してください。
  • Cloud Storage FUSE 自体は無料で使えますが、読み書きには Cloud Storage のコストがかかります。頻繁に読み書きが発生するワークロードで使用する場合は事前の見積りをおすすめします。こちら も確認してください。

まとめ

Cloud Storage FUSE の基本的な情報をまとめました。Cloud Run は ステートレスに動作 しますが、状態を保持したり複数のサービス・ジョブで共有したりする方法のひとつとして活用できると思います。

Google Cloud Japan

Discussion