弊社では、バックエンド側のシステム作成の際には主に Google Cloud Platform (GCP) を利用しています。
その中でも簡単な設定でAPIサーバーを作ることができる Google App Engine (GAE) をよく使っているのですが、Cloud Run でも同様にオートスケーリングされるサーバーを簡単に作ることができるので、その方法をまとめます。
Cloud Run とは
詳細は公式サイトを見るのが一番かと思います。
すごくざっくりいうと、以下の特徴があります。
- Docker とかのコンテナを実行できるサービスです
- サービス (サーバーみたいにしばらく常駐する) と ジョブ (実行したらすぐ終了する) を選ぶことができます
- オートスケーリング (負荷に応じて自動で冗長化して増強したり減らしたりする) に対応しています
- どれくらいのメモリやCPUを使うかなどを指定することができます
- 料金はCPU/メモリの利用 (秒単位) とリクエスト数 (100万単位)
こちらは 実行される Docker イメージを用意すればいいだけで、リクエストの負荷などもあまり気にする必要がないので楽に運用することができます。
また、AWS や Azure にも、それぞれ AWS App Runner や Azure Container Apps のように同じ思想のサービスが存在しているそうですが、この分野では Cloud Run がフロントランナーだそうで、一番機能が高いそうです(他を使ったことがないのでどれくらい違うのか知らないのですが…)。
GCP内の類似サービス
GCPには Cloud Run の他に、 Google Kubaernetes Engine (GKE) や Google App Engine (GAE) も存在します。
どういう部分に違いがあるのかもざっくりと記載します。
Google Kubaernetes Engine (GKE)
- 同じように Docker コンテナを管理・デプロイし、オートスケールができる特徴があります
- GKE は複数のコンテナを組み合わせて大規模なシステムを構築・管理できることに最大の強みがありますが、 Cloud Run はそれよりもシンプルです
- とはいえ、Cloud Run でもマイクロサービス設計で複数のコンテナを組み合わせて大規模なシステムを作ることはできます
- GKE は、かなり細かいチューニングやカスタマイズができますが、その分管理が複雑になります。 Cloud Runはインフラ部分はすべて任せられるためシンプルですが、柔軟性には限界があります
Google App Engine (GAE)
- 同様にフルマネージド型のサービスのため、スケーリングや証明書などインフラ面はすべて任せられる点は GAE も Cloud Run も同じです
- GAE はコードと設定ファイルさえ用意すれば簡単にデプロイすることが出来、Cloud Run より楽にサーバー環境を作ることができます
- GAEのサービス分類は PaaS (Platform as a service) であり、使用できる言語にも制限があったりと Cloud Run より自由度が低いです
Cloud Run は GAE と GKE の間(GAE寄り) に位置付けられてる感じでしょうか。
GAEの楽さを兼ね備えつつ、コンテナを使うことで自由度の高さも持ち合わせています。
簡単な オリジンサーバーを作成する
何か簡単なサンプルをということで、CDNのオリジンサーバーを Cloud Run で作ってみようと思います。
(CDNの設定は置いておいて、ひとまずはオリジンサーバー部分の作成だけ)
構造はこんな感じですね
GCS と Cloud Run は Cloud Storage FUSE を利用して同期させ、CDN側からリクエストがあればそのリソースを返却することを想定します。
実際に運用するならオリジンサーバーを守るためにアクセス制限なども設ける必要がありますが、今回は Cloud Run の実装部分だけフォーカスします。
構成
こんな感じになります。
<Project Root> ├ src │ └ main.go ├ Dockerfile ├ go.mod ├ go.sum └ run.sh
Dockerfile では go のイメージから gcsfuse をインストールしたイメージを作成します。
Cloud Run のデフォルトポートは 8080 なので、それも指定しています。
Dockerfile
FROM golang:1.20.13 WORKDIR /go/src/app RUN apt-get -qqy update && apt-get install -qqy curl apt-transport-https lsb-release gnupg sudo && \ export GCSFUSE_REPO="gcsfuse-$(lsb_release -c -s)" && \ echo "deb https://packages.cloud.google.com/apt $GCSFUSE_REPO main" > /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 COPY ./go.mod /go/src/app COPY ./go.sum /go/src/app COPY ./src /go/src/app/src COPY run.sh /go/src/app RUN chmod +x /go/src/app/run.sh ENV MOUNT_DIR /go/src/app/gcs RUN go mod tidy EXPOSE 8080 CMD ["/go/src/app/run.sh"]
run.sh
run.sh
は以下の感じです。
バックグラウンド上で gcsfuse
を実行した後、サーバーを立ち上げています。
#!/bin/bash mkdir $MOUNT_DIR nohup gcsfuse --implicit-dirs --foreground --debug_gcs --debug_fuse [バケット] $MOUNT_DIR & go run src/main.go
src/main.go
マウントされたリソースを読んでるだけですね。
package main import ( "fmt" "log" "os" "regexp" "strings" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { e := echo.New() e.Use(middleware.Recover()) e.Use(middleware.Logger()) e.GET("/:dir/:path", func(c echo.Context) error { path := c.Param("path") r, _ := regexp.Compile(`.*\.\w+$`) if !r.MatchString(path) { return c.String(404, "Not Found") } extR, _ := regexp.Compile(`\.\w+$`) extension := strings.TrimPrefix(extR.FindString(path), ".") if extension == "jpg" { extension = "jpeg" } var contentType string switch extension { case "jpeg": contentType = "image/jpeg" case "png": contentType = "image/png" case "gif": contentType = "image/gif" case "webp": contentType = "image/webp" case "svg": contentType = "image/svg+xml" case "bmp": contentType = "image/bmp" case "ico": contentType = "image/vnd.microsoft.icon" case "json": contentType = "application/json" case "xml": contentType = "application/xml" case "pdf": contentType = "application/pdf" case "txt": contentType = "text/plain" default: contentType = "application/octet-stream" } ls, err := os.ReadFile(fmt.Sprintf("/go/src/app/gcs/%s/%s", c.Param("dir"), c.Param("path"))) if err != nil { return c.String(404, "Not Found") } return c.Blob(200, contentType, ls) }) log.Fatal(e.Start(fmt.Sprintf(":8080"))) }
Artifact Registry のレポジトリ作成
先述の通り Cloud Run はコンテナを扱うため、まずはそのイメージをpushする必要があります。
GCPには Artifact Registry というサービスがあり、そこでイメージファイルを登録することができるので、そのためのレポジトリを作成します。
# Artifact Registry を使うための認証情報設定 # 下記は us-central1 のリージョンに配置する場合 $ gcloud auth configure-docker us-central1-docker.pkg.dev # レポジトリの作成 $ gcloud artifacts repositories create cloudrun-sample --location=us-central1 --repository-format=docker --project=[PROJECT_ID]
以下のように作成されます。
今回 us-central1
のリージョンで作成をしました。
Cloud Run をデプロイする際には別リージョンの指定もできるので、一つのリージョンに統一するという形で問題ないかと思います。
レポジトリにイメージをpushする
次に、作成した Docker イメージをビルドしてpushします。
# docker のビルド $ docker build -t us-central1-docker.pkg.dev/[PROJECT_ID]/cloudrun-sample/cloudrun-image:tag1 . # dockerイメージのpush $ docker push us-central1-docker.pkg.dev/[PROJECT_ID]/cloudrun-sample/cloudrun-image:tag1
うまくいくと、以下のように作成されます。
サービスアカウントの作成
次に、 Cloud Run 実行時に付与するサービスアカウントを作成します。
今回はGCSにアクセスをするだけなので、 roles/storage.objectViewer
だけを付与したサービスアカウントを作成します。
# 変数 EXPORT PROJECT_ID=[プロジェクトID] EXPORT SERVICE_ACCOUNT_ID=[サービスアカウント名] #例: cloudrun-sample とか # サービスアカウントの作成 $ gcloud iam service-accounts create ${SERVICE_ACCOUNT_ID} \ --description="Cloud Run Sample" \ --display-name="CloudRunSample" \ --project ${PROJECT_ID} # 必要なロールの設定 $ gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}@${PROJECT_ID}.iam.gserviceaccount.com" \ --role="roles/storage.objectViewer"
デプロイ
最後にデプロイをします。
$ gcloud run deploy cdnservice \ --image us-central1-docker.pkg.dev/${PROJECT_ID}/cloudrun-sample/cloudrun-image:tag1 \ # さっきpushしたイメージファイル --platform=managed \ --project=[PROJECT_ID] \ --region=asia-northeast1 \ --service-account [サービスアカウント]@${PROJECT_ID}.iam.gserviceaccount.com # さっき作成したサービスアカウント Deploying container to Cloud Run service [cdnservice] in project [xxxx] region [asia-northeast1] ✓ Deploying... Done. ✓ Creating Revision... ✓ Routing traffic... Done. Service [cdnservice] revision [cdnservice-00003-jeq] has been deployed and is serving 100 percent of traffic. Service URL: https://cdnservice-xxxxxx-an.a.run.app
デプロイ完了後、GCPコンソール上では以下のように表示されます。
アクセスすると、きちんとGCSに配置した画像を読み込むことができました。
補足: Cloud Storage ボリューム でもいいかも?
Cloud Run では、そもそも Cloud Storageをボリュームとしてマウントすることができるそうです。
やってることは、今回手動で行ったやり方と同じみたいなのでこれでやる方がよりシンプルに作れるかもですね。
この存在知ったのが作った後だったのですが、今度使ってみようかと思います。
まとめ
今回は Cloud Run 部分のみにフォーカスを当てて使い方を紹介しました。 先述した通り、実際にはオリジンサーバーへのアクセス制限が必要な他CDNの用意など考えないといけないことも多いですが、AppEngineと違ってDockerイメージをそのままサーブできるのでやれることはぐんと広がりそうです。