こんにちは、Mashです。
様々なお勉強目的で、AWS EC2をご利用の方も結構いらっしゃるかと思います。
わたしもご多分に漏れず、ちょっとしたプログラミングやLinuxコマンドの学習環境としてよく利用します。
また、本業のほうでもチームメンバー全員がパッケージソフトの検証用でEC2を頻繁に利用します。
EC2はめっちゃ便利なサービスなのですが、よくある課題がこちら↓。
EC2の停止忘れによる無駄コスト。
EC2はインスタンスを起動している時間に応じて課金される仕組みになっています。(リザーブドインスタンス除く)
もちろん実際に利用している分に関してはしかるべき費用を払うべきですが、インスタンスを停止し忘れて発生している余計なコストは即刻削減すべきです。
今回はそんな「EC2停止忘れ問題」への対処法として、AWS LambdaとCloudWatch Eventを利用したインスタンス自動停止処理をご紹介します。
全体アーキテクチャ
まずはアーキテクチャの全体像から。

①時間ベースのトリガー(CloudWatch Event)
EC2インスタンスを停止する処理をAWS Lambdaで実装しますが、Lambda関数をトリガーする仕組みとして、CloudWatch Event(最近名称が変わって EventBridge)を利用します。
トリガールールは時間ベースとし、毎日3回(朝昼晩)にしてみました。
このあたりはお好みで変更いただいて問題ありません。
②EC2インスタンスの停止処理(Lambda)
こちらが本丸。AWS Lambdaにインスタンス停止処理を実装します。
今回実装しておきたかった機能としては以下が挙げられます。
- 基本的には稼働中インスタンスをすべて停止する
- とはいっても、長時間起動しっぱなしにしておきたい場合もあるため、EC2インスタンスのタグを見て停止除外対象を判別する
- 意図せず停止してしまった、もしくは意図せず起動し続けているインスタンスがないか気づきを得るため、実行結果をスマホへ通知する
このような機能を盛り込みました。
③④処理内容を通知
前述の通り、Lambdaの実行結果を通知しておきたいのでLINE Notify APIを利用します。
料金
今回ご紹介する仕組みを実装した場合のコストについて触れておきます。
つらつらと書いていますが結論からお伝えすると、無料で実現できます。
Lambdaの料金
Lambdaの課金体系は2つあるので、それぞれ見ていきます。
リクエスト数
前述の通り、Lambda関数を毎日朝昼晩の3回実行する想定としています。
ですので1ヶ月あたりのLambda実行回数は
3回/1日 × 31日 = 93回
です。
1リクエストあたりの単価が 0.0000002USD なので、
93 × 0.0000002USD = 0.0000186USD ≒ 0.001953円
となります。(安っ!)
が!
なんとLambdaには「1ヶ月ごとに100万件の無料リクエスト」枠があります。
93回なんてぜんぜん余裕ですね。笑
ということで、Lambdaのリクエスト数課金は無料!
実行時間
つづいてLambdaの実行時間課金をみていきます。
わたしの検証環境で実行したところ、今回ご紹介するLambdaの実行結果は以下の通りでした。

ポイントは
- 課金対象となる実行時間が1600ms
- Lambdaへの割当メモリが128MB
の2点ですね。
もちろんみなさんのEC2インスタンス数などにも依存するかと思いますが、よほどの超大規模でないかぎりは同じような数値になるかと思います。
ということで、ざっくり計算してみますと
128MB割り当て時の料金(1sec)× 実行時間 =
0.000002083USD × 2秒 × 93回 = 0.000387438USD ≒ 0.04068099円
となります。(安っ!)
が!
もうおわかりですね。実行時間にも「1ヶ月ごとに40万GB-秒」の無料枠があります。
計算過程は省略しますが、さきほど↑計算した内容だと23.25GB-秒しか利用していません。
40万GB-秒どころか100GB-秒も利用していません。
ということで、Lambdaのコンピュート課金も無料!
CloudWatch Eventの料金
CloudWatch関連の料金はこちらのページになります。
無料利用枠のイベント項には「カスタムイベントを除くすべてのイベントが対象」と記載があります。
今回はAWS標準で利用可能な時間ベースのトリガーイベントを作成するだけですので無料になります!
AWSからのアウトバウンド通信
Lambdaで実行した処理内容をLINE Notifyへメッセージを送信するアーキテクチャとしました。
そのためAWSからインターネットへのデータ転送には料金がかかる可能性があるので、確認してみましょう。
Lambdaの料金ページから抜粋します。
データ転送
Lambda 関数が実行されたリージョン外からの AWS Lambda 関数に対する、または Lambda 関数からのデータの転送は、このページの「データ転送」に記載されている EC2 データ転送料金が課金されます。
リンクをたどるとEC2の料金ページにたどりつきまして、そこには以下の記載が。
Amazon EC2 からインターネットへのデータ転送 (アウト)
1 GB/月まで:0.00USD/GB
今回画像などの重いデータは一切扱いませんので、1GBを超えることはありません。
アウトバウンド通信も無料!
実装
さて、おまたせいたしました。
今回の仕組みが無料でご利用いただけることがわかったところで、実装方法のご紹介となります。
やることは大きく5つです。
- LINE Notify APIの準備
- Lambda関数の作成
- トリガー(CloudWatch Event)の作成
- 停止除外対象インスタンスへのタグ付け
- 動作確認
LINE Notify APIの準備
LINE Notifyの使い方については以前記事にしましたのでこちらをご参照ください。
こちらで作成したAPI TOKENを使用して通知を行います。
Lambda関数の実装
つづいて主役のLambda関数の作成です。
もしLambdaをまったく触ったことがないという方は、こちらのチュートリアル記事をご一読いただければ参考になるかと思います。
コードの実装
Lambda関数のコードをご紹介します。
こちらはPython 3.8での実装になっています。
# -*- coding: utf-8 -*- import boto3 import os import json import urllib.parse import urllib.request # 環境変数 nostop_tag_name = os.environ.get('nostop_tag_name') nostop_tag_value = os.environ.get('nostop_tag_value') line_notify_api = os.environ.get('line_notify_api') line_notify_token = os.environ.get('line_notify_token') def lambda_handler(event, context): message = stop_ec2() if message: return notify_to_line(message) else: return { 'statusCode': 200, 'body': json.dumps('Do nothing') } def stop_ec2(): ec2 = boto3.resource('ec2') # 稼働中インスタンスリスト running_instances = [ i for i in ec2.instances.filter(Filters=[{'Name': 'instance-state-name', 'Values': ['running']}])] # 停止除外インスタンスリスト nostop_instances = [ i for i in ec2.instances.filter(Filters=[{'Name': f'tag:{nostop_tag_name}', 'Values': [f'{nostop_tag_value}']}])] # 停止対象インスタンスリスト target_instances = [ target for target in running_instances if target.id not in [i.id for i in nostop_instances]] # 稼働中停止除外インスタンスリスト alart_instances = [ alart for alart in running_instances if alart.id in [i.id for i in nostop_instances]] notification_message = "" if target_instances: notification_message = "\n以下のインスタンス停止しました\n" for instance in target_instances: instance_id = instance.id instance.stop() notification_message += f'{instance_id}\n' if alart_instances: notification_message += '\n以下のインスタンスは稼働中です\n' for instance in alart_instances: instance_id = instance.id notification_message += f'{instance_id}\n' return notification_message def notify_to_line(message): method = "POST" headers = {"Authorization": "Bearer " + line_notify_token} payload = {"message": message} try: payload = urllib.parse.urlencode(payload).encode("utf-8") req = urllib.request.Request(line_notify_api, data=payload, method=method, headers=headers) urllib.request.urlopen(req) return message except Exception as e: return e
環境変数の定義
Lambda関数内で使用するいくつかのパラメータは、セキュリティ面やあとから変更しやすいように、ハードコードするのではなく環境変数に持たせることにしました。

キー | 値 | 説明 |
---|---|---|
line_notify_api | https://notify-api.line.me/api/notify | LINE Notify APIのURL。みんな同じ |
line_notify_token | ※取得したAPI TOKEN | ご自身で取得したLINE NotifyのAPI TOKEN。みんな違う |
nostop_tag_name | nostop | 強制停止対象から除外したいEC2インスタンスに付与するタグの名前。カスタマイズOK |
nostop_tag_value | true | 強制停止対象から除外したいEC2インスタンスに付与するタグの値。カスタマイズOK |
トリガー(CloudWatch Event)の実装
そして、Lambda関数をキックするためにCloudWatch Eventを設定します。
まずAWSマネジメントコンソールの CloudWatch ページへアクセスし、左側メニューの [ルール] をクリックします。

[ルールの作成] ボタンをクリックします。

イベントソースはスケジュール / Cron式 を選択します。
Cronの記載方法は少々クセ強めなので慣れるまで少し大変なのと、CloudWatch Eventでは時刻がUTC基準になっています。日本時間JSTとは9時間差があるのでご注意ください。
このような記載で 毎日朝7時(JST)のトリガー になります。

イベントソースが設定できたら、画面右側の ターゲット を定義します。
こちらはシンプルに実行したいLambda関数を選択するだけでOKです。

最後の作成したイベントルールの名前と説明を定義して [ルールの作成] ボタンをクリックすれば完了です。

以上で朝7時のトリガーを作成できました。
同じ手順でお好みの時間にLambdaが実行されるトリガーを作成してみてください。
(わたしは7時、15時、22時の3回分トリガーを作りました)
停止除外対象インスタンスへのタグ付け
もし強制停止させたくない(あえて長時間起動しっぱなしにしておきたい)EC2インスタンスがある場合は、タグを付与する必要があります。
今回ご紹介している手順では、インスタンスに「タグ名:nostop」「値:true」を付与することで停止対象から除外することが可能です。

動作確認
さて、準備が整いましたので動作確認してみましょう。
テスト用にEC2インスタンス3台を起動した状態にしていて、lambda-test-02には停止対象除外タグ(nostop / true)を付与しています。

この状態でトリガーが発動すると、、、

意図したとおり、1号機と3号機だけ停止してくれました!
LINEの通知はどうかというと、、、

停止したインスタンスと停止除外になったインスタンスを通知してくれましたねー
まとめ
以上、いかがでしたでしょうか。
AWSは便利な反面、EC2の止め忘れのような「うっかり」によってまぁまぁいい金額を請求されてしまうデメリットもあります。
このようなミスはいくら気をつけていても起こってしまうものなので、しっかりと仕組みで対処していきましょう。
今回は以上です。
それじゃあまたね。