前回のあらすじ

  • GatsbyをFirebase Hosting にデプロイした
  • 独自ドメインでアクセスできるようにした

この記事ですること

  • Github Actionsを使ってみる
  • Gatsbyのデプロイをデザイン変更時(GithubにPush時)に自動で行う
  • Gatsbyのデプロイをブログ記事投稿/編集時にも自動で行う

Github Actions

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

Githubが提供しているCI/CDツール。
特に何もしなくてもリポジトリにタブが付いているので手軽に使える。
Pushやプルリクに合わせてビルドしたりデプロイしたりが比較的簡単にできる。
今回はCircleCIとか外部のツールを使う理由もなかったのと、一回触ってみたかったという理由で使ってみた。

Push時にデプロイ

まずはデザインの変更などでGatsbyのソースに変更を加えてGithubに反映した時に、Firebaseに自動デプロイする仕組みを作っていこう。

まずGithubの自分のGatsbyリポジトリにアクセスする。
そしてタブからActionsを選ぶとアクションの例みたいなのが並んでいる画面が出てくる。

画面上にSkip this and set up a workflow yourself→とあるところから独自のアクションを作っていく。

# This is a basic workflow to help you get started with Actions
name: Deploy Gatsby to Firebase Hosting

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js 12.x
      uses: actions/setup-node@v1
      with:
        node-version: '12.x'

    - name: Install dependencies
      run: |
        NODE_ENV=production
        yarn --frozen-lockfile
    
    - name: Build Gatsby
      run: yarn build
      env:
        API_URL: ${{ secrets.API_URL }}

    - name: Setup Firebase CLI
      run: npm install -g firebase-tools
    - name: Deploy Firebase
      run: yarn deploy --token=${{ secrets.FIREBASE_TOKEN }}

書かれている内容以外を詳しく知りたい人はここを読むこと。
https://docs.github.com/ja/actions/reference/workflow-syntax-for-github-actions

最初に書かれているのがこのCI/CD(=ジョブ)をいつ動かすのかだ。
Pushとプルリクエスト時に動かす設定だが、ここにプルリク送る人はいないのでPushだけでいいとおもう。
ブランチはmaster。
個人開発だし特に切る理由もない。
チームならブランチ毎に設定変えたり出来そう。

次にジョブの実行環境を書いていく。
名前はbuildubuntu-latestで動かす。
stepsからが実行する内容になる。
まずはactions/checkout@v2を使ってリポジトリをチェックアウトする。
https://github.com/actions/checkout
usesを使うと他の人が作ったアクションを実行できる。

同じようにactions/setup-node@v1を使ってnodeの12系をインストール。
ステップに名前をつけると、実行時に見やすい。

次はyarn --frozen-lockfileで依存関係のインストール。
ローカルとCIで挙動を変えたくないので更新がかからないようにする。

そしてGatsbyのビルド。
環境変数として本番のAPI_URLが必要なので予めリポジトリのSettingから登録しておく。

最後にFirebase Hostingにデプロイ。
firebase-toolsをなぜかnpmで入れて…(yarnでいいと思うので後で直す)
CI用のトークンでデプロイ。

これでPush時に自動でデプロイされる仕組みが完成した。

Github Actionsで使うSecrets登録

ここから登録。以上。

CIで使うFirebase Tokenの取得方法

https://firebase.google.com/docs/cli?hl=ja#cli-ci-systems

firebase login:ci

ログインしたらトークンが出てくる。以上。

ブログ投稿時にデプロイ

さて、Push時にデプロイされることにはなったが、記事を更新した際にもサイトを最新版にしたいはずだ。
そこで、Github ActionsにはWebhookをトリガーにアクションを実行できる機能もある。
https://docs.github.com/ja/actions/reference/events-that-trigger-workflows#manual-events
https://docs.github.com/ja/rest/reference/actions

これを使ってStrapiでの更新時にジョブを走らせる仕組みを作る。

とりあえずGithubに対してこんな感じで投げればいいらしい。

$ curl \
  -X POST \
  -H "Accept: application/vnd.github.v3+json" \
  -H 'Authorization: token <Githubのトークン>'
  https://api.github.com/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches \
  -d '{"ref":"master"}'

owner: GithubID
repo: リポジトリ
workflow_id: ワークフローのID

Githubのトークンがない状態でしようとすると、"message": "Must have admin rights to Repository." って怒られるので作ろう。

ユーザーのSettingのメニュー左下、Developer settingsからPersonal access tokensで作れる。
権限はread:repo_hook, repoで一応通ってるけどもっと減らせるかもしれない。

ワークフローのIDは以下のAPIから取得できる。

$ curl \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/{owner}/{repo}/actions/workflows

これでコマンドラインからジョブの実行が出来るようになった。
Strapiと連携させていこう。
Strapiには更新時に通知を飛ばせる機能があり、要は特定URLにPOSTしているだけなので使えそうだ。
画面を見てみる。

URLとヘッダーが設定できる。
あれBodyは…?

実はBodyはStrapi側が作ったものが投げられるらしい。
さて、ここでGithub側に投げたいcurlを思い出してみよう。
bodyにブランチを指定している。
これは無理なのでは…?
案の定URLとヘッダーを指定してもエラーを返された。
余計なbodyがあるのも駄目らしい。

ここで諦めると利便性が最悪なブログシステムになってしまうので、やり方を考える。
StrapiはPOSTを投げれるけど形式は選べない。
Githubは特定の形式のPOSTでジョブを動かす。
つまり、StrapiのPOSTを受けてGithubに指示を出す中間管理職を作ってあげればいいということになる。

それがこれ。

https://github.com/Tim0401/dreamer-strapi-webhook

ただのnodeサーバーでアクセスされたらGithubにデプロイ指示を出すだけ。

index.ts

import * as http from "http";

var requestMsg = require('request');

var headers = {
	'Authorization': 'token ' + process.env.GITHUB_TOKEN,
	'Accept': 'application/vnd.github.v3+json',
	'User-Agent': 'dreamer-gatsby'
};

var dataString = '{"ref":"master"}';

var options = {
	url: process.env.WEBHOOK_URL,
	method: 'POST',
	headers: headers,
	body: dataString
};

function callback(error, response, body) {
	if (!error && response.statusCode == 200) {
		console.log(body);
	} else {
		console.log(response);
	}
}

class Main {
	constructor() {
		// httpサーバーを設定する
		const server: http.Server = http.createServer(
			(request: http.IncomingMessage, response: http.ServerResponse) =>
				this.requestHandler(request, response));
		// サーバーを起動してリクエストを待ち受け状態にする
		server.listen(process.env.PORT);
	}

  /*
  * サーバーにリクエストがあった時に実行される関数
  */
	private requestHandler(
		request: http.IncomingMessage,response: http.ServerResponse): void {
			
		requestMsg(options, callback);
		response.end('Hook Github Actions');

	}
}

const main = new Main();

こいつにStrapiから更新通知を投げてあげればいいわけだ。
このサーバーどこに置こう…

今回はStrapiの補助的な役割ということでStrapiコンテナのサイドカーとして走らせることにした。
というわけでStrapi-deployment.yamlを編集して構成していく。

      containers:
      - name: strapi-container
      ...etc
      - name: strapi-webhook-container
        image: tim0401/dreamer-strapi-webhook:latest
        env:
        - name: PORT
          value: "1338"     
        - name: WEBHOOK_URL
          valueFrom:
            secretKeyRef:
              name: secret
              key: webhook-url
        - name: GITHUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: secret
              key: github-token

長いので前に紹介した分はカット。
全文見たい場合はリポジトリにどうぞ。

さっきの簡単なnodeサーバーをImageにして使っているのがtim0401/dreamer-strapi-webhook:latestになる。
ビルド時にPORTを環境変数としてDockerhubで渡しているので、それと同じPORTをこちらのENVでも指定する。
WEBHOOK_URLGITHUB_TOKENsecret.yamlに設定値を追加しておく。

これでStrapiと同じPodで中間管理職が走るようになった。
Pod内のコンテナ間通信はlocalhostでいけるので、Strapiからこいつにアクセスしたい場合は、localhost:1338を使うといいわけだ。

Podsを見てみると2/2になっていて、コンテナが2個入っているのが分かる。

strapi-deployment-754494fd88-74bd5   2/2     Running     0          7d20h
strapi-deployment-754494fd88-dcft2   2/2     Running     0          7d20h
strapi-deployment-754494fd88-smf2f   2/2     Running     0          7d20h

これでStrapi更新時に、通知先URLとしてlocalhost:1338を指定すれば、アクセスを受けたnodeサーバーがGithubのAPIを叩いてデプロイが始まる。
無事に記事更新時に自動でコンテンツを更新することができるようになった。

これで一旦ブログ構築は区切りとなる。
とはいえこの記事を書いている間もちょっとずつアップデートしていっているので、都度記事は増やして行こうと思う。
次回は開発のまとめと後半詰まったところを紹介していく。

次の記事: Gatsby+Strapiでブログを構築した話(12)まとめ+トラブル紹介