定期的にEC2インスタンスを削除するLambda関数をデプロイした話
インフラエンジニアの寺岡です。
今回は社内向けにLambda関数をデプロイした話をまとめてみたいと思います。
弊社では運用でお預かりしているお客様のサーバが稼働しているAWSアカウント以外にも
社内のエンジニアのメンバーが技術的な検証をするためのアカウントも管理しています。
このアカウント、ログイン周りのセキュリティ設定(IAMユーザー・ロール/MFA認証)はルールを定めて設定していますが
アカウント内での操作は権限を持っているメンバーであれば特に制約なく自由に扱えるようにしています。
検証用なのにあれこれルールが多すぎると扱いにくくなってしまいますからね。
検証の過程で最も多く作成されるリソースは弊社の場合はEC2インスタンスですが
自由に作成できる反面、停止忘れや削除忘れが割と多発している状況でした。
当然多発するとその分無駄なコストがかかってしまいます。
使い終わったら必ず停止か削除するという前提はありますが
忘れてしまった場合の予防線としてLambdaによって定期的にEC2インスタンスを自動削除する仕組みを導入しました。
ということで何をやったのかを下記にまとめます。
Lambda関数の作成
Pythonで書きました。
個人的にGolangの方が詳しいのでそっちで書けばよかった感。
import json import boto3 import os import requests def terminate(event, context): EXCLUDE_TERMINATE_TAG = os.environ['EXCLUDE_TERMINATE_TAG'] CHATWORK_API_KEY = os.environ['CHATWORK_API_KEY'] CHATWORK_ROOM_ID = os.environ['CHATWORK_ROOM_ID'] CHATWORK_ENDPOINT = os.environ['CHATWORK_ENDPOINT'] ec2 = boto3.client('ec2') without_tag = ec2.describe_instances() with_tag = ec2.describe_instances( Filters=[{ 'Name': 'tag:' + EXCLUDE_TERMINATE_TAG, 'Values': ['true'] }] ) without_tag_set = set(ec2['InstanceId'] for resId in without_tag['Reservations'] for ec2 in resId['Instances']) with_tag_set = set(ec2['InstanceId'] for resId in with_tag['Reservations'] for ec2 in resId['Instances']) target_instances = without_tag_set - with_tag_set list_target_instances = list(target_instances) if len(list_target_instances) != 0: terminateInstances = ec2.terminate_instances( InstanceIds=list_target_instances ) notify_instances = '' if len(with_tag['Reservations']) != 0: for reservation in with_tag['Reservations']: for instance in reservation['Instances']: if len(instance['Tags']) != 0: instance_name = '' for tag in instance['Tags']: if tag['Key'] == 'Name': instance_name = tag['Value'] instance_state_enc = json.dumps(instance['State']) instance_state_dec = json.loads(instance_state_enc) if instance_name != '': notify_instances += instance['InstanceId'] + ' -> ' + instance_name + '(' + instance_state_dec['Name'] + ')'+ '\n' else: notify_instances += instance['InstanceId'] + ' -> ' + 'None' + '(' + instance_state_dec['Name'] + ')'+ '\n' message = '[To:350118][To:1786285][info][title][Beyond POC] 退役除外されたインスタンスがあるよ(devil)[/title]' + notify_instances + '[/info]' PostChatwork(CHATWORK_ENDPOINT,CHATWORK_API_KEY,CHATWORK_ROOM_ID,message) response = { "TerminateInstances": list_target_instances } print (response) return response def PostChatwork(ENDPOINT,API_KEY,ROOM_ID,MESSAGE): post_message_url = '{}/rooms/{}/messages'.format(ENDPOINT, ROOM_ID) headers = { 'X-ChatWorkToken': API_KEY } params = { 'body': MESSAGE } resp = requests.post(post_message_url, headers=headers, params=params)
コードを見ていただければわかるかなと思いますが
- 除外タグが付いていないインスタンスを削除する
- 削除対象から除外されたインスタンスをチャットワークに通知する
という処理をやってくれます。
問答無用で消されてしまうと困る場合があるので
その場合は除外タグ「EXCLUDE_TERMINATE:true」を付与してね
というルールで社内周知しています。
Serverless Frameworkの設定
書いたコードをどのようにLambdaにデプロイするのかというお話。
Serverless Frameworkというツールを利用しています。
https://serverless.com
これはLambdaなどのサーバレス系のサービスに対して
所定の設定ファイルに従って関数をデプロイしてくれるCLIツールになっていて
Lambdaの場合はCloudFormationと連携してデプロイを実行してくれます。
設定ファイルはserverless.ymlという名前で用意します。
# Welcome to Serverless! # # This file is the main config file for your service. # It's very minimal at this point and uses default values. # You can always add more config options for more control. # We've included some commented out config examples here. # Just uncomment any of them to get that config option. # # For full config options, check the docs: # docs.serverless.com # # Happy Coding! service: beyond-poc plugins: - serverless-prune-plugin provider: name: aws runtime: python3.8 profile: ${opt:profile, ''} region: ap-northeast-1 role: [IAM Role Name] environment: EXCLUDE_TERMINATE_TAG: EXCLUDE_TERMINATE CHATWORK_API_KEY: [ChatWark API KEY] CHATWORK_ROOM_ID: [ChatWark ROOM ID] CHATWORK_ENDPOINT: https://api.chatwork.com/v2 timeout: 10 # my custom env custom: prune: automatic: true number: 5 # you can overwrite defaults here # stage: dev # region: us-east-1 # you can add packaging information here #package: # include: # - include-me.py # - include-me-dir/** # exclude: # - exclude-me.py # - exclude-me-dir/** functions: terminate-instance: handler: handler.terminate events: - schedule: cron(0 15 ? * * *) timeout: 120 memorySize: 128 reservedConcurrency: 1
environmentで環境変数として除外タグとChatWorkの認証情報の設定をしています。
functionsの部分で文字通り関数の設定をしています。
eventsの部分でスケジュールの設定をしているのですが
この設定だとCloudWatchEventsと連携して毎日15:00(UTC)に定期実行してくれます。
JSTだと深夜0:00になりますね。
この設定ファイルがあるディレクトリにデプロイするコードも一緒に保存して
$ sis deploy --profile [AWS Profile Name]
とすればコマンドラインからServerless Frameworkが自動作成した
CloudFormationのスタックを実行してデプロイすることができます。
最後に
ちゃっかりブログのネタにしていますが
社内で一番消し忘れが多いのは実は僕ですごめんなさい。。。