🖥

GCEのインスタンスグループにおけるインスタンス自動再起動(再作成)スクリプトの実装例

2023/12/07に公開

こんにちは、Luup SREチームのにわです。
これは Luup Advent Calendar 2023 の 7 日目の記事です。

最初に

GCEのインスタンスグループにおけるインスタンスの自動再作成をするスクリプトをCloud Functionsに実装し、一時期運用していました。こちらについて、注意点や工夫したことについて記載します。

役立つものは以下のとおりです。

  • 気を付けること
    • InstanceGroupManagersClientInstanceGroupsClient の2つがある。これは混乱しやすいところです。
      • どのようにして理解したか。
        • Cloud SDKのgcloudのコマンド体系から推測しました。
    • Node.js におけるライブラリにおける展開後のサイズが大きい場合があるため、Cloud Functionsに実装する場合は気を付ける必要があります。
  • 工夫したこと
    • パラメーターで起動できるものを増やしたこと

経緯

一部のサーバー群をGoogle Cloud Platformにおいて、インスタンスグループを利用しています。
一時期、そのインスタンスグループのサーバー群がひどくメモリーリークを起こしていました。
アプリケーションのパッチやサーバー側での対策が取れるまでの暫定措置として、インスタンスの再作成を定期的に行うことを試しました。
インスタンスを再作成した理由は、コマンドの命令群に再起動がなくインスタンスグループで管理しているため、インスタンスの停止と開始をそれぞれ行うよりも置き換えてしまったほうが早いと判断したためでした。

本題

構成は以下のようにしました。

Cloud scheduler --(metadata)--> Cloud PubSub --(queue)--> Cloud Functions --(request restart)--> Compute Engine

こちらの記事を参考にして作りました
GCEのインスタンスを自動停止/自動開始をスケジュールする方法

ソースコードは以下のとおりです。

  • このコードでできることは、インスタンスの開始、停止、再作成、インスタンスグループのインスタンスサイズ変更、インスタンスグループ内におけるインスタンス一覧の取得です。
  • 考慮したところとして、再作成のコマンドにおけるインスタンスグループの可用性の維持がありました。payloadにmax_unavailable_per_percentageを設定することによって、その割合をもとに適用することが可能となりました。
  • payload.json内で、メンテナンスの有効無効のトグルをつけ、実行をスキップも可能です。
gceInstanceMaintenance.js
gceInstanceMaintenance.js
const { InstanceGroupManagersClient } = require('@google-cloud/compute').v1;
const compute = require('@google-cloud/compute');
const instancesClient = new compute.InstancesClient({ fallback: 'rest' });

exports.gceInstanceMaintenance = async (event, callback) => {
  // Setup the varibles for retry mthod
  const eventAgeMs = Date.now() - Date.parse(event.timestamp);
  const eventMaxAgeMs = 12000;

  // Ignore events that are too old for retry method
  if (eventAgeMs > eventMaxAgeMs) {
    console.log(`Dropping event ${event} with age[ms]: ${eventAgeMs}`);
    callback();
    return;
  }

  // Trying to execute the  main function
  try {
    const computeClient = new InstanceGroupManagersClient();
    const projectFromInstancesClient = await instancesClient.getProjectId();
    const projectFromInstanceGroupManagersClient = await computeClient.getProjectId();
    const payload = _validatePayload(event);

    // Validate a payload value whther it is undefined or null.
    if (payload === undefined || payload === null) return;

    const instanceGroup = payload.instance_group;
    const instanceGroupManager = payload.instance_group;
    var isoDateString = new Date().toISOString();

    if (!payload.instance_template === undefined || !payload.instance_template === null) {
      if (payload.instance_template.replace(/^\s+|\s+$/g, "")) {
        var instanceTemplate = payload.instance_template.replace(/^\s+|\s+$/g, "");
      };
    } else {
      // Get current configuration
      const requestInstanceGroup = {
        instanceGroupManager,
        project: projectFromInstanceGroupManagersClient,
        zone: payload.zone,
      };
      const iterableListInstanceGroup = await computeClient.listManagedInstancesAsync(requestInstanceGroup);
      for await (const responseListInstanceGroup of iterableListInstanceGroup) {
        console.log("InstanceTemplace: " + responseListInstanceGroup.version.instanceTemplate);
        var instanceTemplate = responseListInstanceGroup.version.instanceTemplate
      }
    }

    if (!instanceTemplate) {
      throw new Error("Attribute 'instanceTemplate' missing ");
    }

    // Rolling replace action's attributes
    const instanceGroupManagerResource = {
      "updatePolicy": {
        "maxSurge": {
          "fixed": payload.instance_horizontal_size
        },
        "maxUnavailable": {
          "percent": payload.max_unavailable_per_percentage
        },
        "minimalAction":
          "REPLACE",
        "type":
          "PROACTIVE"
      },
      "versions": [{
        "instanceTemplate": instanceTemplate,
        "name": isoDateString
      }]
    }

    var message = '';
    if (payload.svstatus == 'start') {
      const options = {
        project: projectFromInstancesClient,
        zone: payload.zone,
        instance: payload.instance,
      };
      await instancesClient.start(options);
      message = 'Successfully started instance ' + payload.instance;
    } else if (payload.svstatus == 'stop') {
      const options = {
        project: projectFromInstancesClient,
        zone: payload.zone,
        instance: payload.instance,
      };
      await instancesClient.stop(options);
      message = 'Successfully stopped instance ' + payload.instance;
    } else if (payload.svstatus == 'replace') {
      // Rolling action: replace
      // Hints by: gcloud compute instance-groups managed rolling-action replace NAME --max-unavailable=50% --max-surge=2
      const request = {
        instanceGroupManager,
        instanceGroupManagerResource,
        project: projectFromInstanceGroupManagersClient,
        zone: payload.zone,
      };
      await computeClient.patch(request);
      message = 'Successfully a request of rolling-replace instance: ' + instanceGroup;
    } else if (payload.svstatus == 'resize') {
      const request = {
        instanceGroupManager,
        project: projectFromInstanceGroupManagersClient,
        size: payload.instance_horizontal_size,
        zone: payload.zone,
      };
      await computeClient.resize(request);
      message = 'Successfully a request of resize instance: ' + instanceGroup;
    } else if (payload.svstatus == 'list') {
      const request = {
        instanceGroupManager,
        project: projectFromInstanceGroupManagersClient,
        zone: payload.zone,
      };
      const iterable = await computeClient.listManagedInstancesAsync(request);
      for await (const response of iterable) {
        console.log(response);
      }
      message = 'Successfully a request of list instance: ' + instanceGroup;
    }
    console.log(message);
    callback(null, message);
  } catch (err) {
    console.log(err);
    setTimeout(callback(err), 3000);
  }
};

/**
 * 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.enable_maintenance === undefined || payload.enable_maintenance === null) {
    throw new Error("Attribute 'enable_maintenance' missing from payload");
  } else if (!payload.enable_maintenance == true ) {
    console.log("Attribute 'enable_maintenance' is not true");
    return null;
  } else 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.instance_group) {
    throw new Error("Attribute 'instance_group' missing from payload");
  } else if (!payload.instance_horizontal_size) {
    throw new Error("Attribute 'instance_horizontal_size' missing from payload");
  } else if (!payload.max_unavailable_per_percentage) {
    throw new Error("Attribute 'max_unavailable_per_percentage' missing from payload");
  } else if (!payload.svstatus) {
    throw new Error("Attribute 'svstatus' missing from payload");
  }

  return payload;
};
package.json
{
  "name": "schedule-gceinstance",
  "version": "1.0.0",
  "private": true,
  "engines": {
    "node": ">=12.0.0"
  },
  "devDependencies": {
    "mocha": "^9.0.0",
    "proxyquire": "^2.0.0",
    "sinon": "^11.0.0"
  },
  "dependencies": {
    "@google-cloud/compute": "^3.0.0",
    "googleapis": "^89.0.0"
  }
}
payload.json
{
  "enable_maintenance": true,
  "zone": "asia-northeast1-c",
  "instance": "instance-hoge-group-5d8c",
  "instance_group": "instance-hoge-group",
  "instance_template": "instance-template-hoge-v1",
  "instance_horizontal_size": 2,
  "max_unavailable_per_percentage": 50,
  "svstatus": "list"
}

考慮事項

最後に

Google Cloudのインスタンスグループのサーバー群をメンテナンスする方法を自動化する話について記載しました。
いかがでしたでしょうか。フィードバックをいただいたり、皆さんのお役に立てれば幸いです。

また、今月は記事を2回書く予定です。
次回はGitHub上でのレビューフローを変えずに、仕組みを整えて「Developers BlogをPublicationに移行した話」を12月19日にお送りする予定です。

お楽しみに!

Luup Developers Blog

Discussion