GCP 排程開關機-省錢大作戰

前言

客戶剛好今天升級了compute engine的主機,但又詢問我主機的費用與實際的使用時間落差有點大,畢竟下班時間主機都是閒置的狀況,GCP的 GCE是用多少付多少,如果用滿一個月可以有折扣,另外還有購買承諾折扣能打折,但目前這專案還是以用多少付多少的方式,試想能不能設定讓他依照上班時間自動開關機,這樣主機規格可以用好一點,但又能省錢也就有了這一篇文章的誕生。

翻找了一下Google 的技術文件,有一個可以設定排程的Node.js的範例,但我按照範例上面的程式碼與步驟設定了一次,發現完全不起作用,浪費了許多時間,整體架構圖如下。

取自Google 官方

設定Google Cloud Function

以往我們開發Web Serice都是開一台VPS的主機,然後在上面開發API,那Cloud Function 就跳過VPS這一段,我們可以直接部署一個Cloud Function,我們先確定Star / Stop Instance 是可以動作的,我們再把排程補上。

  1. 請到GCP的主控台找到Cloud Function 並「建立函式

2. 設定相關參數
函式名稱(自己定義)
區域默認即可
觸發條件 Cloud Pub/Sub
建立主題

然後,點選頁面最下面的下一步,就可以開始撰寫程式碼,這邊的範例和官方的略有不同,至於為什麼呢?
因為我用官方的完全沒辦法正常執行…。

執行階段請選擇 Node.js 10,右邊進入點輸入stopInstancePubSub,右邊程式碼區塊我放在下面,直接貼上就可以用了。

const compute = require('@google-cloud/compute');
const instancesClient = new compute.InstancesClient({fallback: 'rest'});

/**
 * Stops Compute Engine instances.
 *
 * Expects a PubSub message with JSON-formatted event data containing the
 * following attributes:
 *  zone - the GCP zone the instances are located in.
 *  label - the label of instances to stop.
 *
 * @param {!object} event Cloud Function PubSub message event.
 * @param {!object} callback Cloud Function PubSub callback indicating completion.         
 */
exports.stopInstancePubSub = async (event, context, callback) => {
  try {
    const project = await instancesClient.getProjectId();
    const payload = _validatePayload(event);
    const options = {
      project,
      zone: payload.zone,
    };
    console.log(payload.zone);
    console.log(project);
    const [instances] = await instancesClient.list(options);

    if (instances.length > 0) {
          for (const instance of instances) {
              console.log(` - ${instance.name} (${instance.machineType})`);
              instancesClient.stop({
            project,
            zone: payload.zone,
            instance: instance.name,
          });
    }

    }else{
      console.log("instance not found");
    }

    // Operation complete. Instance successfully stopped.
    const message = 'Successfully stopped instance(s)';
    console.log(message);
    callback(null, message);
  } catch (err) {
    console.log(err);
    callback(err);
  }
};

/**
 * Validates that a request payload contains the expected fields.
 *
 * @param {!object} payload the request payload to validate.
 * @return {!object} the payload object.
 */
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.label) {
    throw new Error("Attribute 'label' missing from payload");
  }
  return payload;
};

package.json

{
  "name": "cloud-functions-schedule-instance",
  "version": "0.1.0",
  "private": true,
  "license": "Apache-2.0",
  "author": "Google Inc.",
  "repository": {
    "type": "git",
    "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
  },
  "engines": {
    "node": ">=12.0.0"
  },
  "scripts": {
    "test": "mocha test/*.test.js --timeout=20000"
  },
  "devDependencies": {
    "mocha": "^9.0.0",
    "proxyquire": "^2.0.0",
    "sinon": "^12.0.0"
  },
  "dependencies": {
    "@google-cloud/compute": "^3.0.0"
  }
}

依樣畫葫蘆我們建立一個關閉的function

儲存,下一步
const compute = require('@google-cloud/compute');
const instancesClient = new compute.InstancesClient({fallback: 'rest'});

/**
 * Stops Compute Engine instances.
 *
 * Expects a PubSub message with JSON-formatted event data containing the
 * following attributes:
 *  zone - the GCP zone the instances are located in.
 *  label - the label of instances to stop.
 *
 * @param {!object} event Cloud Function PubSub message event.
 * @param {!object} callback Cloud Function PubSub callback indicating completion.         
 */
exports.stopInstancePubSub = async (event, context, callback) => {
  try {
    const project = await instancesClient.getProjectId();
    const payload = _validatePayload(event);
    const options = {
      project,
      zone: payload.zone,
    };
    console.log(payload.zone);
    console.log(project);
    const [instances] = await instancesClient.list(options);

    if (instances.length > 0) {
          for (const instance of instances) {
      console.log(` - ${instance.name} (${instance.machineType})`);
      instancesClient.stop({
            project,
            zone: payload.zone,
            instance: instance.name,
          });
    }

    }else{
      console.log("instance not found");
    }

    // Operation complete. Instance successfully stopped.
    const message = 'Successfully stopped instance(s)';
    console.log(message);
    callback(null, message);
  } catch (err) {
    console.log(err);
    callback(err);
  }
};

/**
 * Validates that a request payload contains the expected fields.
 *
 * @param {!object} payload the request payload to validate.
 * @return {!object} the payload object.
 */
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.label) {
    throw new Error("Attribute 'label' missing from payload");
  }
  return payload;
};

package.json 一樣要編輯,跟上面的一模一樣,就不再貼上來了。

設定Compute Engine Instacne 標籤

我們新增一個標籤
記得儲存

測試Cloud Function

在清單中我們先選擇,任一個最右方三個點,點選測試函式。

data的Value 其實是,{“zone”:”asia-east1-b”, “label”:”function=restart”}的BASE64編碼,具體可以使用這個工具,可以直接點選測試函式。

等待一下,下面會跑出記錄檔和輸入,這時候就可以去檢查是否真的被關閉或關閉中。

設定Google Cloud Scheduler

地區無須變更

設定其名稱與頻率,頻率如果你有用過Linux 應該挺熟悉的,可以使用這個工具來模擬,這邊設定週一到週五早上10點執行

訊息內文,就依照剛剛設定的標籤,與主機的區域做設定,主題就是與剛剛Cloud Function一致,點繼續

點選最右邊的「立即執行」,就可以模擬排程執行,就可以去檢查是否開啟主機,關閉的排程與開啟排程一樣,只是頻率和主題依照你的需求去修改。

Cloud Scheduler計費

可以參考這個網頁,具體來說還是以工作的數量來收費,但還是有免費的額度,超過額度的話,還是比長期開著主機來得便宜。