Cloud Run と Cloud Storage FUSE (GCS FUSE) の基本
2023年は「Cloud Run を触って覚える」をテーマとした ひとりアドベントカレンダー を開催しており、Cloud Run のさまざまな機能や Cloud Run でよく使う構成などをご紹介しています。
20日目は Cloud Run と Cloud Storage FUSE の基本的な使い方についてご紹介します。
Cloud Run の概要は「gihyo.jp」で解説していますので、こちらもぜひご覧ください。
Cloud Storage FUSE (GCS FUSE) とは
オープンソースの FUSE アダプタを使用した、Cloud Storage バケットをファイルシステムとしてマウントする方法です。Cloud Run からも利用することができ、Cloud Storage を複数のコンテナ間、サービス間、ジョブ間の共有ストレージとして利用することができます。
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 の代替として動作します。
マルチプロセス コンテナに対応した Dockerfile を書くには高度な知識が必要でしたが、現在は マネージドにマウントする機能 をプレビューで提供しています。これを使うことで Dockerfile に特別な対応を記述する必要なく Cloud Storage FUSE を扱うことができます。
Cloud Storage バケットを作成する
マウント元となる Cloud Storage バケットを作成します。
作業は 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
を新規に作成します。
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
では type
を cloud-storage
にすることで Cloud Storage FUSE を使ってボリュームを追加しています。bucket
でマウントしたいバケットを指定します。
--add-volume-mount
では、追加したボリュームを mount-path
で指定したディレクトリパスにマウントしています。MNT_DIR
はアプリケーション内から参照しています。
動作確認
Cloud Run サービスのデプロイが完了したら、エンドポイントにブラウザでアクセスします。このアプリケーションはアクセスのたびにテキストファイルを生成し、保存する動作になっています。保存先は Cloud Storage FUSE でマウントしているディレクトリになっています。
Cloud Run サービスの動作確認
Cloud Storage バケットを見てみると、同じファイルが作成されていることが確認できます。
Cloud Storage バケットの確認
ファイルアップローダーにカスタマイズする
サンプルコードを修正し、アップローダーを作ってみましょう。ここでは Express の Multer モジュールを使ってアップローダーを作ってみます。
まず Multer をインストールします。
npm install multer
index.js
を次のコードに修正します。基本的には Multer のお作法に則ったアップローダーにしているだけで、Multer が使うディレクトリを Cloud Storage FUSE でマウントしたディレクトリに設定しているだけです。
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 FUSE の使用にあたっての注意点がいくつかあります。これらの注意点に留意した上で採用してください。
- Cloud Storage FUSE は POSIX に準拠していません。そのため、同じファイルへの複数の書き込みに対して同時実行制御を行うことはできないなど、いくつかの制限事項があります。ユースケースが当てはまらない場合は Filestore の利用を検討してください。
- Cloud Storage FUSE 自体は無料で使えますが、読み書きには Cloud Storage のコストがかかります。頻繁に読み書きが発生するワークロードで使用する場合は事前の見積りをおすすめします。こちら も確認してください。
まとめ
Cloud Storage FUSE の基本的な情報をまとめました。Cloud Run は ステートレスに動作 しますが、状態を保持したり複数のサービス・ジョブで共有したりする方法のひとつとして活用できると思います。
Discussion