GitHub Actions を利用した NestJS アプリケーションの Google AppEngine への自動デプロイ

この記事は NestJS Advent Calendar 2019 15 日目の記事です。

本日は NestJS アプリケーションのデプロイ先として最も有力な PaaS 環境である Google AppEngine へと、新興の CI/CD サービスである GitHub Actions を利用して自動デプロイを実現してみます。

## NestJS のデプロイ環境について

NestJS のデプロイ先はたびたび話題にあがります。最終的にはケースバイケースなのでこれ！という形で断定はできませんが、少なくとも大抵の場合は適当な CI サービス + AppEngine で問題ないかと思います。

以下に参考程度のメモをまとめておきます。

### PaaS or FaaS

※ 簡単な環境に関する考察ですが、読み飛ばしていただいて問題ありません ※

昨今の Node.js 実行環境といえば、Google AppEngine のようなアプリケーション単位のデプロイ環境としての PaaS でのサービス提供と、AWS Lambda をはじめとした関数単位でのデプロイ環境としての FaaS 環境のどちらかが主流です。

FaaS 環境は PaaS 環境と比較し、より手軽でメンテナンスフリーなので可能であれば導入したいところです。一方で PaaS 環境は立ち上がり速度や最大許容スペック、(AppEngine Flexibleなど)環境によっては Web Socket などを貼ることができる点も魅力です。

NestJS は、[開発者の所属する企業にて Azure Functions での利用記事が存在する](https://trilon.io/blog/deploy-nestjs-azure-functions) など、幾分か FaaS 環境に適合使用する流れがあります。一方で、Node.js アプリケーションを多少無理やり FaaS に乗せる場合いくつか考えなければならないことがあります。

一つにコールドスタートの許容とサーバー起動時の所要時間の把握。

FaaS 環境は頻繁にアプリケーションを立ち上げては落とす環境となります。その特性を把握した上で利用する場合は問題ありませんが、Express や koa と比較して立ち上がりの遅い NestJS との組み合わせによる遅延が要件にマッチするか、高パフォーマンスを要求されないかなどを考慮した上で選択する必要があります。

もう一つがサーバーレス向けの設定変更です。

サーバーレス環境で Node.js アプリケーションを動かす場合、 [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) などのブリッジ系のライブラリや、クラウド向けの設定をいくつか用意しつつも、ローカルでは通常通りサーバーを立てての開発となる場合が大半です。

ある程度クラウドネイティブになれたメンバーがいる場合などは問題になりづらいですが、ある程度クラウド慣れしている場合でないとおすすめしづらい部分も多くなります。

基本的にはひとつ目の理由がクリアできているかどうかで FaaS か PaaS を選択、その上でもしふたつ目がクリアできそうな場合に限り FaaS 環境を利用する形で良いかと思います。

「NestJS のサーバーどうすればいいですか？」というざっくりとした問に対しては、基本的は PaaS 環境で良いという認識です。

FaaS は本来クラウドネイティブを実現するためのツールです。そこに HTTP リクエストが中心のスケールアウトよりスケールアップが向きそうなサーバープログラムを配置するのは、あくまでも自分で選定できる人がプロジェクト条件に合致した場合にやることで効果を発揮するものかと思います。

## GitHub Actions から AppEngine へとデプロイする

というわけで選定についてはここまでとして、実際にデプロイしてみましょう。

### 事前準備について

以下に事前準備事項をまとめます。

#### サンプルプロジェクト

サンプルは以下となります。

https://github.com/nestjs-jp/advent-calendar-2019/tree/master/day15-actions-and-appengine

#### 必要となる準備

事前に以下の準備をお願いいたします。

- GitHub アカウントと適当なレポジトリの作成
- GCP アカウント及びプロジェクトの作成
- AppEngine の有効化

### プロジェクトの準備

プロジェクト準備として、AppEngine の初期化だけではなく、API の有効化も必要となります。

以下の URL から、API の有効化を行ってください。

https://console.cloud.google.com/apis/api/appengine.googleapis.com/overview

<img width="1200" alt="スクリーンショット 2019-12-15 22.54.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/4a51b6b2-258e-09fd-a5bc-ea65c2a62849.png">

### プロジェクトの初期化

作業に入ります。普段通り CLI から初期化しておいてください。

```terminal
$ nest new day15-actions-and-appengine
```

### GitHub Actions の導入

GitHub Actions は、 GitHub が提供する CI サービスとなります。Docker イメージをベースとしたデプロイ環境と、基本無料の料金設定が個人利用にはぴったりです。

CircleCI や Travis CI でも問題ありませんが、それらと比較して単一の GitHub アカウントで完結するため、今回はこちらからデプロイしてみます。

https://github.co.jp/features/actions

#### ビルドワークフローの追加

今回は後でデプロイするため、Deploy のワークフローを作ることとします。

`.github/workflows/deploy.yml` を作成し、以下のように記述します。

```deploy.yml
name: Deploy to AppEngine

on:
  push:
    branches:
      - master

env:
  PROJECT_ID: 'nest-advent-calendar-2019'

jobs:
  deploy:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1
      - name: Cache node modules
        id: cache
        uses: actions/cache@v1
        with:
          path: node_modules
          key: v1-dependencies-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            v1-dependencies-
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Build Application
        run: |
          yarn
          yarn build
```

設定はこれだけです。このように記述することで、Docker の Node.js イメージを利用し、 NestJS せいのアプリケーションのデプロイができます。

これが作成できたら、適当なコミットを打って push してください。GitHub レポジトリの Actions からビルドログが確認できます。

<img width="1200" alt="スクリーンショット 2019-12-15 22.59.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/e3656576-fbf8-ebaf-ad8a-fca9aaaf7196.png">

Build Application が成功したら、これで初期化処理は成功です。

### AppEngine へのデプロイ

次に、 AppEngine へのデプロイ設定を GitHub Actions へと追加します。

#### 本番用ファイルの指定

まずはAppEngine 用の設定をいくつか追加します。

- package.json
- .gcloudignore
- app.yaml
- main.ts

を変更していきます。

既存設定への変更として package.json へと main の指定を追加します。 AppEngine は、 main に指定されたファイルをデフォルトでは `node ${filename}` の形で実行します。

```diff:package.json
+++   "main": "dist/main.js",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "tslint -p tsconfig.json -c tslint.json",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
```

これでビルドされた成果物を実行するようになります。

さらに本番環境用のファイル群を AppEngine 環境へとアップロードするために、 `.gcloudignore` を作成し、以下のように設定を追加します。

```.gcloudignore
node_modules
```

続けて app.yaml を追加。今回は静的ファイルホストなどはないので、全てをインスタンスに向けます。

```app.yaml
runtime: nodejs12
env: standard

handlers:
- url: /.*
  secure: always
  redirect_http_response_code: 301
  script: auto
```

最後に AppEngine でのポート割当に対応するため、 main.ts を書き換えて 3000 番ポート固定を解除します。

```main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000);
}
bootstrap();
```

#### GitHub へのサービスアカウントの鍵登録

続いて AppEngine の設定を GitHub レポジトリに登録します。

https://console.cloud.google.com/iam-admin/serviceaccounts からサービスアカウントの鍵を取得してください。

<img width="1282" alt="スクリーンショット 2019-12-15 23.39.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/f16279c3-4323-589f-bc7a-561a9b3de289.png">

ダウンロードした鍵を、クリップボードにコピーしておきます。

```terminal:terminal
cat nest-advent-calendar-2019-0d7124781.json | pbcopy
```

続いて GitHub の Settings から Secrets に行き、 JSON の中身をそのままコピーします。

<img width="1282" alt="スクリーンショット 2019-12-15 23.41.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/58470ce2-bb7f-52d7-7d6d-537bf5acdd9b.png">

これでサービスアカウントの認証情報を、Encryption した状態で GitHub が保持してくれるようになりました。

#### GitHub Actions の設定ファイルの更新

GitHub Actions 側にも設定を追加します。執筆時の Google Cloud SDK の最新版イメージ v273 を利用します。追加された項目は、「環境変数に格納したサービスアカウントの出力」および「AppEngine へのデプロイ」です。

PROJECT_ID は適宜変更ください。

```deploy.yml
name: Deploy to AppEngine

on:
  push:
    branches:
      - master

env:
  PROJECT_ID: 'nest-advent-calendar-2019'
  GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}

jobs:
  deploy:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1
      - name: Cache node modules
        id: cache
        uses: actions/cache@v1
        with:
          path: node_modules
          key: v1-dependencies-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            v1-dependencies-
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Build Application
        run: |
          yarn
          yarn build
      - name: Copy Service Account from Environment
        run: |
          echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
      - name: Use cloud-sdk
        uses: docker://google/cloud-sdk:273.0.0
      - name: Deploy
        run: |
          gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
          gcloud app deploy --quiet --project $PROJECT_ID
```


これで準備が整いました。適当にコミットを追加し、 push しましょう。

<img width="1243" alt="スクリーンショット 2019-12-15 22.45.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75633/72d0df04-a4f1-7740-5ab1-789f7561b9b4.png">

デプロイが完了すると、アクセス可能になります。〆にデプロイログに表示されたサーバーへと実際にリクエストを送信してみて、問題なければデプロイ完了です。

```terminal:terminal
> http get https://nest-advent-calendar-2019.appspot.com/
HTTP/1.1 200 OK
Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
Content-Length: 12
Content-Type: text/html; charset=utf-8
Date: Sun, 15 Dec 2019 13:37:16 GMT
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Server: Google Frontend
X-Cloud-Trace-Context: 58f02b1a41c0bed5d17171d352dd7df0;o=1
X-Powered-By: Express

Hello World!
```

これで master へとコミットが push されるたびに本番デプロイが行われる環境が構築できました。

## おわりに

アドベントカレンダーも折り返しに来たので、そろそろ学習用プロジェクトをデプロイする人も出てきそうかな。と思い AppEngine へのデプロイについて紹介しました。

今後アドベントカレンダーで AWS Lambda や Firebase Functions などの FaaS 環境でのデプロイも取り扱うかもしれませんが、その際も CI / CD は GitHub Actions で行ってみるなどをやってみると面白いかと思います。