Herokuで時間ごとにDynoの数を変更するスクリプト

2023/11/06に公開

経緯

テンマドでは「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分に、指定されたコードを実行します。

Heroku Schedulerの設定画面のキャプチャ。毎時0分に指定のコマンドを実行するように

結果

その後、順調に祝日、土曜日、日曜日、平日と想定していた動作をしていることを確認しました。もっと早く書いておけばよかったなあ、というのが正直なところです。そうしたら「Process Scheduler」をそもそも利用する必要がなかったかも。

とは言え、今まですごく「Process Scheduler」にはお世話になったので、ありがとうという気持ちです。どうして終了してしまったのかは謎のままだったりするのですが(Herokuから急に存在が消えてしまったので、正直困惑しています…)。

しばらくはこのスクリプトを使い続けることになりそうです。

テンマド技術メモ

Discussion