定期的に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のスタックを実行してデプロイすることができます。
最後に
ちゃっかりブログのネタにしていますが
社内で一番消し忘れが多いのは実は僕ですごめんなさい。。。
1