Open51

Cloud Runの勉強

br_branchbr_branch

やってみること

  • Google Coud Run でCDNのオリジンサーバーを立てる
    • GCS にあらかじめ配置した画像を返却
  • CDN は CloudFlareを使う
br_branchbr_branch

ひとまず CloudFlare登録。
無料で使えるけど、ドメインは必須らしく外部のドメイン移管は面倒だったから安いの購入

br_branchbr_branch

CloudFlareで取得先の設定とかが必要になるんだと思うんだけど、取得先がないから次から CloudRun 側の設定をしていく

br_branchbr_branch

整理はしてないけどこんな感じでローカルではできた

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
#!/bin/bash

mkdir $MOUNT_DIR
nohup gcsfuse --implicit-dirs --foreground --debug_gcs --debug_fuse miistin.appspot.com $MOUNT_DIR &

go run src/main.go

main.go は Echoサーバーでリクエストのパスを解析してマウントされた画像をとってくるだけ

br_branchbr_branch

ローカルでの実行コマンド

$ docker run --env GOOGLE_APPLICATION_CREDENTIALS=/app/src/go/mount/XXXXX.json --privileged -v .\mount:/app/src/go/mount -p 8080:8080 <IMAGE ID>

gcsfuse を使う際に --privileged パラメータが必要だったけどCloud Rundでちゃんと動くのかな

br_branchbr_branch

goのコードはこんな感じ(1ファイルのみ。整理してないしエラーとか考慮してない)
ここでCloudFlareからきたリクエストかをチェックする必要あるけど今はまだやってない

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")))
}
br_branchbr_branch

現状のフォルダ構成

・ src
 ・ main.go
・ Dockerfile
・ go.mod
・ go.sum
・ run.sh
br_branchbr_branch

Cloud Run のコマンド

# 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]

# 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

pushされた

br_branchbr_branch

ちなみに、以下の権限を持つサービスアカウントを作成してる。

  • roles/storage.objectViewer
br_branchbr_branch

デプロイ

$ gcloud run deploy cdnservice --image us-central1-docker.pkg.dev/[PROJECT_ID]/cloudrun-sample/cloudrun-image:tag1 --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

別リージョンでもちゃんとデプロイできるね

br_branchbr_branch

gcsfuse のマウントもEchoサーバーの立ち上げもうまくいってるっぽい

br_branchbr_branch

こっからはCloudflareの設定だけどひとまず今日はここまで。

br_branchbr_branch

ちなみに今のこのやり方、GCSのバケットの中のアセットが少ないからいいけど Cloud Run へのアクセスがなくなって停止したら中身消えちゃう(はず)だから量が多いと毎回立ち上がるたびに マウント → GCSの中身全取得 って感じになりそうだから、量が多いならGCS API使ってひとつずつ取ってくる方がいいかも

というか運用するなら多分そうすべき

br_branchbr_branch

昨日の続き。直接 GCS をAPI経由で見に行くようにサーバー側修正。

main.go
package main

import (
	"fmt"
	"io"
	"log"
	"regexp"
	"strings"

	"cloud.google.com/go/storage"
	"github.com/bradhe/stopwatch"
	"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 {
		client, err := storage.NewClient(c.Request().Context())
		if err != nil {
			return c.String(500, "Internal Server Error")
		}

		path := c.Param("path")
		tm := stopwatch.Start()

		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"
		}


		bucketName := os.Getenv("BUCKET_NAME")
		rc, err := client.Bucket(bucketName).Object(fmt.Sprintf("%s/%s", c.Param("dir"), c.Param("path"))).NewReader(c.Request().Context())

		if err != nil {
			return c.String(404, "Not Found")
		}
		ls, err := io.ReadAll(rc)
		if err != nil {
			return c.String(404, "Not Found")
		}

		log.Printf("Served %s in %s", path, tm.Stop().Milliseconds())
		return c.Blob(200, contentType, ls)
	})

	log.Fatal(e.Start(fmt.Sprintf(":8080")))
}
br_branchbr_branch

Dockerfileも修正

FROM golang:1.20.13

WORKDIR /go/src/app

COPY ./go.mod /go/src/app
COPY ./go.sum /go/src/app
COPY ./src /go/src/app/src
COPY run.sh /go/src/app

ENV MOUNT_DIR /go/src/app/gcs

RUN go mod tidy
EXPOSE 8080

CMD ["go", "run", "src/main.go"]

あと面倒だから debug用に docker-comopose作成

version: '3.8'

services:
  webapp:
    build: .
    ports:
      - "8080:8080"
    environment:
      - BUCKET_NAME=[BUCKET_NAME]
      - GOOGLE_APPLICATION_CREDENTIALS=/app/src/go/mount/XXXX.json
    volumes:
        - ./mount:/app/src/go/mount
br_branchbr_branch

Docker内で使用されるメモリ量は、statを見るかぎりは60MB くらいなんだけど…
memory limit を調整することでデプロイできるとは思うけど、Cloud Run ではそんなにメモリ食うの…?

br_branchbr_branch

というか、今のサーバー側の作り方だとやっぱり GAEでいいじゃんって気がするんだけど、Cloud Run のメリットよくわからなくなってきたな。。

br_branchbr_branch

gcsfuse を使ってとった場合 : 140ms
gcs api を使ってとった場合 : 168ms

ちょっと増えるけど全然 gcs api 経由でよさそう

br_branchbr_branch

というか本当になんでこんなにメモリ食ってるんだ?
マイクロなスペックでもサクサク動くのがgo君のいいところだったのに

br_branchbr_branch

まぁでもやることは同じっぽいな。Artifact Repositoryにコンテナ作成してごにょごにょするみたい

br_branchbr_branch

もしかして go build やってないことで余計にメモリ食ってるとか…?
関係あるのかな…

br_branchbr_branch

訂正版のDockerfile

FROM golang:1.20.13-bullseye

WORKDIR /go/src/app

COPY ./go.mod /go/src/app
COPY ./go.sum /go/src/app
COPY ./src /go/src/app/src

RUN go mod download
RUN go mod tidy
RUN go build -o main ./src

EXPOSE 8080

CMD ["./main"]
br_branchbr_branch

Cloud Run のカスタムドメインの設定

とりあえずポチる



あらかじめCloudFlareでとっておいたドメインを設定

br_branchbr_branch

Search Consoleで確認作業をとれって言われるので言われるがままにやる

CloudFlareに遷移する。便利だね

サクッと確認が完了する

br_branchbr_branch

もう一度「CloudRunのドメインマッピング」を表示すると、選択できるようになってる。

br_branchbr_branch

これ設定しろってなってるから、 CloudFlareで設定する

br_branchbr_branch

Waiting for certificate provisioning. You must configure your DNS records for certificate issuance to begin.

から1日経っても変わらない。

br_branchbr_branch

Turning proxying off in CloudFlare resolved the issue in my case (keeping it as DNS only).

Most likely the Google balancer needs to get the request first-hand in order to make the certificate safe.

と言ってる人がいるのでこっちも一応やってみる。

br_branchbr_branch

24時間たったところ反映された。
上記のせいでできなかったのか、上記をしなくても2日かかったのかは不明

br_branchbr_branch

CloudFlareのDNS設定を「Proxy」にしてみたけど、今のところ毎回アクセスのたびに直接取りに来てる感じがする。TTLの問題かな

br_branchbr_branch

CloudFlareがhttpで接続しにきてる。それでCloudRun側でHTTPSにリダイレクトし、CloudFlareがまたhttp経由で接続し・・・

という感じになってるっぽい

br_branchbr_branch

SSL/TLS -> 設定をみると、フレキシブルになっていたのでいったんフルにしてみる

br_branchbr_branch

フルにしたらまた毎回アクセスされるようになったな・・・

br_branchbr_branch

少し経ったらちゃんとキャッシュが乗るようになった。

br_branchbr_branch

ロードバランサ―を利用するデプロイ

$ gcloud run deploy cdnservice \
--image us-central1-docker.pkg.dev/[PROJECT_ID]/cloudrun-sample/cloudrun-image:tag1 \
--platform=managed \
--project=[PROJECT_ID] \
--region=asia-northeast1 \
--service-account [サービスアカウント]@[PROJECT_ID].iam.gserviceaccount.com \
--update-env-vars BUCKET_NAME=<バケット名> \
--allow-unauthenticated \ #追加
--ingress=internal-and-cloud-load-balancing #追加