ottijp blog

Gatsby.jsで作った静的サイトで予約投稿を実現する

2019-11-09 Tags: FirebaseGatsbyNetfily

ちょっと前にGatsby.jsを使ってブログを運用しているのですが,予約投稿ができなくて困っていました.

現状の構成はGitHubのプライベートリポジトリでブログを管理して,Netlifyにデプロイしています. これだとGitHubのmasterにコミットしたタイミングでデプロイされてしまうので,予約投稿ができませんでした.

previous architecture

そこで,Firebase Functionを使って自動的にプルリクエストをmergeし,予約したタイミングで自動でデプロイされるようにしてみました.

current architecture

ブログ作成時にPRを作成し,Firebase Functionが定期的にPRをチェックし,投稿時刻を過ぎていたら自動でマージしています. 投稿時刻はPRのタイトルをreserve-YYYYMMDD-hhmmssのようにしておき,時間が過ぎていたらマージされます.

投稿時刻にスケジューラを実行させるようなフローも考えられますが,自分が普段触るのはGitHubに統一しておきたかったので,このような構成にしました.

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132

$ firebase --version
7.6.2

$ node --version
v8.11.4

手順

Firebaseプロジェクトの作成

まず,Firebseのプロジェクトを新規作成します. どちらでもよいと思いますが,ファンクションだけ利用するのでGoogle Analyticsは使用しないオプションで作成しました.

その後,firebase-toolsでログインして,予約投稿ファンクション用のプロジェクトを作成します.

$ firebase login
$ mkdir blog-auto-merger && cd blog-auto-merger
$ firebase init functions

(略)

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: <さきほど作成したプロジェクト>

(略)

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
✔  Wrote functions/package.json
✔  Wrote functions/.eslintrc.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore

後述するように,Firebase FunctionをGoogle Cloud Schedulerで定期実行するため,プロジェクトをBlaze(従量課金プラン)にアップグレードしておく必要があります. また,Google Cloud Schedulerの実行のために,GCPリソースロケーションを選択しておきます. リソースロケーションはどこでも良いですが,GitHubにアクセスするだけなので,私はus-east1にしておきました.

select resource location

GitHubアクセストークンの発行

GitHub API経由でPRの取得とマージを行うため,アクセストークンを発行します.こちらのページで発行できます. スコープはrepoだけでOKです.

get github token

発行されたアクセストークンは,次の環境変数設定で利用します.

Firebase Functionの環境変数設定

GitHubのユーザ,リポジトリ,アクセストークンはファンクションに含めず,GCPのRuntime Configというサービスで環境変数として設定しました. Runtime Configの使い方は以前Qiitaに書いたので参照してください.

参考: Cloud Functionsで環境変数を設定する方法 - Qiita

実際には次のように環境変数を設定します.

$ gcloud --project <your-project-name> beta runtime-config configs create github
Created [https://runtimeconfig.googleapis.com/v1beta1/projects/<your-project-name>/configs/github].
$ gcloud --project <your-project-name> beta runtime-config configs variables set OWNER <your-github-owner> --config-name github --is-text
Created [https://runtimeconfig.googleapis.com/v1beta1/projects/<your-project-name>/configs/github/variables/OWNER].
$ gcloud --project <your-project-name> beta runtime-config configs variables set REPO <your-github-repo> --config-name github --is-text
Created [https://runtimeconfig.googleapis.com/v1beta1/projects/<your-project-name>/configs/github/variables/REPO].
$ gcloud --project <your-project-name> beta runtime-config configs variables set TOKEN <your-github-token> --config-name github --is-text
Created [https://runtimeconfig.googleapis.com/v1beta1/projects/<your-project-name>/configs/github/variables/TOKEN].

<your-project-name>, <your-github-*>は自身の環境に置き換えてください.

Firebase Function用コードの作成

完成コードはこちらに置いています.→ ottijp/blog-auto-merger

GitHubのPRの取得とマージは,GitHub APIのPull Requestsを使っています

api-client.js
  async getPulls () {
    return await axios.get(`${BASE_URL}/repos/${this.owner}/${this.repo}/pulls?access_token=${this.token}`)
  }

  async mergePulls (numbers) {
    await Promise.all(numbers.map(number => {
      return axios.put(`${BASE_URL}/repos/${this.owner}/${this.repo}/pulls/${number}/merge?access_token=${this.token}`)
    }))
  }

マージすべきタイミング(予約時刻を過ぎた)かどうかはPRのタイトルから判断しています.

app.js
  static _shouldMergePull (prName, currentDate) {
    const m = prName.match(/reserve-([0-9]{4})([0-9]{2})([0-9]{2})-([0-9]{2})([0-9]{2})([0-9]{2})/)
    if (!m) return false

    const reservedDate = Date.parse(`${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}+09:00`)

    return reservedDate <= currentDate
  }

その他は完成コードを見てください.

デプロイ

次のようにデプロイします.

$ firebase use <your-project-name>
$ npm run deploy

次のようなエラーが表示される場合,前述のGCPのリソースロケーションが設定されていないので,設定してください.

Error: Cloud resource location is not set for this project but scheduled functions requires it. Please see this documentation for more details: https://firebase.google.com/docs/projects/locations.

予約投稿する

予約投稿用のブランチに記事を投稿し,PRを作っておきます. ここでは2019/11/09 17:00に投稿したいので,PR名をreserve-20191109-170000としています.

pr for reserved post

その後,2019/11/09 17:00:00以降のFirebase Functionの実行で,自動マージされました.

firebase function log

pr merged

気になること

こちらのページに次のような注記があるのですが,GAEアプリ分の料金が課金されるということなんでしょうか? 料金がどの程度になるのかちょっと不明なので,あとでわかったら追記します.

重要: Cloud Scheduler では、プロジェクトに Google App Engine(GAE)アプリが必要です。このアプリの設定中には、プロジェクトのデフォルトの Google Cloud Platform(GCP)リソース ロケーションを選択するように求められます。このロケーションは、プロジェクト内のロケーション設定が必要な GCP サービスで使用されます。具体例としては、デフォルトの Cloud Storage バケットや Cloud Firestore データベースなどがあります。

refs


ottijp
Satoshi SAKAO (@ottijp)

都内でアプリケーションエンジニアをしています

...