SlackApp快速開発 - デプロイ(GAE with CircleCI)

概要

前回、BoltベースのSlackAppを開発するためのローカル環境を作りました。今回はそれを実際に使えるようにデプロイします。デプロイ先はGoogleAppEngine(以下GAE)で、最終的にCircleCIを利用して自動的にデプロイされることを目指します。SlackAppと快速開発と言いながらほとんどはGAEとCircleCIの話なのでSlackApp以外にも簡単に応用できます。

また、今回はGAEにデプロイするためGAE特有の事情にも触れますが、他のPaaSへのデプロイにも応用できる知見も多いと思います。

シリーズ:

なぜGoogleAppEngine

まずBoltでSlackAppを作る場合サーバーレスではなくNode.jsが動きサーバーとして扱えるものが必要です。自分でNode.jsが動くサーバーを立てて運用するのは少々面倒ですので、PaaSで考えてみます。そうなるとお手軽なのが、Heroku, Now, GAEあたりでしょうか。

中でもGAEは同じプロジェクトとしてFirestoreをDBとしてデータを保持しやすく、またCronでの定期実行も備えています。扱い方にもよりますがFirestoreを使ってGAEで運用しても基本的には無料の範囲で収まるはずです。

逆に言えば保持しておくようなデータの扱いやCronも使わない、もしくは別の仕組みを使って実現する、ということであればGAEでやるメリットはあまりありません。ただし、スラッシュコマンドなどは3秒以内にレスポンスを返す必要があり、インスタンスがスリープするようなものは注意する必要があります。

GAEにローカルからデプロイ

GAEのNode.js環境に関するドキュメントはこちら

最小限の設定としては

  • gcloud CLIをインストールしてセットアップ
  • GAEビルド用のコマンドをpackage.jsonに追加
  • app.yamlにデプロイ設定を記述
  • gcloud CLIでapp deployコマンドを叩く

だけです。

gcloud CLIのインストール

これは公式に案内されているとおり

です。が、macOSの場合は非公式のbrew cask経由でもインストール可能です。

$ brew cask install google-cloud-sdk

インストールが完了したら、Googleアカウントとプロジェクトのセットアップを行います。これも公式ドキュメントが一番正しくわかりやすいので紹介します。

GAE用のビルドコマンドを追加

前回ローカル開発環境ではprestart内でTypeScriptのビルドをしました。しかし、GAEは基本的にread-onlyのため、ビルドが成功しません。
これに対応するにはgcp-buildというGAE用のビルドコマンドを設定します。gcp-buildを通した場合は生成されるファイル郡は期待通りに配備されます。

package.jsonのscriptを以下のように書き替えます。

{
  "scripts": {
    "gcp-build": "tsc -p .",
    "prestart": "npm run gcp-build",
    "start": "node ./dist/index.js",
    "watch": "nodemon",
    "dev": "nf start",
  },
}

従来のビルドコマンドをgcp-buildに設定して、prestartからはgcp-buildコマンドを呼ぶようにします。「カスタム ビルドステップの実行  |  Node.js 用 App Engine スタンダード環境に関するドキュメント  |  Google Cloud」で紹介している方法と同一です。

余談ですが、GAE以外でデプロイするのであっても同じようにbuildみたいなコマンドにビルドを設定したほうが良いでしょう。見通しが良くなりますし、CI上で実行するケースもあるかもしれません。

app.yamlの設定

デプロイの方法についてはこちら

簡単に言えば、gcloud app deployコマンドを叩くと先の手順で設定されたgcloudのプロジェクトに対してapp.yamlの設定に従ってデプロイされる、というだけです。

最小設定はruntimeさえ指定すればデプロイできちゃいます。

runtime: nodejs10

と書くだけですね。

ただし、GAEのAlwaysFreeの対象はオートスケーリングされるF1インスタンスの実行時間ベースです。つまり無料に抑えたいのであればできる限りオートスケールしないような制御をすると安心できます(通常のSlackAppの運用であればそうそうスケールされることはなさそうですが)。

一例として僕の運用しているものを記載します。

runtime: nodejs10
instance_class: F1
automatic_scaling:
  min_idle_instances: automatic
  max_idle_instances: 1
  min_pending_latency: 3000ms
  max_pending_latency: automatic
  target_cpu_utilization: 0.95
  target_throughput_utilization: 0.95
  max_concurrent_requests: 80

デフォルト値の設定から調整してを極力1インスタンスで頑張るように設定している感じです。
各設定の詳細についてはこちら

また、無料対象については「GCP の無料枠 - 無料の長期トライアル、Always Free  |  Google Cloud」を参照してみてください。

デプロイ対象から外す設定

.gcloudignoreというファイルにデプロイ対象に含めないファイルを指定できます。このファイルはinitした場合すでにできてるかもしれません。書き方は.gitignoreと同様です。

ignoreしたいファイルの設定例は以下のとおりです。

.gcloudignore
.git
.gitignore
node_modules/
__tests__/
.circleci/
.github/
nodemon.json
Procfile
.env
serviceAccountKey.json
/dist/

基本的には開発用の設定ファイルを対象外にします。また、node_modulesもデプロイ時にinstallされるのでローカルからのデプロイ対象から外します。ビルド自体もデプロイ作業後に行なわれるため、ビルドされたものである/dist/ディレクトリ以下は含める必要がありません。

デプロイしてみるその前に

実際にデプロイしてみるんですが、前回ローカル開発環境の中ではSlackで利用するためのキーを秘匿するために環境変数にしました。GAEでも環境変数の設定が必要です。

GAEの場合、環境変数もapp.yamlに記載します。勘の鋭い方は「app.yamlはデプロイ設定なのでGit管理したい。でも環境変数を含めたファイルをGit管理対象にはしなくない。どうすれば……」と思うはずです。
これに対して解決方法はいくつかありますが、1つの解決例については後述します。今はまず「デプロイができて、それが正常に動作する」を目指しましょう。いったんベタ書きで記載します。

先程設定したapp.yamlの最後に以下のように追記(もちろんマスクしてある部分は実際に使うキーを記述)します。ここで使うキーは開発用Slackワークスペースではなく、本番用のSlackワークスペースです。

env_variables:
  SLACK_BOT_TOKEN: xoxb-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx
  SLACK_SIGNING_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

デプロイしてみる

$ gcloud app deploy app.yaml

を叩くだけです。成功したら、https://{プロジェクト名}.appspot.com/のようなGAEの公開URLが表示されます。特に設定を変更しない限りはこのURLは固定で、次にデプロイしても同じURLで公開されます。ということはSlack側で設定しているURLは一度設定した以後変更しなくて良いということです。もちろんGAE側の設定で独自ドメインを使うこともできます。

ちなみにGAEではデプロイ時にnpm installされた後、自動でnpm run startが走るようになっています。それによって自動的にビルドされ、Boltサーバーが起動するはずです。

CircleCIでデプロイする

デプロイの度に一々手元でデプロイコマンドを叩くのは面倒なのでCIでできるようにします。また、CIでデプロイできるようにしておくことで他にコントリビューターが居る場合でも自分が必要以上に関与しなくてよくなります。

今回CIはCircleCIを使うことにしました。CircleCIの概要と詳細については以下をご参照ください。

まずはGitHubにリポジトリを作り、CircleCIと連携させておきます。その際に後ほど説明する手順を行なうまでは、環境変数の含まれたapp.yamlをpushしないようにご注意ください。

CircleCIを設定する

Orb

紹介したドキュメントにもあるように、CircleCIの設定は.circleci/config.ymlに書きます。

CircleCIではOrbと言う再利用可能な共通設定の型が用意されているものがあります。今回はgcloud用のOrbを使います。

そして、このOrbを使ってGAEをデプロイするには必要な情報をCircleCI側のプロジェクトの環境変数として設定する必要があります。
必要な環境変数は3つで

これらを環境変数の使い方 - CircleCIを参考にCircleCIからプロジェクトの環境変数として設定します。GCLOUD_SERVICE_KEYはJSONの内容をそのままコピー&ペーストで入れてOKです。

Slack用環境変数もCircleCIで扱う

さて、さきほどapp.yamlにベタ書きしたSlackの環境変数ですが、このままだとapp.yamlをGit管理対象に含められません。少々強引ですがapp.yamlからCircleCIにSlackの環境変数を扱わせるようにします。

gcloudの環境変数と同様に、CircleCIのプロジェクトの環境変数にSLACK_BOT_TOKENSLACK_SIGNING_SECRETを設定します。app.yamlからはenv_variablesごと削除します。

そして後程記載するCircleCIでデプロイ時にデプロイコンテナ内でapp.yamlにシェルスクリプトで追記します(多少強引で泥臭い対処なのでもっと良い方法があれば教えてほしいところ)。

PullRequestをマージしたときにデプロイする

CircleCIはデフォルトではpushされたときに走ります。これをPullRequestがマージされたタイミングでのみデプロイが自動実行されるように制御します。通常良くあるGit運用を踏襲しmasterブランチはトピックブランチのマージコミットのみ、リリース可能な状態、というルールを守るようにします。

具体的にはCircleCIにはfilter機能を使います。特定のブランチやタグに関する変更時のみトリガーされる設定です。今回の場合はmasterに変更があったときのみデプロイジョブが実行されるようにすればOKです。

設定を書く

以上を踏まえて実際にconfig.ymlを書きます。

version: 2.1
orbs:
  gcp-cli: circleci/gcp-[email protected]

jobs:
  deploy:
    working_directory: ~/repo
    docker:
      - image: google/cloud-sdk:latest

    steps:
      - checkout
      - run:
          name: Overwrite env variables
          command: |
            echo "env_variables:" >> app.yaml
            echo "  SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}" >> app.yaml
            echo "  SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET}" >> app.yaml
      - gcp-cli/initialize
      - run:
          name: Deploy to Google App Engine
          command: |
            gcloud app --quiet deploy app.yaml

workflows:
  version: 2
  deploy:
    jobs:
      - deploy:
          filters:
            branches:
              only:
                - master

ステップを見てみましょう。

checkout:
これは最新の状態にするためのもので特に説明はいりませんね。

Overwrite env variables:
前述のとおり、app.yamlに対してシェルからCircleCIに登録した環境変数を用いて追記しています。良い方法には思えませんが、この方法であればapp.yamlをGit管理下に置きつつ、GAE内で使う環境変数を秘匿したままにできます。

gcp-cli/initialize:
orbで定義されているものです。これによって、gcloudでデプロイする準備をします。

Deploy to Google App Engine:
ほぼローカルから叩いたときと同じデプロイコマンドです。違いは--quietオプションで、デプロイ時に入力を求められる状況が発生しても無視してデプロイを進めることができるオプションです。

そして最後のほうにfilterでmasterブランチのみを対象としています。PullRequestをマージすれば当然リモートのmasterブランチが進みますのでこのデプロイが走ります。

感想

最初はいろいろ設定が上手くいかず苦労しました。filter部分を外せばトピックブランチにpushしても設定したジョブが走るので、まずはそれで試して期待通りに行くようになってfilterをつけるような試行錯誤しました。

また当初はgcp-buildコマンドの扱いが特別なことを知らずにGAEではビルドできないものだと思っていました。その時はCircleCI内でビルドして、ビルド結果をデプロイするようにして一応上手くいっていましたが。

ちなみにマージしたときにデプロイしたくない場合(例えばREADMEの更新のみの場合)はマージコミットのメッセージに[skip ci]と書けば大抵のCIはトリガーされません。いろいろ考えましたが今のところはその対応で十分に感じています。

とまあいろいろ苦戦したものの最終的になんとかなりました。いろいろ作っていて思うのはデプロイファーストという概念もあるように、デプロイの確立を早めにやっておくと後が楽だなぁとよく思います。

SlackApp快速開発 - BoltでHTTPリクエストを受け、Cronで定時実行SlackApp快速開発 - ローカル開発環境構築(with Bolt + TypeScript)