Herokuで時間ごとにDynoの数を変更するスクリプト
経緯
テンマドでは「iruca」という業務向けの在席確認ツールを提供しています(大学等の教育機関でもご利用いただいています)。サービス開始当初からHerokuを利用しているのですが、便利に利用していた「Process Scheduler」というAdd-onが先日なくなってしまいました。
「Process Scheduler」で何を実現していたかというと、曜日・時間帯ごとにDyno(プロセスを実行する単位)の数を変更していました。業務向けのサービスなので、平日データイムの利用がほとんどです(出退勤の多い9時ごろと18時ごろが利用のピークだったりします)。夜間や週末・祝日の利用はあまり多くないので負荷も少なく、Dynoの数を調整することでコストを削減していました。
具体的には、普段はDynoを1つだけ動かし、平日データイムのみ2つ動かすようにしていたのです。
この先どうしようかな、と思ったところでHerokuのPlatform APIを利用すればよいことに気が付きました。もっと早く気が付けばよかった…!
ちなみに負荷の状況によってオートスケールさせるようなAdd-onがいくつかあるのですが、irucaでは単純に時間帯によって調整できるようにしたいので、今回は採用の対象外です。
調査
まずはPlatform APIのドキュメントを読んで、今回の目的が実現可能かどうか調べます。「Dyno Size」という項目があったのでそれかな…と思ったら違っていて、「Formation」(日本語にすると「構成」ですかね)という項目があって、「quantity」を操作すればできそうです。
それから、時間ごとにDynoの数を調整する方法を考えます。Herokuには「Heroku Scheduler」という機能があって、定期実行するコマンドを指定することができます。いわゆるバッチ処理向けの機能ですね。その機能を使って、1時間に一度、定期的にスクリプトを実行すればよさそうです。
また、直接APIをHTTP経由で操作するのは面倒です。irucaはNode.jsのアプリケーションなのですが、HerokuのAPI利用の手引きのドキュメントに、公式クライアントのnpmパッケージ「heroku-client」が紹介されていました。READMEを読んだ感じ、Promiseに対応していてそのまま使えそうです。そんなわけで、Node.jsのスクリプトを書くことにしました。
要件
要件をまとめます。
- 時間ごとにDynoの数を定義して、その数にDynoの数を変更できる
- 普段はDynoを1つだけ動かす
 - 負荷が上がる期間は、2つ動かすようにする
 
 - 変更するのは月曜日〜金曜日のみで、土曜日・日曜日は変更しなくてよい
 - (できれば)祝日も土曜日・日曜日と同じ扱いとしたい
- 「Process Scheduler」では実現できていなかったが、この機会にできるようにしたい
 
 
実装
というわけで書いたコードです。平日の8時〜20時の間だけDynoの数を2つにして、それ以外は1つになるようにしてみました。Dynoは「Standard-1X」という種類を利用しています。
'use strict';
const Heroku = require('heroku-client');
const holidayJp = require('@holiday-jp/holiday_jp');
// Dynoの数、平日の時間による推移
const quantities = [
  1,  // 0時
  1,  // 1時
  1,  // 2時
  1,  // 3時
  1,  // 4時
  1,  // 5時
  1,  // 6時
  1,  // 7時
  2,  // 8時
  2,  // 9時
  2,  // 10時
  2,  // 11時
  2,  // 12時
  2,  // 13時
  2,  // 14時
  2,  // 15時
  2,  // 16時
  2,  // 17時
  2,  // 18時
  2,  // 19時
  2,  // 20時
  1,  // 21時
  1,  // 22時
  1,  // 23時
];
const size = 'Standard-1X';
const type = 'web';
(async () => {
  const now = new Date();
  const dayOfTheWeek = now.getDay();
  const hour = now.getHours();
  // Dynoの数、通常1つ
  let quantity = 1;
  if (
    dayOfTheWeek !== 0            // 日曜日ではない
    && dayOfTheWeek !== 6         // 土曜日ではない
    && !holidayJp.isHoliday(now)  // 祝日ではない
  ) {
    // 平日にのみ変更する
    quantity = quantities[hour];
  }
  const heroku = new Heroku({ token: '{your_heroku_api_token}' });
  // https://devcenter.heroku.com/articles/platform-api-reference#formation
  // Formation List API: 現在のDynoの数を取得
  const formation = await heroku.get('/apps/{app_id_or_name}/formation');
  const currentQuantity = formation[0].quantity;
  if (currentQuantity !== quantity) {
    // Formation Batch Update API: Dynoの数を変える
    await heroku.patch(
      '/apps/{app_id_or_name}/formation',
      { body: { updates: [{ quantity, size, type }] }}
    );
    console.log(`[Complete] Heroku Dyno: Scaled to ${type}@${quantity}:${size}`);
  } else {
    console.log('[Complete] Heroku Dyno: Kept the Dyno count');
  }
})().catch((reason) => {
  console.error(reason);
  process.exit(1);
});
ちなみにこのHerokuのアプリケーションでは「Config Vars」(環境変数)で「TZ」に「Asia/Tokyo」を設定済みなので、Node.jsのDateクラスを使う場合、タイムゾーンとして日本時間が用いられるようになっています。
このコードをHerokuの環境にデプロイした後で、「Heroku Scheduler」で定期実行の設定をしました。毎時0分に、指定されたコードを実行します。

結果
その後、順調に祝日、土曜日、日曜日、平日と想定していた動作をしていることを確認しました。もっと早く書いておけばよかったなあ、というのが正直なところです。そうしたら「Process Scheduler」をそもそも利用する必要がなかったかも。
とは言え、今まですごく「Process Scheduler」にはお世話になったので、ありがとうという気持ちです。どうして終了してしまったのかは謎のままだったりするのですが(Herokuから急に存在が消えてしまったので、正直困惑しています…)。
しばらくはこのスクリプトを使い続けることになりそうです。
合同会社テンマドは、Web制作・システム開発だけでなくいろいろなことを企んでいく会社です。事業として「クライアントワーク」「ミソラプロジェクト」「オウンドサービス」のそれぞれに取り組んでいます。会社としてのミッションは「あかるい未来をかたちにする」です。 10mado.jp/
Discussion