
みなさまこんにちはゴリさんです。
エンジョイワークスではAWSを利用しており、EC2でサーバーを稼働させています。
ご存知の通りEC2は従量課金となっており起動している時間分費用が発生します。
本番環境であれば24時間稼働が当たり前なのですが、検証や動作確認を行うためのステージング環境や開発環境用に立てているEC2インスタンス、利用者がいない時間に稼働させるのって無駄だと思いませんか?
節約は小さいところからコツコツと。
今日はAWSの節約術の定番、開発系EC2インスタンスを夜間停止をLambdaで行う方法をご紹介します。
公式ドキュメントにpythonを使った例がありますので、今回はjavascriptを使っていきたいと思います。
IAMロールの作成
LambdaからEC2インスタンスの停止/開始が行える様、IAMロールを作ります。
AWSのコンソールからLambdaのIAMロールの作成を行ってください。

権限はAmazonEC2FullAccessを与えます(アクセス権が広すぎると気になる方は公式ドキュメントの様にIAMポリシーを作成しても良いと思います)

名前をつけてロールを作成します、名前はなんでも良いです。
今回は「EC2InstanceControlRole」と名付けました。

「ロールの作成」クリックでLambdaからEC2インスタンスを操作するためのIAMロールが作成されます。
Lambda関数の作成
続いて制御を行うLambda関数を用意します。
名前を「SaveCostEC2」、ランタイムをnodejsにし、先ほど作ったIAMロールを割り当てて作成します。

作成したらコードを書いていきましょう。
公式ドキュメントの方法ではインスタンスID指定で制御していますが、メンテナンスなどでインスタンスが再起動したらインスタンスIDが変わる可能性もあります。
今回はタグを指定して対象のインスタンスを特定する形にします。
Lambda組み込みのAWS-SDKを使います。
ec2.describeInstancesのパラメタのフィルターでタグが指定できますので使っていきましょう!
const AWS = require("aws-sdk");
AWS.config.update({ region: process.env.AWS_REGION });
const describeInstances = async (ec2) => {
return new Promise((resolve, reject) => {
ec2.describeInstances(
{
Filters: [{ Name: "tag:Economize", Values: ["true"] }],
},
(err, data) => {
if (err) {
reject(err);
return;
}
const instances = data.Reservations.reduce(
(acc, current) => acc.concat(current.Instances),
[]
);
resolve(instances);
}
);
});
};
exports.handler = async (event) => {
const ec2 = new AWS.EC2({ apiVersion: "2016-11-15" });
const instances = await describeInstances(ec2);
console.log(instances.map(i => i.InstanceId))
};
「Economize」というタグに”true”が設定されているインスタンスを取得し、そのインスタンスIDをコンソール出力しています。
まずは意図通りにインスタンス情報が取れているか確認します。
早速実行したいところですが、Lambdaの実行時間はデフォルトで3秒になっているのでタイムアウトしてしまう可能性があります。
先に「設定」>「基本設定」からタイムアウトの時間を1分に延長します。

続けて適当にEC2インスタンスを何本か起動し、「Economize」を設定してみましょう。
EC2インスタンスの起動が終わったらAWSのコンソールから実行してみます。
「Deploy」をクリック後、「Test」をクリックしてください。
(「Test」クリック後はテストイベントの作成が促されますが、Hello-worldのテンプレートのままで適当に名前をつけてもらってOKです)
処理結果のログに先ほど起動してタグを設定したEC2インスタンスのIDが表示されてれば成功です!

処理対象のインスタンスの情報を取得できることを確認しました。
続けてインスタンス開始の処理を追加します。
AWS-SDKのec2.startInstancesを使ってインスタンスの開始指示を行います。
(開発環境ということもありますので、インスタンス開始の完了を待つコードは入れません)
const startInstances = async (ec2, instances) => {
return new Promise((resolve, reject) => {
ec2.startInstances(
{
InstanceIds: instances.map((i) => i.InstanceId)
},
(err, data) => {
if (err) {
reject(err);
return;
}
resolve();
}
);
});
};
続けて同じ要領で停止の処理を追加します。
const stopInstances = async (ec2, instances) => {
return new Promise((resolve, reject) => {
ec2.stopInstances(
{
InstanceIds: instances.map((i) => i.InstanceId)
},
(err, data) => {
if (err) {
reject(err);
return;
}
resolve();
}
);
});
};
次にhandlerの現在コンソールに出力している部分を上記処理を呼ぶ形に修正します。
exports.handler = async (event) => {
const ec2 = new AWS.EC2({ apiVersion: "2016-11-15" });
const instances = await describeInstances(ec2);
if (event.action === "stop") {
await stopInstances(ec2, instances);
} else {
await startInstances(ec2, instances);
}
};
event.actionに設定されている値でどちらの処理をするか呼び分けています。
先ほど「Hello-world」で作ったテンプレートのイベントを以下の様なjsonに修正して動作確認を行ってください。
{
"action": "stop"
}
実行後、対象のインスタンスが停止し、actionをstartに変えることで対象のインスタンスが開始する様になったと思います。
トリガー登録
次にLambdaを実行するためのトリガーを追加します。
火-土のAM0:00に停止したいので、EventBridge (CloudWatch Events)のトリガーを登録します。

トリガーが無事追加されたと思います。
このままだとevent.actionが指定されませんのでリンクをクリックしてEventBridgeの詳細画面を開きます。

設定項目はいくつかありますが、「ターゲットを選択」セクションの中に「入力の設定」というものがありますので、それを展開してJSONテキストを設定し{“action”:”stop”}と入力してください。

そのまま更新すればOKです。
続けて同じ様に月-金のAM8:00にインスタンスを開始するトリガーを追加します。


以上で設定は終わりです。
Lambdaの実行結果はCloudWatchに出力されます、ちゃんと動いているか気になる方は確認してみてください。
まとめ
いかがだったでしょうか、私はjavascriptが手に馴染んているので採用しましたがコードが若干冗長気味なのは否めませんね。。
ただpythonでもjavascriptでも良いのでサーバーがなくてもこういう形でインフラを操作できる、というのは覚えておいた方が良いと思います!
RDSやElasticSearch Serviceなど起動しているだけで費用がかかるサービスは他にもありますので費用対効果などを鑑みつつコツコツ節約していきましょう。
それではまた。