くらげになりたい。

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

Nuxt(SSR)をGAEに自動デプロイ(GitHub連携)とハマったポイント

Clund Runを試したので、次はGAE。

Cloud Runでの課題

試してみた結果、コールドスタートがよく出るっぽい。。

Cloud Schedulerを使って、定期的にリクエストを送って、
インスタンスが停止しないようにする or 停止してたら起こす、ようにするればよいが気になる。。

GAEのメリット

GAEはGoogle Cloud Platformが提供するPaaSサービスの一つ。
決まった言語や構成であれば、簡単にサーバをたてられる。

特に最小インスタンス数を設定でき、常に1つは稼働させられるので、
Cloud Runでの課題が解消できそう

GAEでデプロイする

Clund Runを試したで以下の2つを実施すればOK

1. GAEの設定ファイル(app.yaml)の用意

Nuxtの公式ドキュメントを参照しつつ対応。
Google App Engine へデプロイするには? - NuxtJS

app.yamlの設定は以下の通り。細かい設定は、以下を参照。
app.yaml 構成ファイル  |  Node.js 用 App Engine スタンダード環境に関するドキュメント

# Nodeのバージョン設定
runtime: nodejs12

# インスタンス: F1は無料なので、F1を設定
instance_class: F1

service: default

# ハンドラー。URLごとの挙動を設定
handlers:
  # Nuxt v2のクライアントモジュールは .nuxt/dist/client に生成される。静的化しておく。
  - url: /_nuxt
    static_dir: .nuxt/dist/client
    secure: always

  # static ディレクトリも同様に静的化しておく。
  - url: /(.*\.(gif|png|jpg|ico|txt|json|svg))$
    static_files: static/\1
    upload: static/.*\.(gif|png|jpg|ico|txt|json|svg)$
    secure: always

  # その他はそのまま
  - url: /.*
    script: auto
    secure: always

# インスタンスのスケール設定
automatic_scaling:
  # インスタンスの最大数を指定。各バージョンで有効
  max_instances: 1
  # アイドル中のインスタンス最小数を指定。トラフィック割当が多いバージョンで有効
  min_idle_instances: 1
  # インスタンスが設定値。CPUやスループットの稼働率は最大に設定
  target_cpu_utilization: 0.95
  target_throughput_utilization: 0.95
  max_concurrent_requests: 50
  min_pending_latency: 3000ms
  max_pending_latency: automatic

# 環境変数の設定
env_variables:
  HOST: "0.0.0.0"
  PORT: "8080"
  NODE_ENV: "production"

無料枠に収まるよう、F1で1インスタンスが稼働する状態になるよう、
インスタンスのスケールを設定している。(automatic_scaling周り)

また、nuxt.config.tsでソースディレクトリを変更している場合は、
staticディレクトリのパスも変わるので注意

handlers:
  # ...略
  
  # static ディレクトリも同様に静的化しておく。
  - url: /(.*\.(gif|png|jpg|ico|txt|json|svg))$
    # ※app/staticに変更
    static_files: app/static/\1
    # ※app/staticに変更
    upload: app/static/.*\.(gif|png|jpg|ico|txt|json|svg)$
    secure: always

  # その他はそのまま

手動デプロイ

gcloudコマンドを利用した手動デプロイは以下。

$ gcloud app deploy --project=<PROJECT-ID>

コマンドの他の引数は、以下の公式ドキュメントを参照。
gcloud app deploy  |  Cloud SDK Documentation  |  Google Cloud

2. Cloud Buildの設定ファイル(cloudbuild.yaml)に"gcp-build"を追加

Cloud Buildを使って、GitHubにプッシュしたらGAEにデプロイされるように設定する。
基本は、Clund Runを試したときと同じなので割愛。

cloudbuild.yamlはこんな感じ。

steps:
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    id: Deploy
    entrypoint: gcloud
    args:
      - app
      - deploy
      - "--appyaml=${_APP_YAML}"
      - "--project=${PROJECT_ID}"
      - "--version=${_VERSION}"
      - "--quiet"

options:
  substitutionOption: ALLOW_LOOSE
substitutions:
  _APP_YAML: "app.yaml"
  _VERSION: "${COMMIT_SHA}"
tags:
  - gcp-cloud-build-deploy-gae

Cloud Runのときと比べ、デプロイするだけのstepは1つのみ。
ただ、ビルドは必要なので、package.jsonにカスタムビルドの設定が必要。

Node.jsでのカスタムビルドは、scriptsにgcp-buildで指定できる。
カスタム ビルドステップの実行  |  Node.js 用 App Engine スタンダード環境に関するドキュメント

{
  // ...
  "scripts": {
    // ...
    "gcp-build": "npm run build"
  },
  // ...
}

これで、デプロイ時にnpm run gcp-buildを実行してくれるようになる感じ。

ハマったポイント

メリットはあるけど、かなりハマった。。注意点たくさん。。

GAE関連

リージョンは一度設定すると変更できない

そのため、Firebaseで作成したプロジェクトだと、Firebaseで設定したものが反映される。 変更はできないので、必要があれば、別プロジェクトを作成するしかない。

Firebaseで利用しているプロジェクトだとGAEを停止できない

FirebaseでGAEを利用しているので、GAE停止したいなと思っても難しい。
GAEは最低1つはインスタンスを稼働していなければならず、
GAEの無効化をするとFirestoreにアクセスできなくなる。。

なので、一度上げてしまった場合、削除ができなくなるので、
アクセスがないとインスタンスが0になる設定で空のアプリをデプロイした対応した。

また、無効化してFirestoreが利用できなくなった場合、
有効化すれば、5分後くらいにFirestoreが使えるようになる。

GAEはdefaultサービスがないといけない

app.yamlでサービス名(service)を指定すると、複数のサービスをデプロイできるらしい。

ただ、defaultない状態で、deployすると以下のエラーが出る。

The first service (module) you upload to a new application must be the 'default' service (module). Please upload a version of the 'default' service (module) before uploading a version for the 'release' service (module). See the documentation for more information. Python: (https://developers.google.com/appengine/docs/python/modules/#Python_Uploading%%20modules) Java: (https://developers.google.com/appengine/docs/java/modules/#Java_Uploading%%20modules)

F1は自動スケーリングのインスタンス

GAEのインスタンスタイプには、2種類ある。

  • F系(F1,F2...。自動スケーリング)と
  • B系(B1, B2...。基本スケーリング/手動スケーリング)

無料枠のF1は自動スケーリングでは、自動でシャットダウンされる。 - インスタンスの管理方法  |  Node.js 用 App Engine スタンダード環境に関するドキュメント  |  Google Cloud

デフォルトのautomatic_scaling設定だと、期待してるインスタンスの常駐がされない。

そのため、min_instancesやmin_idle_instancesなどを設定が必要だが、
それぞれ特徴がある。

  • min_instances/max_instances
    • バージョンごとに有効。なのでトラフィック割当のないバージョンでも残ってしまう...
  • min_idle_instances/min_idle_instances

なので、上記のサンプルでは、以下のように設定して、
複数のインスタンスが立たないようにし、トラフィック割当が少ないものはインスタンスが0になるよう設定。

automatic_scaling:
  # インスタンスの最大数を指定。各バージョンで有効
  max_instances: 1
  # アイドル中のインスタンス最小数を指定。トラフィック割当が多いバージョンで有効
  min_idle_instances: 1

min_instancesを1にすると古いバージョンでも残り続け、
max_idle_instancesを1にするとアイドル1、稼働中1と2つのインスタンスが立ち上がってしまう。

ウォームアップ リクエストを使っても起動が遅い?

GAEでも、「別のバージョンのアプリを再デプロイするとき」や「リクエスト数が容量を超え、新しいインスタンスが作成されるとき」に読み込みリクエストが発生し遅くなってしまう。

この初期の立ち上げのレイテンシを回避するため、ウォームアップ リクエストの機能がある。
ウォームアップ リクエストを構成してパフォーマンスを改善する

ウォームアップ リクエストを有効にすると、デプロイ後に/_ah/warmupに対してGETリクエストを発行する。
そのため、NuxtのserverMiddlewareを追加して、リクエストを処理する関数を追加する。

app.yamlに、以下を追加して、有効化し、

inbound_services:
  - warmup

Nuxt側でこんな関数を用意して、

// serverMiddleware/warmup.ts
import { IncomingMessage, ServerResponse } from "http";

export default async function(req: IncomingMessage, res: ServerResponse) {
  res.end();
}

nuxt.config.tsで設定を追加した。

import { Configuration } from "@nuxt/types";

const config: Configuration = {
  // ...
  serverMiddleware: [
    { path: "/_ah/warmup", handler: "~/serverMiddleware/warmup.ts" },
  ],
};

export default config;

実際に試してみたところ、

  • /_ah/warmupへのリクエストを発行され、warmup.tsも実行されているが、
  • インスタンスにアクセスすると読み込みリクエストが発生するよう。。

また、warmup.ts/に対しリクエストする処理を追加してみたところ、
タイミングによっては、トラフィック割当が0のため、読み込みリクエストを発生させられないっぽい。。
ウォームアップ リクエストが 特別な読み込みリクエス であるからかもしれない。

さらに、ウォームアップ リクエスト自体が必ず呼び出されるものではないらしい。。

ウォームアップ リクエストは必ず呼び出されるとは限りません。代わりに読み込みリクエストが送信されることがあります。 ... すでにウォームアップされているインスタンスにリクエストを送信する「ベスト エフォート方式」が試行されます。

ウォームアップ リクエストを有効にする|ウォームアップ リクエストを構成してパフォーマンスを改善する

そのため、期待した動作をしない可能性が高い感じだっった。。

Cloud Build関連

buildにname: 'gcr.io/cloud-builders/gcloud'を使うとエラー

以下のドキュメントに従って設定したが、エラーに。。 ・App Engine へのデプロイ  |  Cloud Build のドキュメント  |  Google Cloud

ドキュメントが古いかも?
name: gcr.io/google.com/cloudsdktool/cloud-sdkを使って解決

versionの制限

GAEのversionに使える文字には制限があり、エラーになった。

versionは、小文字の英字と数字とハイフン。小文字の英字か数字。63文字までらしい。
versionにv2.0.2-6053688856cc6b05a3af8e65c73a3507f357483fを指定したら以下のメッセージが出た。。

ERROR: (gcloud.app.deploy) argument --version/-v: Bad value [v2.0.2-6053688856cc6b05a3af8e65c73a3507f357483f]: May only contain lowercase letters, digits, and hyphens. Must begin and end with a letter or digit. Must not exceed 63 characters.

この場合、bash形式の文字列操作を利用して、.-に置き換えるなどすれば、対応できたりする。

App Engine Admin APIを有効にしないとデプロイできない

権限や有効化していないと、以下のエラーが出る

ERROR: (gcloud.app.deploy) User [581217444506@cloudbuild.gserviceaccount.com] does not have permission to access app [tsundoku-web] (or it may not exist): App Engine Admin API has not been used in project 581217444506 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/appengine.googleapis.com/overview?project=581217444506 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

また、Cloud Buildの設定で、App Engine管理者を有効化していないと、
Cloud Buildでパーミッションエラーになる

App Engine へのデプロイ  |  Cloud Build のドキュメント  |  Google Cloud

Cloud RunとGAEを試してみた結果

Cloud Runを利用することにしようと思ってる。

ポイントは、以下の4点

  • FirebaseとリンクしているとGAEが停止できない
  • リージョンを変更できない
  • 複数のサービスを建てるときがめんどくさい(stagingとか作りたいため)
  • カスタムドメインを設定がやや複雑(stagingとか作りたいときにちょっと手間)

上記2つは別プロジェクトにすれば解決するけど、プロジェクトを増やすと管理が大変なので、
個人的には統一できて設定も楽なCloud Runが良さそうな感じ。

ただ、Cloud Schedulerで10分ごとにインスタンスを立たせる必要があるため、
実際やってみて費用面でどうかを確認していきたい感じ。

以上!!

参考サイト