GCEのインスタンスグループにおけるインスタンス自動再起動(再作成)スクリプトの実装例
こんにちは、Luup SREチームのにわです。
これは Luup Advent Calendar 2023 の 7 日目の記事です。
最初に
GCEのインスタンスグループにおけるインスタンスの自動再作成をするスクリプトをCloud Functionsに実装し、一時期運用していました。こちらについて、注意点や工夫したことについて記載します。
役立つものは以下のとおりです。
- 気を付けること
-
InstanceGroupManagersClient
とInstanceGroupsClient
の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
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;
};
{
"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"
}
}
{
"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"
}
考慮事項
-
payloadにどんな値を入れればいいかが不明であったことが難しいところでした。
- gcloud commandの詳細を読んで理解したうえでコーディングを進めました。
-
リクエスト方法も
post
ではなく、patch
でリクエストするものだというのがわかったところも意外な点でした。 -
Node.js でコーディングするのは初めてでかつGCPのライブラリを扱ったことがなかったため、GCEのインスタンスを自動停止/自動開始をスケジュールする方法を参考にしたのですがインスタンスグループを扱う場合、どのライブラリを使えばよいか判別がつきませんでした。
- gcloud commandの詳細を読んで理解したうえでコーディングを進めました。
- そのうえで、以下のライブラリドキュメントを読んで理解を深めました。
-
ライブラリのサイズについて、 以下のライブラリにおけるサイズが展開後に95MB, 69MBくらいありました。
- googleapis: 94.3MB
- Compute Engine: 68.9MB
したがって、使う際はCloud Functionsのメモリー容量に注意して利用する必要があります。
最後に
Google Cloudのインスタンスグループのサーバー群をメンテナンスする方法を自動化する話について記載しました。
いかがでしたでしょうか。フィードバックをいただいたり、皆さんのお役に立てれば幸いです。
また、今月は記事を2回書く予定です。
次回はGitHub上でのレビューフローを変えずに、仕組みを整えて「Developers BlogをPublicationに移行した話」を12月19日にお送りする予定です。
お楽しみに!
Discussion