Cloud Runの勉強
やってみること
- Google Coud Run でCDNのオリジンサーバーを立てる
- GCS にあらかじめ配置した画像を返却
- CDN は CloudFlareを使う
ひとまず CloudFlare登録。
無料で使えるけど、ドメインは必須らしく外部のドメイン移管は面倒だったから安いの購入
情報を登録後購入したらアクティブになった
以下を言われるがままにやった。
CloudFlareで取得先の設定とかが必要になるんだと思うんだけど、取得先がないから次から CloudRun 側の設定をしていく
もろできそうなチュートリアルが公開されてる
整理はしてないけどこんな感じでローカルではできた
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"]
#!/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サーバーでリクエストのパスを解析してマウントされた画像をとってくるだけ
ローカルでの実行コマンド
$ 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でちゃんと動くのかな
goのコードはこんな感じ(1ファイルのみ。整理してないしエラーとか考慮してない)
ここでCloudFlareからきたリクエストかをチェックする必要あるけど今はまだやってない
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")))
}
現状のフォルダ構成
・ src
・ main.go
・ Dockerfile
・ go.mod
・ go.sum
・ run.sh
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された
ブラウザのGCPコンソール上にはUIないけど、以下で一般公開もできるらしい。やらないけど
ちなみに、以下の権限を持つサービスアカウントを作成してる。
- roles/storage.objectViewer
デプロイ
$ 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
別リージョンでもちゃんとデプロイできるね
gcsfuse のマウントもEchoサーバーの立ち上げもうまくいってるっぽい
画像もとれた
こっからはCloudflareの設定だけどひとまず今日はここまで。
ちなみに今のこのやり方、GCSのバケットの中のアセットが少ないからいいけど Cloud Run へのアクセスがなくなって停止したら中身消えちゃう(はず)だから量が多いと毎回立ち上がるたびに マウント → GCSの中身全取得 って感じになりそうだから、量が多いならGCS API使ってひとつずつ取ってくる方がいいかも
というか運用するなら多分そうすべき
昨日の続き。直接 GCS をAPI経由で見に行くようにサーバー側修正。
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")))
}
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
Memory limit of 512 MiB exceeded with 517 MiB used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits
が出るようになったな…
かなりシンプルなプログラムなのになぜ…?
Docker内で使用されるメモリ量は、statを見るかぎりは60MB くらいなんだけど…
memory limit を調整することでデプロイできるとは思うけど、Cloud Run ではそんなにメモリ食うの…?
というか、今のサーバー側の作り方だとやっぱり GAEでいいじゃんって気がするんだけど、Cloud Run のメリットよくわからなくなってきたな。。
gcsfuse を使ってとった場合 : 140ms
gcs api を使ってとった場合 : 168ms
ちょっと増えるけど全然 gcs api 経由でよさそう
というか本当になんでこんなにメモリ食ってるんだ?
マイクロなスペックでもサクサク動くのがgo君のいいところだったのに
ぇぇ…docker経由じゃなくソースからでもデプロイできるんだ…
まぁでもやることは同じっぽいな。Artifact Repositoryにコンテナ作成してごにょごにょするみたい
もしかして go build やってないことで余計にメモリ食ってるとか…?
関係あるのかな…
go build するようにしたら 512MBに収まった。
そだったんだ…?
訂正版の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"]
そろそろここまでをブログにまとめる。明日
Cloud Run のカスタムドメインの設定
とりあえずポチる
あらかじめCloudFlareでとっておいたドメインを設定
Search Consoleで確認作業をとれって言われるので言われるがままにやる
CloudFlareに遷移する。便利だね
サクッと確認が完了する
もう一度「CloudRunのドメインマッピング」を表示すると、選択できるようになってる。
あっという間にできた。
これ設定しろってなってるから、 CloudFlareで設定する
Waiting for certificate provisioning. You must configure your DNS records for certificate issuance to begin.
から1日経っても変わらない。
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.
と言ってる人がいるのでこっちも一応やってみる。
24時間たったところ反映された。
上記のせいでできなかったのか、上記をしなくても2日かかったのかは不明
CloudFlareのDNS設定を「Proxy」にしてみたけど、今のところ毎回アクセスのたびに直接取りに来てる感じがする。TTLの問題かな
リダイレクトループに陥っちゃった。。。
CloudFlareがhttpで接続しにきてる。それでCloudRun側でHTTPSにリダイレクトし、CloudFlareがまたhttp経由で接続し・・・
という感じになってるっぽい
これか
SSL/TLS -> 設定をみると、フレキシブルになっていたのでいったんフルにしてみる
フルにしたらまた毎回アクセスされるようになったな・・・
少し経ったらちゃんとキャッシュが乗るようになった。
Cloud Run で以下を行うようにしてみる。
- ロードバランサ―によるIP制限
- ヘッダーの認証
Cloudflareでは以下のIPでくるらしい
ロードバランサ―を利用するデプロイ
$ 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 #追加