TECH BLOG
メタサイト
メタサイト

【祝日対応版】GCEインスタンスの自動停止/自動起動でサーバー費を節約しよう

Cover Image for 【祝日対応版】GCEインスタンスの自動停止/自動起動でサーバー費を節約しよう
目次

    メタサイトエンジニアのYKです。

    突然ですが、日常生活で水を使い終えたらどうしていますか?水を止めているのではないでしょうか。ガスコンロを使い終えたときはどうでしょうか?ガス(火)を止めていますよね?

    では、サーバーは?

    24時間365日稼働しているシステムだから止めることはできない、という声が聞こえてきそうですが、テスト環境はどうでしょうか?

    様々なコストが上がっている中、少しでもコストを抑えるべく、祝日に対応したサーバーの自動停止/自動起動の仕組みを導入したので、今回はその方法を紹介したいと思います。

    何故サーバーを止めるのか?

    サーバー費節約のためと言ってしまえばそれまでなのですが、もう少し具体的な理由を挙げると次の2点です。

    1. 円安ドル高によるサーバー費の高騰

      Google Cloud Platform(GCP)やAmazon Web Services(AWS)、Microsoft Azureといったクラウドサービスを利用する場合、いずれのサービスも米国企業のため利用料は米ドル基準になります。

      そのため、日本円基準で考える場合は米ドル/円の為替レートに左右されます。

      少し前と比べれば多少は落ち着きましたが、まだまだ円安ドル高の状況は続いているのでサーバー費は高止まりしています。

    2. サーバー数が多い

      クロス・マーケティンググループ全体では様々なシステムがあり、その殆どが本番環境だけでなくテスト環境もあります。
      つまり、その分だけサーバーが存在することになります。

      これらのサーバーにはクラウドサービスを利用しているためサーバーを起動しているだけでも利用料は発生しますが、その一方でテスト環境は業務時間外に使用することは殆どない状況です。

    このような事情があり、費用節約のためにテスト環境のサーバーを「使わないときは止める」ようにしたい、という思惑がありました。

    サーバー費の節約効果は?

    以下の内容は本記事執筆時点の内容です。最新の料金については、料金ページをご確認ください。

    「使わないときは止める」ので、なんとなくサーバー費を節約できそうなことはイメージが沸くと思いますが、節約効果はどのくらいになるのかを具体的にイメージできる人は少ないと思います。

    そこで、Google Compute Engine(GCE)で今回紹介する設定を反映した場合に、どの程度の節約効果が見込めるのかを試算してみたいと思います。

    1ヵ月は30日、VM インスタンスのマシンタイプ、自動停止/自動起動の設定は以下で試算します。

    設定項目設定値
    VMインスタンスの設定リージョンasia-northeast1(東京)
    マシンタイプn1-standard-1
    仮想CPU数1
    メモリ3.75GB
    料金(米ドル)$0.0610 / hour
    自動起動の設定曜日月曜日~金曜日
    時間9:00
    自動停止の設定曜日月曜日~金曜日
    時間22:00

    GCEでは継続利用割引(SUD)があり、1ヵ月の総時間(24h × 1ヵ月の日数)のうち実際に使用した時間に応じて割引が適用されるので、上記のVMインスタンスで1ヵ月を30日(24h × 30日 = 720h)とした場合は以下のようになります。

    使用時間の割合使用時間継続利用割引
    割引率割引後料金
    0 ~ 25%0 ~ 180h0%$0.0610
    25 ~ 50%180 ~ 360h20%$0.0488
    50 ~ 75%360 ~ 540h40%$0.0366
    75 ~ 100%540 ~ 720h60%$0.0244

    GCEインスタンスの料金

    また、祝日の日数は年によって変わりますが、現時点では年間で16日が「国民の祝日」として法律で定められているため、1ヵ月あたりは平均で1.3日になります。

    1ヵ月のうち、土曜日、日曜日は8日、祝日は1日、平日は21日とすると、自動停止/自動起動をした場合は1ヵ月の起動時間は273h(平日の起動時間:13h × 平日の日数:21日)で$15.51、それに対して停止しなかった場合は720h(24h × 30日)で$30.73になり、約半額程度になります。

    もし、上記の試算と異なる条件で試算したい場合は、Google社がGoogle Cloud 料金計算ツールを公開していますので、そちらを使うと簡単に試算できます。

    GCEインスタンスの自動停止/自動起動方法

    GCEインスタンスの自動停止/自動起動は、インスタンス スケジュールやCloud Functionsを使用した方法を既に多くの先人が紹介していますが、日本の祝日に対応した方法は少ない気がします。

    無いなら作ってしまえば良い、ということで、以下のような処理フローで作ってみました。

    処理フロー

    Cloud Schedulerで祝日判定はできそうになかったので、Cloud Functionsで祝日判定をします。

    肝心の祝日の情報については、内閣府のHPの「国民の祝日」についてに祝日の一覧がCSVで公開されているので、そちらを使用します。

    また、年末年始や会社の創立記念日のような、祝日ではない休日にも対応できるようにしました。

    「祝日まで考慮しなくても良いから、とにかく簡単、手軽に導入したい」という場合は、インスタンス スケジュールを使用するのが簡単だと思います。以下で設定方法が紹介されていますので、そちらをご確認ください。

    VM インスタンスの起動と停止をスケジュールする  |  Compute Engine ドキュメント  |  Google Cloud

    事前準備

    GCEインスタンスを停止、起動することになるので、Webサーバー(NginxやApache)やDB(MySQLやPostgreSQL、MariaDB)といった、システムの動作に必要なサービスがあればサーバー起動時にサービスが自動起動するように設定変更しておいてください。

    Cloud Functions/Cloud Pub/Subの作成

    1. Cloud Functionsの概要ページ」 > 「ファンクションを作成」 CloudFunctions作成 001

    2. 「基本」情報を入力する CloudFunctions作成 002

      設定項目設定値備考
      環境第 1 世代
      関数名schedule-gce-instance
      リージョンasia-northeast1自動起動/自動停止をするGCEインスタンスと同じリージョン設定を推奨。
    3. 「トリガー」を設定(Cloud Pub/Subの作成)

      1. トリガーの情報を入力する CloudFunctions作成 003

        設定項目設定値備考
        トリガーのタイプCloud Pub/Sub
        Cloud Pub/Sub トピック次の手順で設定します
        失敗時に再試行するチェックを付ける失敗時に再試行させたい場合のみ選択してください。
      2. 「Cloud Pub/Sub トピック」 > 「トピックを作成する」 CloudFunctions作成 004

      3. 「トピックID」を入力 > 「作成」 CloudFunctions作成 005

        設定項目設定値備考
        トピックIDgce-instance-event
      4. 「保存」でトリガー設定を保存する CloudFunctions作成 006

    4. 「ランタイム」 > 「ランタイム環境変数」の設定をする CloudFunctions作成 007

      名前設定値の例備考
      TZAsia/Tokyoタイムゾーンを設定します。
      後続の手順で作成するスケジューラーと同じタイムゾーンになるようにしてください。
      UNIQUE_HOLIDAY1/2,1/3,12/29,12/30,12/31「国民の祝日」以外で設定したい休日を設定します。
      フォーマットはm/dで設定し、複数指定する場合はカンマ(,)で列挙してください。
    5. 「次へ」をクリック

    6. 「コード」を設定する

      1. 「ランタイム」/「ソースコード」/「エントリ ポイント」 CloudFunctions作成 008

        設定項目設定値備考
        ランタイムNode.js 18
        ソースコードインライン エディタ
        エントリ ポイントscheduleGceInstance
      2. 「index.js」のコードを設定する

        const axios = require('axios');
        const compute = require('@google-cloud/compute');
        const instancesClient = new compute.InstancesClient({fallback: 'rest'});
        
        /**
         * Compute Engineインスタンスの起動・停止処理
         */
        exports.scheduleGceInstance = async (event, context, callback) => {
          try {
            process.env.TZ = 'Asia/Tokyo';
        
            const project = await instancesClient.getProjectId();
            const payload = _validatePayload(event);
        
            let instances = payload.instance.split(',');
            let callback_message = '';
            let message = '';
        
            if (!await isHoliday() || payload.processing == 'stop') {
              for (let instance of instances) {
                const options = {
                  project,
                  zone: payload.zone,
                  instance: instance,
                };
        
                if (payload.processing == 'start') {
                  await instancesClient.start(options);
                  message = 'Successfully started instance ' + instance;
                } else if (payload.processing == 'stop') {
                  await instancesClient.stop(options);
                  message = 'Successfully stopped instance ' + instance;
                }
        
                callback_message += message + '\n';
              }
            }
        
            callback(null, callback_message);
          } catch (err) {
            console.log(err);
            callback(err);
          }
        };
        
        /**
         * request payloadのバリデート処理
         *
         * @param object event
         * @return json payload
         */
        const _validatePayload = event => {
          let payload;
          try {
            payload = JSON.parse(Buffer.from(event.data, 'base64').toString());
          } catch (err) {
            throw new Error('Invalid Pub/Sub message: ' + err);
          }
          if (!payload.zone) {
            throw new Error("Attribute 'zone' missing from payload");
          } else if (!payload.instance) {
            throw new Error("Attribute 'instance' missing from payload");
          }else if (!payload.processing) {
            throw new Error("Attribute 'processing' missing from payload");
          }
          return payload;
        };
        
        /**
         * 祝日判定処理
         * @returns bool true:祝日, false:平日
         */
        async function isHoliday() {
          let systemDate = new Date();
        
          // カレンダーに無い独自の祝日を判定する
          if (process.env.UNIQUE_HOLIDAY.split(',').some((value) => {
            let holiday = value.split('/').map(e => parseInt(e)).join('/');
            return holiday === [(systemDate.getMonth() + 1), systemDate.getDate()].join('/');
          })) {
            return true;
          }
        
          // 内閣府のHP 「国民の祝日」について(https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)
          // に掲載されているCSVファイルをHTTPリクエスト(GET)で取得する
          const response = await axios.get("https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv");
        
          // 祝日を判定する
          const datePattern = /^(\\d+(\/|\-|\.)\\d+(\/|\-|\.)\\d+).*/;
          const dateSplitter = /\/|\-|\./;
          if (response.data.split(/\r\n|\r|\n/g).some((value) => {
            let holiday = value.replace(datePattern, '$1').split(dateSplitter).map(e => parseInt(e)).join('/');
            return holiday === [systemDate.getFullYear(), (systemDate.getMonth() + 1), systemDate.getDate()].join('/');
          })) {
            return true;
          }
        
          return false;
        }
        
      3. 「package.json」のコードを設定する

        {
          "name": "schedule-gce-instance",
          "version": "1.0.0",
          "private": true,
          "engines": {
            "node": ">=12.0.0"
          },
          "dependencies": {
            "@google-cloud/compute": "^3.0.0",
            "googleapis": "^89.0.0",
            "axios": "^1.0.0-alpha.1"
          }
        }
        
    7. 「デプロイ」をクリック

    Cloud Schedulerの作成

    GCEインスタンス起動用/停止用のスケジューラー各1つを作成します。

    1. Cloud Scheduler ページ」 > 「ジョブを作成」 CloudScheduler作成 001

    2. 「スケジュールを定義する」を設定する CloudScheduler作成 002

      用途設定項目設定値備考
      起動用名前startup-gce
      リージョンasia-northeast1 (東京) 自動起動/自動停止をするGCEインスタンスと同じリージョン設定を推奨。
      頻度0 9 * * 1-5月曜日~金曜日の9:00。
      任意の設定でOKです。
      タイムゾーン日本標準時(JST)タイムゾーンを設定します。
      Cloud Functionsの設定と同じタイムゾーンになるようにしてください。
      停止用名前shutdown-gce
      リージョンasia-northeast1 (東京) 自動起動/自動停止をするGCEインスタンスと同じリージョン設定を推奨。
      頻度0 22 * * 1-5月曜日~金曜日の22:00。
      任意の設定でOKです。
      タイムゾーン日本標準時(JST)タイムゾーンを設定します。
      Cloud Functionsの設定と同じタイムゾーンになるようにしてください。
    3. 「実行内容を構成する」を設定する CloudScheduler作成 003

      用途設定項目設定値備考
      起動用ターゲット タイプPub/Sub
      Cloud Pub/Sub トピックgce-instance-event
      メッセージ本文{"zone":"asia-northeast1-a","instance":"instance-name1,instance-name2","processing":"start"}zone:対象のGCEインスタンスのゾーン
      instance:対象のGCEインスタンス名(複数の場合はカンマ(,)区切りで指定)
      停止用ターゲット タイプPub/Sub
      Cloud Pub/Sub トピックgce-instance-event
      メッセージ本文{"zone":"asia-northeast1-a","instance":"instance-name1,instance-name2","processing":"stop"}zone:対象のGCEインスタンスのゾーン
      instance:対象のGCEインスタンス名(複数の場合はカンマ(,)区切りで指定)
    4. 「オプションの設定を行う」を設定する

      こちらの内容は必要に応じて設定してください。 CloudScheduler作成 004

    5. 「作成」をクリック

    GCEインスタンスの自動停止/自動起動の結果は?

    GCEインスタンスの自動停止/自動起動の設定をした結果は・・・。

    GCEインスタンス稼働状況

    2023年4月16日~2023年5月20日

    4/16
     
    171819202122
    23
     
    242526272829
    昭和の日
    30
     
    5/123
    憲法記念日
    4
    みどりの日
    5
    こどもの日
    6
    7
     
    8910111213
    14
     
    151617181920

    CPU使用率やメモリ使用率のグラフが平日の9:00~22:00以外は消えているため、期待通りの挙動(平日の9:00~22:00の間だけ起動)が実現できていることがわかります。

    GCEインスタンス1つ、1ヵ月分だけでみるとサーバー費の削減量は少なく見えますが、「塵も積もれば山となる」という諺もあるように対象のGCEインスタンスが複数ある場合や長期間続けた場合の総額で見たら無視できない金額になるかもしれません。

    少しでもコストを抑えたい方にとって、今回紹介した方法が少しでも役に立てば良いなと思っています。

    私たちは積極的に採用活動をしております。
    https://www.metasite.co.jp/recruit

    Companies

    エクスクリエ
    クロス・マーケティンググループ
    メタサイト
    クロス・コミュニケーション

    Tags