くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt(SSR)をCloud Runに自動デプロイする(GitHub連携)

開発している積読ハウマッチNetlify+Nuxt(SPA)で作ってるけど、 SPAはやっぱり遅い感じが...
SSRにできて、リージョンも東京を選べるCloud Runを試してみたときの備忘録

Cloud Runについて

Google Cloud Runは、コンテナ化されたアプリをデプロイできるフルマネージド型プラットフォーム。
Dockerなどのコンテナイメージをデプロイして公開できる仕組み。

料金は、使用した分だけを払う従量課金制で、CPU/メモリ/リクエストなどそれぞれの合計。
無料枠も大きく、CPUだと最初の180,000 vCPU 秒は無料。
""請求先アカウントごとにプロジェクト間で合計され"" 、毎月リセットされるので注意。
""プロジェクトごとじゃない""

料金  |  Cloud Run のドキュメント  |  Google Cloud

Cloud Runにデプロイする

Cloud Runにデプロイできるようにする

Nuxtのガイドだとこのあたり
How to deploy on Google Cloud Run? - NuxtJS

1. Cloud Run/Cloud Buildの有効化

まずは、機能の有効化。gcloudコマンドで実行する。

# Enabling Cloud Run
$ gcloud services enable run.googleapis.com

# Enabling Cloud Build
$ gcloud services enable cloudbuild.googleapis.com

古い記事だとCloud Runがベータ版の頃の記事があり、
コマンドにbetaなどついていることがあるので注意。

2. nuxt.config.tsの設定

環境変数で起動するポートとホストを設定できるように変更。

import { Configuration } from "@nuxt/types";
const config: Configuration = {
  // ... 略
  
  // for GAE
  server: {
    port: process.env.PORT || 3000,
    host: process.env.HOST || "0.0.0.0",
    timing: false,
  },
};

export default config;

3. Dockerfileの作成

nuxt.config.tsと同じ場所に、Dockerfileを作成。

FROM node:10

# コンテナ内のwork dirを設定
WORKDIR /src

# 環境変数を設定し、ポートとホストを指定
ENV PORT 8080
ENV HOST 0.0.0.0

# package.jsonをコピーして、パッケージのインストール
COPY package.json ./
RUN npm install

# ソースをコピーして、ビルド
COPY . .
RUN npm run build

# コンテナが起動したら、nuxtを起動するよう指定
CMD [ "npm", "run", "start" ]

これで一旦、コンテナ上での起動を確認

# コンテナのビルド
$ docker build -t nuxt:1.0.0 .

# コンテナイメージの確認
$ docker images
REPOSITORY                             TAG                 IMAGE ID            CREATED             SIZE
nuxt                                   1.0.0               8567b21340f4        12 seconds ago      1.98GB

# コンテナの起動
$ docker run -d -p 8080:8080 --name nuxt nuxt:1.0.0

これで、 http://localhost:8080 で起動を確認できたらOK。

コンテナは停止/削除しておく。

# コンテナの停止
$ docker stop nuxt

# コンテナの削除
$ docker rm nuxt

# コンテナイメージの削除
$ docker rmi nuxt:1.0.0

まれにソースのコピーに失敗して、ビルドできない時がある。 .dockerignoreを用意するなどして、必要なものだけに絞るといいかも。

4. Container Registryにビルドしたコンテナを配置

Cloud Runで起動するためには、Container Registryに配置する必要がある。

gcloudコマンドで実行できる。こんな感じ。

$ gcloud builds submit --tag <GCR_HOST_NAME>/<PROJECT_ID>/<IMAGE_NAME> .

<GCR_HOST_NAME>は、場所によって異なる。gcr.ioではUS。asia.gcr.ioはアジアなど。
ドキュメントのこのあたり。
Container Registry のドキュメント |レジストリ名

<PROJECT_ID>は、GCPのプロジェクトIDを指定

<IMAGE_NAME>は、イメージの名前で任意。末尾に:1.0.0などバージョンを区別できる

今回の例だと、東京リージョンがいいので、以下のような感じ。
不要なファイルが送られないのように、.gcloudignoreも用意しておく。

# .gcloudignoreの作成
$ cp .gitignore .gcloudignore
# or 
$ cp .dockerignore .gcloudignore

# Container Registryにアップロード。末尾の.を忘れない
$ gcloud builds submit --tag asia.gcr.io/sample-project/nuxt:1.0.0 .

結構長い。完了すると、以下のようなログになる。

DONE
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

ID                                    CREATE_TIME                DURATION  SOURCE                                                                                          IMAGES                                   STATUS
9c9f5a49-de31-4ecd-8e87-710d3c2bbc2a  2020-09-03T20:18:51+00:00  5M31S     gs://sample-project_cloudbuild/source/1599164294.784973-4c85cf396c404117b03c02398b3c1702.tgz  asia.gcr.io/sample-project/nuxt:1.0.0  SUCCESS
Container Registryのイメージで起動できるかを確認

Container Registryのイメージでちゃんと起動できるかをローカルで確認する。

Container Registryにアクセスできるように、認証が必要。

$ gcloud auth configure-docker

ドキュメントだと、このあたり。
認証方法  |  Container Registry のドキュメント  |  Google Cloud

認証が完了したら、起動してみる。

$ docker run -p 8080:8080 --name nuxt --rm asia.gcr.io/sample-project/nuxt:1.0.0 

これで、 http://localhost:8080 で起動を確認できたらOK。
上記と同様に、コンテナやイメージを停止/削除しておく。

ローカルでのテスト方法のドキュメントはこの辺り。
コンテナ イメージをローカルでテストする  |  Cloud Run のドキュメント  |  Google Cloud

5. Cloud Runにデプロイする

やっと準備が整ったので、Cloud Runにデプロイ。

Container Registryを指定して、gcloudコマンドを実行。

$ gcloud run deploy --image=asia.gcr.io/sample-project/nuxt:1.0.0 --platform managed --port 8080 --region asia-northeast1

--platformは、通常であればmanagedを指定。Cloud Run for Anthosを使うはgkeなどを指定する

--regionは、リージョンの指定。asia-northeast1は東京リージョン。
他のリージョンは、以下のドキュメントを参照。
Cloud Run のロケーション  |  Cloud Run のドキュメント  |  Google Cloud

実行すると、以下のようなプロンプトが表示される。
2つ目は、認証無しでアクセスできるかをなので、yを選択する。

Service name (nuxt):  
Allow unauthenticated invocations to [nuxt] (y/N)?  

うまくデプロイできると、以下のように表示されるので、
URLにアクセスして確認する。

Done.                                                                                                                                                                                                            
Service [nuxt] revision [nuxt-00001-waf] has been deployed and is serving 100 percent of traffic at https://nuxt-usdf3rusd-an.a.run.app

Cloud RunにデプロイするのこれでOK!!

6. Cloud BuildとGitHubを使った自動デプロイ

Cloud Buildのトリガーを利用することで、masterにpushしたらデプロイすることもできる。
ドキュメントだと以下のあたり。
Cloud Build を使用した Git からの継続的なデプロイ  |  Cloud Run のドキュメント  |  Google Cloud
ビルドトリガーの作成と管理  |  Cloud Build のドキュメント  |  Google Cloud

ビルド構成ファイルの作成

まずは、構成ファイルを作成する。
Dockerfileと同じ階層にcloudbuild.yamlを作成。

中身はこんな感じ。steps配下に手順を記載していて、

  1. コンテナイメージのビルド
  2. Container Registryでアップロード
  3. Cloud Runへのデプロイ

をやっている感じ。

steps:
  # Build the container image
  - name: gcr.io/cloud-builders/docker
    id: Build
    args:
      - build
      - "--no-cache"
      - "-t"
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
      - .
      - "-f"
      - Dockerfile
  # Push the container image to Container Registry
  - name: gcr.io/cloud-builders/docker
    id: Push
    args:
      - push
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
  # Deploy container image to Cloud Run
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    id: Deploy
    entrypoint: gcloud
    args:
      - run
      - services
      - update
      - $_SERVICE_NAME
      - "--platform=managed"
      - "--image=$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
      - "--labels=managed-by=gcp-cloud-build-deploy-cloud-run,commit-sha=$COMMIT_SHA,gcb-build-id=$BUILD_ID,gcb-trigger-id=$_TRIGGER_ID,$_LABELS"
      - "--region=$_DEPLOY_REGION"
      - "--quiet"
images:
  - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
options:
  substitutionOption: ALLOW_LOOSE
tags:
  - gcp-cloud-build-deploy-cloud-run
  - gcp-cloud-build-deploy-cloud-run-managed

ファイル内では変数も利用でき、各変数は以下のドキュメントに記載されている。
変数値の置換  |  Cloud Build のドキュメント  |  Google Cloud

GitHub連動とトリガーの作成

Cloud Runの画面の「継続的デプロイの設定」から設定できる。

f:id:wannabe-jellyfish:20200904093135p:plain

GitHub認証後にリポジトリやブランチを設定すればOK

f:id:wannabe-jellyfish:20200904093058p:plain

f:id:wannabe-jellyfish:20200904093007p:plain

この段階では、Build Typeにcloudbuild.yamlを設定できないので、
同じ場所にある「継続的デプロイを編集」から変更する。

これで、pushしてみて、Cloud Buildの履歴を見ながら、動作するかを確認する。

ハマったり悩んだりしたところ

アクセスが少ないとコールドスタートが発生して遅い

Cloud Runで請求される時間は、リクエストを処理している間になる。

請求対象時間

処理するリクエストがないと、インスタンスが停止する。
そのため、料金は加算されなくなるが、次にリクエストがきたときにコールドスタートになる。

必要な分だけの請求になるけど、稼働していないと遅いので、悩みどころ。
自分の場合、PCからアクセスしてもコールドスタートだと5~6秒くらいかかる。。

コールドスタートについては、以下の記事が参考になった。 Schedulerによる停止の抑止など対策もいくつか記載されている。
[Cloud OnAir] Cloud Run Deep Dive ~ GCP で実践するモダンなサーバーレス アプリケーション開発 ~…

Cloud Buildなどで利用したファイルはStrageに保存される

アップロードしたソースやコンテナイメージは、Google Storageに保存されるので注意。
なにもしないと、どんどんファイルが増えていき、保存量で課金が発生していく。

<PROJECT_ID>_cloudbuildバケットへ、ローカルからアップロードするとソースが配置され、
asia.artifacts.<PROJECT_ID>.appspot.comasia.gcr.ioのコンテナイメージが配置される

オブジェクトのライフサイクル管理で自動削除などもできるので、設定するのがいい。

f:id:wannabe-jellyfish:20200904093229p:plain

Cloudflareを利用する場合は、ポート3000ではだめ

Cloudflareを利用する場合は注意が必要。
HTTPとして扱うポートが決まっているので、3000ではだめ。
Identifying network ports compatible with Cloudflare's proxy – Cloudflare Help Center

そのため、8080を設定している。

CloudflareでSSL/TLS 暗号化モードはFull

f:id:wannabe-jellyfish:20200904093301p:plain

Cloudflareで暗号化モードはいくつかあるが、
フレキシブルを選択すると、リダイレクトループになる。
リダイレクトループエラーのトラブルシューティング – Cloudflareヘルプセンター

また、カスタムドメインマッピングしたあと、GCP側でSSL証明書のプロビジョニングがおこなわれる。
プロビジョニング中は、Cloudflareから525が返される。。 ・カスタム ドメインのマッピング  |  Cloud Run のドキュメント  |  Google Cloud

Container Registryのパスに不要なスラッシュがあるとエラーになる

Container Registryのパスは決まっていて、
<GCR_HOST_NAME>/<PROJECT_ID>/<IMAGE_NAME>でないとエラーになる。

置換できる変数値にブランチ名($BRANCH_NAME)が利用できるので、
それを使おうとしたら、ブランチ名にスラッシュが入っていてエラーに...

変数値に対してbash 形式の文字列操作ができるようだけど、うまくいかず断念。。

Firebase HostingとCloud Runを連動させる

Firebase HostingのリクエストをCloud Runにリダイレクトさせることできる。
Firebase Hostingは、グローバルCDNが利用されるので、Cloudflareなどを利用しない場合によい。

ドキュメントだとこの辺り。
Cloud Run を使用した動的コンテンツの配信とマイクロサービスのホスティング  |  Firebase

firebase.jsonhosting部分にrewritesを追加すればOK

"hosting": {
  // ...

  // Add the "rewrites" attribute within "hosting"
  "rewrites": [ {
    "source": "**",
    "run": {
      "serviceId": "helloworld",  // "service name" (from when you deployed the container image)
      "region": "us-central1"     // optional (if omitted, default is us-central1)
    }
  } ]
}

Slackでの通知もできるらしい

試していないけど、Cloud BuildでSlackなどの通知処理を組み込めるよう。
ドキュメントのこのあたり。
Slack 通知を構成する  |  Cloud Build のドキュメント  |  Google Cloud

Cloud RunとGAEのどちらがよいかのフローチャート

以下のドキュメントに選定基準がのってる。 ・サーバーレス オプションの選択  |  Serverless Guide  |  Google Cloud

選定基準

ここをみると、

  • k8sが不要で、特別なシステムライブラリが不要なら、GAE
  • そうでなければ、Cloud Run

のよう。GAEで動かせなければ、コンテナを使えるCloud Runがよさそう。

さらにいえば、上記のコールドスタートの問題が気にならなければいいかもしれない。

GAEでもコールドスタートの問題があるが、ウォームアップ リクエストなるものがあるので、軽減できる余地がある。
ウォームアップ リクエストを構成してパフォーマンスを改善する

また、無料枠的にも、Cloud Runはフル稼働させると2日しか持たないので、
コールドスタートが許容できるかが目安になるかもしれない。

以上!!

参考にしたサイト様