HerokuでRailsタスクを定期実行する方法（Google Apps Scriptのトリガーを使う方法）

HerokuにデプロイしたRailsアプリでタスクを定期実行する方法を紹介します。
ここで示す方法は、Google Apps Script (GAS)を使う方法です。

結論を先に述べると以下のようにします：
1. Railsタスクを作成しておく
2. GASでHerokuのAPI経由でコマンドをpostする関数を書く
3. GASでスケジューラーを作成する（トリガーを作る）

例として、Railsの場合を示していますが、GAS経由で「herokuのコマンドを定期実行する」という点がポイントなので、他の用途にも使えます。言語がRubyである必要性もありません。

この記事では、Railsの記事投稿アプリを例にとります。
スケジューラーを使って、24時間ごとにデータベースのテーブルのレコードを全て削除し、初期データをデータベースに入れる方法を示します。

## 変更履歴
- 200229 `runHerokuCommand("heroku run rails initialize_db:add_initial_blog");`の部分を`runHerokuCommand("rails initialize_db:add_initial_blog");`に修正。併せてcurlコマンドも`"command": "rails initialize_db:add_initial_blog"`に修正。

## 対象読者：プログラミング初心者向け
- 私自身は転職活動中の未経験エンジニアです。内容には正確性を期しますが、間違いがあればご指摘くださると幸いです。
- 初学者がHerokuを使っていて、「Herkouでデータベースを定期的にリセットしたいが調べても方法が分からない」と疑問を持った際に、当記事の内容を少しでも活用してもらえればと思って書きました。

## 経緯 (経緯に興味のない人は飛ばしてください)
HerokuにRailsアプリをデプロイしていて、一定時間ごとにデータベースを初期化するために今回の方法に行き着きました。

前提として、**無料プランで使用できるデータベースの[Heroku Postgres](https://jp.heroku.com/pricing#postgres-pricing)では保存できるレコード数が1万件までという制限があります。**

そのため、ポートフォリオとして公開しているだけの（ブログアプリのような）webアプリなら、定期的にデータベースのデータを全て削除したいことがあるでしょう。
一方、アプリの体裁を整えるために、幾つかのデータ（例えば、投稿記事）をデータベースに見本として入れたいことがあり得ると思います。

今回の私がこのような状況でした。
これを解決するために以下のようにしました。

- Railsのタスクを作成する：このタスクの実行でテーブルを空にしてから、初期データを投入する
- Heroku APIを使い、httpリクエストでRailsタスクを実行する
- GASでhttpリクエストを定期実行する

これで何とか解決したので、そのぞれの方法を説明します。

## 前提となるサンプリアプリの作成
### 記事投稿機能
例として、テキストを投稿するサンプルアプリを以下のように作ります。

```bash
rails new blog-app
rails db:create
rails generate scaffold post content:text
rails db:migrate
```

最低限の内容ですが、これを記事投稿アプリの例とします。

### 記事投稿するためのRailsタスクを作成
`rails g task initialize_db`として、タスクのファイルを以下のように編集します。

```ruby:lib/tasks/initialize_db.rake
namespace :initialize_db do
  desc "初期データとしてblogsテーブルにデータを入れる"
  task add_initial_blog: :environment do
    Post.destroy_all
    Post.create!([
      {content: "初めての投稿です"},
      {content: "本日初めてqiitaに記事を投稿しました。"}
    ])
  end
end
```

これで、ターミナルで`rails initialize_db:add_initial_blog`とすれば、postsテーブルのレコードを全て削除して、２つの記事を投稿することができます。

ひとまず、ここまでの内容をHerokuにデプロイ済みだとします。
このタスクをスケジューラーで定期的に実行するのが今回の目的となります。

## HerokuのコマンドをAPI経由で実行する
Herokuにアプリをデプロイする際や、ログを確認する際には[Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)を使って、コマンドを実行するのが一般的です。

しかし、コマンドの実行を自動化するにはAPIを使う方が（おそらく）便利です。
Heroku APIについては公式のリファレンスにまとまっています。

### APIでone-off Dynoを作り、コマンドを実行
今回は、API経由でコマンドを実行する方法に着目します。
公式によれば以下のようにします: [Platform API Reference | Heroku Dev Center](https://devcenter.heroku.com/articles/platform-api-reference#dyno-create)

```
POST /apps/{app_id_or_name}/dynos
```

今回はherokuのCLIで`heroku run rails initialize_db:add_initial_blog`と打つのをAPIで代替したいので、curlで以下のようにします。

```
$ curl -n -X POST https://api.heroku.com/apps/$APP_ID_OR_NAME/dynos \
  -d '{
  "command": "rails initialize_db:add_initial_blog",
}' \
  -H "Content-Type: application/json" \
  -H "Accept: application/vnd.heroku+json; version=3"
  -H "Authorization: Bearer $HEROKU_API_KEY"
```

`$APP_ID_OR_NAME`と`$HEROKU_API_KEY`については個々のアプリに応じて読み替えてください。

### Herokuの`$APP_ID_OR_NAME`と`$HEROKU_API_KEY`の確認方法
`$APP_ID_OR_NAME`を確認するにはCLIで`heroku apps`と打ちます。
もしくは、アプリの公開URLが`https://<$APP_ID_OR_NAME>.herokuapp.com/`のような形式になっているので、そこから読み取ることもできます。

`$HEROKU_API_KEY`を確認するには、`heroku auth:token`とCLIで打ちます。このトークンは１年しか有効ではないので、半永久的に使いたい場合には、`heroku authorizations:create`としてトークンを生成してください：[Getting Started with the Platform API | Heroku Dev Center](https://devcenter.heroku.com/articles/platform-api-quickstart#authentication)


## GASでスケジューラーを作成
### 他のスケジューラーではダメなのか
herokuで特定の処理を一定時間ごとに実行する代表的な方法は[Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler)というアドオンを使うものです。

しかし、Heroku Schedulerの使用にはクレジットカードの登録が必要であり、無料Dynoの枠を超えると課金される可能性もあるので、完全に無料の方法として、GASを使うことにしました。

他の方法としてClock ProcessesというHerokuの機能を活用する手もあります： [Scheduled Jobs and Custom Clock Processes | Heroku Dev Center](https://devcenter.heroku.com/articles/scheduled-jobs-custom-clock-processes)。Clock Processesはクレジットカード登録なしで使用できますが、追加のdynoを使うため、今回は見送りました。

### GASなら無料でスクリプトの定期実行ができる
Googleドライブで使用できるGoogle Apps Scriptは、無料でスクリプトを定期実行できます。
実際には実行回数に制限がありますが、無料プランでも十分な回数です：[Quotas for Google Services  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/guides/services/quotas)。
例えば、今回利用するURL Fetch callsでは一日あたり20000回使用できます。

### GASの環境設定
GASの実行環境を整えます。googleのアカウントを持っていれば、１分で完了します。
googleドライブで「Google Apps Script」というアプリを選択するだけです。

![Google Apps Script](https://gyazo.com/8d53cc2b329280778e090d3a0442e29c.png)

初回はメニューに登録されていないので、「アプリを追加」からGoogle Apps Script検索して、アプリを追加してください。

### GASで定期実行するスクリプト
GASは、一部独自機能があるものの、文法はJavaScriptに準拠しています。

heroku APIをcurlで使用するためのコマンドをGASのコードに読み替えます。
結果だけ示すと、以下のようにrunHerokuTask関数を定義し、この関数を定期実行すればうまくいきます。

```javascript
function runHerokuTask() {
  runHerokuCommand("rails initialize_db:add_initial_blog");
}

function runHerokuCommand(command) {
  var urlTemplate = "https://api.heroku.com/apps/%s/dynos";
  var herokuToken = PropertiesService.getScriptProperties().getProperty("heroku_token");
  var herokuAppName = PropertiesService.getScriptProperties().getProperty("heroku_app_name"); 
  var herokuAuth = Utilities.formatString("Bearer %s", herokuToken);
  var url = Utilities.formatString(urlTemplate, herokuAppName);

  var headers = {
    "Content-type": "application/json",
    "Accept": "application/vnd.heroku+json; version=3",
    "Authorization": herokuAuth
  }
  
  var data = {
    "command": command
  }

  var options = {
    "method": "post",
    "payload": JSON.stringify(data),
    "headers": headers
  };

  var response = UrlFetchApp.fetch(url, options);
  var text = response.getContentText();
  Logger.log(text);
}
```

このコードでのポイントを幾つか説明します。

- トークンやアプリ名などは、プロパティから取り出す
- `UrlFetchApp.fetch`でhttpリクエストをする

GASのコードをgithubで公開する場合、パスワードや環境依存の定数はコードに含めたくはありません。
そのため、GASの機能の[プロパティサービス](https://developers.google.com/apps-script/guides/properties)を活用します。

プロジェクトのプロパティというところからスクリプトのプロパティで「キーと値のペア」を登録します。
![プロジェクトのプロパティ](https://gyazo.com/87817653b5e4867f6305a01e8c819b44.png)

`PropertiesService.getScriptProperties().getProperty("キーの名前")`として、キーに紐付いた値を取り出します。

またhttpリクエストには、[UrlFetchApp](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)を使います。
今回は、POSTメソッドなので、オプションが必要ですが、getメソッドであれば、`UrlFetchApp.fetch("http://www.google.com/")`とするだけでwebサイトにアクセスできます。

### GASのスクリプトの定期実行
![GASのスクリプトの定期実行](https://gyazo.com/c97c21ebb3701f558914f71d4261cb6f.gif)

GASのスクリプトの定期実行には、トリガーを登録します。

![トリガーを登録](https://gyazo.com/1aaa79793bfb74356a6788af070eae1d.png)

これで、毎日、0~1時ごろに登録した関数が自動実行されます。