GitHub Actions と Google App Engine でCI/CD

最近GitHub Actionsを触り始めました。
リポジトリで発生したイベントをトリガーに処理したり、JavaScriptでActionを作ってパッケージにして公開できたり、かなり遊べそうな感じなのですが、まずはベーシックな使い方としてCI/CDパイプラインを構築してみたので記録します。

アプリケーション

TypeScript(Node.js)で作ったTwitter botのアプリケーションを対象とします。

構成


name: Node.js CI


on:
  push:
    branches: [master]


jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        os: [ubuntu-latest]
        node-version: [12.x]
    steps:
      - uses: actions/checkout@v2


      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}


      - name: yarn install, and test
        run: |
          yarn
          yarn test
        env:
          CI: true
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2


      - name: yarn install and build
        run: |
          yarn
          yarn build


      - name: upload dist
        uses: actions/upload-artifact@main
        with:
          name: dist
          path: dist
  deploy:
    needs: [test, build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2


      - name: douwnload dist
        uses: actions/download-artifact@main
        with:
          name: dist
          path: dist


      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@master
        with:
          project_id: ${{ secrets.GCP_PROJECT_ID }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true


      - name: Setup Node.js
        uses: actions/setup-node@v1
        with:
          node-version: "12.x"


      - name: Load Env Variables
        run: node ./ci/load-env-variables.js
        env:
          APP_TWITTER_COSUMER_KEY: ${{ secrets.APP_TWITTER_COSUMER_KEY}}
          APP_TWITTER_COSUMER_SECRET: ${{ secrets.APP_TWITTER_COSUMER_SECRET}}
          APP_TWITTER_TOKEN_KEY: ${{ secrets.APP_TWITTER_TOKEN_KEY}}
          APP_TWITTER_TOKEN_SECRET: ${{ secrets.APP_TWITTER_TOKEN_SECRET}}
          APP_TWITTER_BOT_ACCOUNT_ID: ${{ secrets.APP_TWITTER_BOT_ACCOUNT_ID}}


      - name: Deploy App to GAE
        run: |
          gcloud app deploy
          rm -f app.yaml


  • masterブランチへのpushをトリガーにワークフローが起動します。
  • test, build, deployというJobによって構成されます。
  • test, buildは並列で実行されます。deployはtest, buildの完了をもって実行が開始されます。


test

jestを実行します。今回はユニットテストしか行っていませんが、静的解析ツールによるチェックなどを入れてもいいでしょう。
strategy.matrixに、テストを実行する環境の組み合わせ(OS, 言語のバージョン)を指定することで複数の環境でのテストを実行可能です。
今回はUbuntu, Node.12.xで指定しました。

build

yarn build(中身はtscでのコンパイル)で、コンパイル済みの成果物(distディレクトリ)を出力します。
distディレクトリはdeployステップで必要なので、artifactとしてアップロードしておきます。
生成されたdistはJob間で共有することができないためです。
アップロードされたartifactは、ワークフローの詳細画面からダウンロードすることもできるようです。

deploy

needsに、test, buildを指定しているため、test, buildのJobが完了してから実行されます。
buildでアップロードされたartifact(distディレクトリ)をダウンロードします。
次に、GCPのCloud SDKの初期化を行います。
初期化処理は、それ用のアクションが提供されているので使いましょう。
https://github.com/google-github-actions/setup-gcloud
GCP_SA_KEYには、GCP管理画面から取得できる秘匿情報のjsonをbase64でエンコードした文字列を指定します。
secretsはリポジトリのsettingsから設定可能です。
その後、secretsに設定された環境変数をapp.yamlに埋め込みます。
最終的にgcloud app deployするときに参照されるapp.yamlにアプリケーションで使用する環境変数を指定する必要があるのですが、APIキーなどはGitの管理下に置くことができません。
そのため、環境変数の値の部分だけを置換用の文字列で設定したapp.template.yamlを用意しておき、デプロイ直前に置換用の文字列をsecretsで取得した値で置き換えることで、バージョン管理に晒すことなくアプリケーションに環境変数を注入します。
このやり方は以下のブログで紹介されており、参考にさせていただきました。
GitHub Actionsから環境変数をマスクしてGAEにデプロイする
最後は、gcloud app deployで完了です。

デプロイが完了すると、GCP管理画面から新しいバージョンが追加されていることが確認できます。

作成したワークフロー

https://github.com/EringiV3/dev-article-reply-twitter-bot/blob/master/.github/workflows/node.js.yml

感想とワークフローがお蔵入りした理由

肝心のアプリケーションが仕様的にGAEとマッチしていなかったため、このワークフローはお蔵入りになってしまったのですが、GitHub Actionsを使ってGAEにデプロイしたくなるときがいつか来そうな気がしたので記事にしました。
Node.jsの実行環境としてのGAEはとてもお手軽で便利だと感じたので、手持ちの弾にしておきたいです。
スタンダード環境だと最初からNode.js(この記事書いた時点では12.x)が入っていて、言語のセットアップをすることなくNode.jsアプリケーションを動かすことができます。
しかもスタンダード環境はある程度までは無料で使える!
静的なサイト作成時やNext.jsを使うときは、それらに特化したプラットフォーム(NetlifyやVercel)を使ったほうが良さそうですが、素のNode.jsでなにかやりたいときはGAEかなり良さそうです。
ただ、GAEはデフォルトの自動スケーリング(https://cloud.google.com/appengine/docs/standard/nodejs/how-instances-are-managed?hl=ja)だと、HTTPリクエストが10分来ないとシャットダウンされてしまいます。
今回デプロイしたアプリケーションは、Twitterのタイムラインをストリーミングして特定の条件を満たすツイートが流れてきたらアクションするものだったので、アプリケーションは常に稼働状態である必要がありました。そのため、GAEの制約がある実行環境ではうまく動作しませんでした(デプロイされたサーバーにHTTPリクエストしてから10分間しか動かない)。
アプリケーションの仕様をよく考えず、実行環境の選定をしてしまった結果踏んでしまった間違いだと言えます。
セットアップの手間が増えますが、GCE(Google Compute Engine)でうまくできないか実験してみます。
それでもダメそうなら、おとなしくラズパイ買ってお家サーバー立てて運用しようと思います。。