Miistin's Tech Blog

株式会社ミースチンの技術ブログ

Cloud Run + Go で Webサーバーを作成する

Cloud Run + Go で Webサーバーを作成する

弊社では、バックエンド側のシステム作成の際には主に Google Cloud Platform (GCP) を利用しています。
その中でも簡単な設定でAPIサーバーを作ることができる Google App Engine (GAE) をよく使っているのですが、Cloud Run でも同様にオートスケーリングされるサーバーを簡単に作ることができるので、その方法をまとめます。

Cloud Run とは

詳細は公式サイトを見るのが一番かと思います。

cloud.google.com

すごくざっくりいうと、以下の特徴があります。

  • 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の設定は置いておいて、ひとまずはオリジンサーバー部分の作成だけ)

構造はこんな感じですね

Cloud RunでOrigin サーバーを作成する - Miistin's Tech Blog

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]

以下のように作成されます。

Google Artifact Registry にレポジトリを作成

今回 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

うまくいくと、以下のように作成されます。

Artifact Registry でレポジトリを作成 : Miistin&#x27;s Tech Blog

サービスアカウントの作成

次に、 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コンソール上では以下のように表示されます。

CloudRun デプロイ完了後 : Miistin&#x27;s Tech Blog

アクセスすると、きちんとGCSに配置した画像を読み込むことができました。

Cloud Run で画像を表示する : Miistin&#x27;s Tech Blog

補足: Cloud Storage ボリューム でもいいかも?

Cloud Run では、そもそも Cloud Storageをボリュームとしてマウントすることができるそうです。

cloud.google.com

やってることは、今回手動で行ったやり方と同じみたいなのでこれでやる方がよりシンプルに作れるかもですね。
この存在知ったのが作った後だったのですが、今度使ってみようかと思います。

まとめ

今回は Cloud Run 部分のみにフォーカスを当てて使い方を紹介しました。 先述した通り、実際にはオリジンサーバーへのアクセス制限が必要な他CDNの用意など考えないといけないことも多いですが、AppEngineと違ってDockerイメージをそのままサーブできるのでやれることはぐんと広がりそうです。

技術ブログを始めます

はじめまして、株式会社ミースチンの小田です。
2023年3月に会社を設立し、少しずつ落ち着いてきたので技術ブログを週一くらいで定期的に書いていこうと思います。
なお、Qiitaにもちょくちょく書いたりしているのでよければそちらも見てみて下さい。

qiita.com

今後、Qiitaは雑多に備忘録的な記事やポエムを書き、こちらでは カテゴリなどでまとめた内容を書いていこうと思います。

ちなみに、業務では主に以下を取り扱ってます。なので、それらの話題が主になります。