Nuxt+SSR+TypeScript なアプリケーションを GitHub Actions で GAE に自動デプロイする

# 対象読者

- `Nuxt.js` で SSR したときのお手軽デプロイ方法を知りたい
- `Google App Engine(GAE)` に Nuxt をデプロイしたい
- `GitHub Actions` で GAE に自動デプロイしたい

# 概要

`Nuxt.js` のデプロイ先に悩むことが多く、特に SSR した場合に手軽にデプロイできる方法を探していました。

静的サイトなら Netlify で十分なのですが、SSR だったり、動的にコンテンツが増える状況において、Netlify では役不足です。でも、`mater merge` 時の自動デプロイとカスタムドメインはありがたい存在なので、他のデプロイ先に移行しつつ、Netlify のメリットを受け継げる方法を探しました。

調査の結果、`GAE` への `Nuxt.js` のデプロイによって、自動デプロイとカスタムドメインの要件も含めすべて満たすことができました。

# `GCP` と `GAE` の設定

まずは `GCP` および、 `GAE` の初期設定について解説し、その後デプロイ方法について、最後に `GitHub Actions` について説明していきます。

## gcloud コマンドのインストール

自身の PC に、下記ページを参考に `gcloud` コマンドをインストールします(※MacOS の場合)。

https://cloud.google.com/sdk/docs/quickstart-macos?hl=ja

インストール後は PATH を通しておくと便利です。

## `GCP` Project の作成

続いてデプロイ先になる `GCP` のプロジェクトを作ります。今回の場合、僕が作ってみたサービスでは Firebase を利用しているので、対応する `GCP` プロジェクトはすでに作られていました。

`GCP` プロジェクトが作成できたら、課金を有効にしておきます。

そのあと、`gcloud init`コマンドを実行して、作成した `GCP` プロジェクトを利用できる状態にします。

## app.yaml の設置

続いて、GAE の設定ファイルである`app.yaml`をプロジェクトルートに設置します。

以下のような内容で作成します。とりあえずこれで動いていますが、`Node.js` のバージョン、および静的ファイルやインスタンスの大きさなどは適宜調整してください。

公式ドキュメントは[こちら](https://ja.nuxtjs.org/faq/appengine-deployment/)

```yml
runtime: nodejs12
instance_class: F2
handlers:
  - url: /_nuxt
    static_dir: .nuxt/dist/client
    secure: always
  - url: /(.*\.(gif|png|jpg|ico|txt))$
    static_files: static/\1
    upload: static/.*\.(gif|png|jpg|ico|txt)$
    secure: always
  - url: /.*
    script: auto
    secure: always
env_variables:
  HOST: "0.0.0.0"
  NODE_ENV: "production"
```

# `Nuxt.js` アプリケーションの作成とビルド

## アプリケーションの作成

今回は `Nuxt.js` （バージョン 2.9 以降）で `TypeScript` を利用しており、かつ、nuxt.config.js などの設定ファイルも `TypeScript` 対応しているものとします。僕は 2.10.2 と 2.11.0 でデプロイできることを確認済みです。

また、SSR 対応している前提なので、`mode: 'spa'`ではありません。

```javascript:nuxt.config.ts
import { Configuration } from '@nuxt/types'

const config: Configuration = {
    ...
```

`package.json` のビルドコマンドも `nuxt-ts` を利用しています。

```shell

"build": "nuxt-ts build",
```

## ビルド

ローカルで`npm run build`を実行してビルドが通る状態にしておきます。

```shell
$ npm run build
...
ce849990709e675b2f0d.js    7.4 KiB      13  [emitted] [immutable]
e1e7424856593dd60cc8.js   20.1 KiB       8  [emitted] [immutable]  pages/index
ef4de0a4edcd2c1e3068.js   7.49 KiB       0  [emitted] [immutable]
              server.js   32.6 KiB       2  [emitted]              app
   server.manifest.json  899 bytes          [emitted]
Entrypoint app = server.js
```

# GAE への Nuxt アプリケーションのデプロイとカスタムドメイン

ビルドが通ったら、続いて`gcloud app deploy`コマンドを実行します。

```
$ gcloud app deploy --project={プロジェクト名}
```

最初のデプロイはファイル数が多いので案外時間がかかります。また、途中で yes/no をプロンプトで訊かれるので、忘れず yes を入力しましょう。

## デプロイ後エラーになったとき

僕の記憶ではデプロイ自体に失敗したことはなかったはずですが、デプロイ後にページを開くとエラーになる場合があります。

基本的にはエラーの原因になったライブラリを`devDependencies`から`dependencies`に移行すれば解決しました。

`@nuxtjs/style-resources`や`@nuxt/typescript-build`が最初`devDependencies`に入っていたので、実行時にエラーを起こしていました。`package.json`を編集し、再度`npm run build`からやり直すことで、動作しました。

この時点で、`https://{プロジェクトID}.appspot.com`でページを開くことができるはずです。

## デプロイした環境にカスタムドメインを設定する

GAE にはカスタムドメインを紐付けることができます。

僕は AWS の `Route53` で日頃ドメインを取っているので、今回も `Route53` で取得したものを紐付けました。こだわりがなければ Google Domain で取得したほうが取り回しはいいかもしれません。

概ね、下記記事の通りで紐付けできました。

https://consulting.blog.rakumo.com/entry/20180109/customdomain

# GitHub Actions を設定し、master merge したときに自動でデプロイ

以上で、手動でデプロイすることは可能になったので、次は `master merge` したときにデプロイを実現します。

`GitHub Actions` 自体は、リポジトリ内に`.github/workflows/gcloud.yml`といった名称のファイルを置いてリモートに Push するだけで設定可能です。

`GitHub` の `Actions` タブからポチポチするだけで、勝手に yml が置かれて、Feature ブランチに Push されるので本当にすぐ設定は終わります。

問題は yml の中身です。僕は下記の yml で動作しました。

```yml:.github/workflows/gcloud.yml
name: Google App Engine Deploy

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.11.0]

    steps:
      - uses: actions/checkout@v2
      - name: Setup gcloud environment
        uses: GoogleCloudPlatform/github-actions@0.1.1
        with:
          # The version of the gcloud SDK to be installed.  Example: 275.0.0
          # The service account email which will be used for authentication.
          service_account_email: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}
          # The service account key which will be used for authentication.
          service_account_key: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_CREDENTIALS }}
      - name: Run tests with ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: npm install, build
        run: |
          npm install
          npm run build
        env:
          APIKEY: ${{ secrets.FIREBASE_APIKEY }}
          AUTHDOMAIN: ${{ secrets.FIREBASE_AUTHDOMAIN }}
          DATABASEURL: ${{ secrets.FIREBASE_DATABASEURL }}
          PROJECTID: ${{ secrets.FIREBASE_PROJECTID }}
          STORAGEBUCKET: ${{ secrets.FIREBASE_STORAGEBUCKET }}
          MESSAGINGSENDERID: ${{ secrets.FIREBASE_MESSAGINGSENDERID }}
          APPID: ${{ secrets.FIREBASE_APPID }}
          MEASUREMENTID: ${{ secrets.FIREBASE_MEASUREMENTID }}
      - name: GAE deploy
        run: gcloud app deploy app.yaml --project {プロジェクト名}
```

## ポイント

上記の yml を作成するためのポイントはいくつかあります。

### on push で master ブランチを指定する

`master merge` したときにデプロイしたいので、動作するトリガーは master への Push とします。

### GoogleCloudPlatform/github-actions を利用する

公式がサービスアカウントのクレデンシャルなどを読み取ることを前提とした設定を公開してくれているので、ありがたく利用します。

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

サービスアカウントを `GCP` コンソールから作成します。これは、`GitHub Actions` にデプロイ権限を渡すために必要な作業です。`GitHub Actions` は第三者のアプリケーションになるので、そこからデプロイを実行するためには適切な権限が必要なので、その権限を付与したサービスアカウントを作成し、そのクレデンシャルを GitHub の Secrets に設定のうえ、yml で読み取るという流れです。

GCP のコンソールから IAM→Service Accounts へアクセスします。

<img width="258" alt="スクリーンショット 2020-02-24 13.05.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/b617163e-e726-42a2-a5c5-3b40dfbebc52.png">

画面内の「＋サービスアカウントを作成」ボタンを押し、サービスアカウント名には「GitHub Actions」等の命名をつけます。

また、作成後は、下記の権限を付与します。僕は下記の権限を付与したところで無事にデプロイができるようになりましたが、過不足あればぜひおしえてください。できれば思考停止で管理者権限付与などはしたくないものです。

<img width="534" alt="スクリーンショット 2020-02-24 13.04.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/5c34d7c8-f0ce-9dad-b7de-f8f584155c2d.png">

サービスアカウントの作成時に、キー（クレデンシャル）の作成ができるところがあるので、これをクリックし、JSON 形式でダウンロードしてください。

<img width="543" alt="スクリーンショット 2020-02-24 13.10.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/bd919c45-7224-aa35-df66-57c6e78d5078.png">

以上でサービスアカウントの作成が終わりです。

### キーは base64 エンコードする

さて、https://github.com/GoogleCloudPlatform/github-actions/tree/master/setup-gcloud に記してあることですが、上記でダウンロードしたクレデンシャルの JSON ファイルは、`Base64` エンコードしたうえで `GitHub` の `Secrets` に保存します。

ローカルで `base64` コマンドを叩くなどしてエンコードしてください。

> service_account_key: (Optional) The service account key which will be used for authentication. This key should be created, encoded as a Base64 string (eg. cat my-key.json | base64 on macOS), and stored as a secret.

### GitHub Secrets 設定

`GitHub` の `Setting` タブから、`Secrets` 画面に移動し、上記の yml ファイルに指定されている「`GOOGLE_SERVICE_ACCOUNT_CREDENTIALS`」などの環境変数を保存していってください。僕がやっていたプロジェクトでは `Firebase` を利用しているので、`Firebase` 関連の情報も全部 `Secrets` に載せました。

<img width="759" alt="スクリーンショット 2020-02-24 13.14.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/223ee4c0-7458-cc1f-4e77-c8ca5ca3dd6c.png">

---

以上で設定は終わりです。実際に `master merge` してみると、`Actions` タブからデプロイが動いていることを確認できますし、もし失敗してもログが出ているので、だいたいなんとかなると思います。

<img width="802" alt="スクリーンショット 2020-02-24 13.16.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/7aed29d2-f172-4ada-413e-13ad9ea55b85.png">

# 補足
```
ERROR: (gcloud.app.deploy) Error Response: [7] Access Not Configured. Cloud Build has not been used in project XXXXXXX before or it is disabled.
```

というエラーがデプロイ時に出たら、その後に表示されているリンクをクリックして`Code Build`を有効にしてください。

# まとめ

最後の `GitHub Actions` の設定がもっとも難しいところですが、一度やってしまえば、SSR 対応の Nuxt アプリケーションを `master merge` 時に自動デプロイでき、しかもカスタムドメインで SSL 対応も終わっているので大変便利だと思います。

簡単な用途であれば、SSR 対応のアプリケーションで Netlify とほとんど近しい DX を再現できたと思います。