きっかけ
個人のAWSアカウント上でハンズオンのために基本的なAWSリソースが使用できるようにしていて、Amazon Auroraもその対象に含めているのですが、テスト用でr5.largeの最低限の構成でAuroraクラスターを起動しているだけでもそれなりのコストがかかってしまうのに加えて、Amazon RDSにしてもAmazon Auroraにしてもそうなのですが、手動で停止しても7日間経過すると自動的に起動するという謎仕様になっているので、コスト削減のためにも、必要な時だけ起動して、それ以外は定期的に起動状態をチェックして、もしも起動していたら止める、という仕組みを作れないかなと考えていました。
で、ふと気が付いたのですが、そういえば前回Amazon EC2インスタンスの起動停止をAmazon EventBridge + AWS Lambdaでコントロールする仕組みを既に導入していたので、これをそのまま応用すればいいじゃない、ということで、サクッと実装してしまおうということになったのでした。
Lambda関数作成時にまずは実行ロールの設定を
例によってLambda関数を作成していくのですが、今回は気をつけなければいけない点がひとつ。Lambda関数からAuroraに対する実行ロールを作成しなければいけません。一旦デフォルトの設定で実行ロールを作成した場合は、作成されたロールに対してIAMポリシーを追加してあげる必要があります。でないと、Lambda関数を実行した時点でアクセス権限エラーが出力されます。
今回はひとまず、Amazon Auroraがプライベートサブネット上に構成されていることと、インターネット側からLambda関数が実行されることがないので、取り急ぎAWS管理ポリシーであるAmazonRDSFullAccessをつけてしまいます。ただし、これはあくまでもとり急ぎなので、最小権限の原則に基づいて後々必要な権限のみをアタッチするカスタマー管理ポリシーを実装することにしましょう。
Lambda関数の実装
基本的にやっていることは、EC2インスタンスの自動的な起動停止と一緒で、Auroraクラスターに付与したタグを持ってきて、タグに合致するAuroraクラスターを丸ごと停止させてしまいましょう、という内容です。コードはこちら。
import boto3
import logging
import traceback
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
region = event['Region']
client = boto3.client('rds', region)
rds_clusters = client.describe_db_clusters().get('DBClusters', [])
print ("Found " + str(len(rds_clusters)) + " Aurora Clusters")
for rds_clusters in rds_clusters:
try:
cluster_status = rds_clusters['Status']
cluster_ids = rds_clusters['DBClusterIdentifier']
cluster_arn = rds_clusters['DBClusterArn']
print ("DBClusterIdentifier is %s and Status is %s" % (cluster_ids, cluster_status))
tags = client.list_tags_for_resource(ResourceName=cluster_arn).get('TagList', [])
print ("Tags is %s" % tags)
for tags in tags:
if tags['Key'] == 'AutoStop':
print ("Current instance_state of %s is %s" % (cluster_ids, cluster_status))
if cluster_status == 'available':
client.stop_db_cluster(DBClusterIdentifier=cluster_ids)
print ("Aurora Cluster %s comes to stop" % cluster_ids)
else:
print ("Instance %s status is not right to start or stop" % cluster_ids)
return {
"statusCode": 200,
"message": 'Started automatic stop Aurora clusters process. [Region: {}]'.format(event['Region'])
}
except Exception as e:
logger.error(e)
print(traceback.format_exc())
return {
"statusCode": 500,
"message": 'An error occured at automatic stop Aurora clusters process.'
}
ここで躓いたポイントとしては、Auroraクラスターに設定したタグの情報を取得してくること。EC2インスタンスの時は、
responce = client.describe_instances(Filters=[{'Name': 'tag:AutoStartStop', 'Values': ['1']}])
でいけたのですが、同じ要領でコードを書いても、テスト実行時にどうもうまくいかなかったので、一旦起動しているAuroraクラスターの一覧を取得して、各クラスターごとに、arnを取得してそこに紐づいているタグを取得してくる方式に実装方法を変更しました。さらに、EC2インスタンスの時はタグのvalueの値によって制御していましたが、今回はタグのkeyが存在するかどうかで判定する方式に変更。
今回作成したLabda関数のワークフロー
あとはEventBridgeをトリガーとしてcron式とlambda_handlerに送り込むイベント引数を追加して設定し、実行に失敗した時だけ自分の携帯電話にSMSを送信するAmazon SNSトピックを設定して終了です。
この方法、今回はAuroraクラスターに対して実行しているため、stop_db_clusterメソッドを実行することでクラスターに紐づいているrdsインスタンスを一気に落としていますが、Amazon RDSを使用している場合でも、ここのメソッドを書き換えてあげるだけで各々のrdsインスタンスを自動停止したいときに適用できるので使い回しが効くと思います。
自動化する方法は意外とハードルが低いかも
もちろん適切なマネージドサービスがあればそれを使うに越したことはないのですが、コードの実装に対する気分的なハードルを下げることと、Lambda関数の仕組みの理解のためにも、せっせとPythonを書いてこのような形で自動化できそうな仕組みは可能な限り自動化していこうと思います。
まぁエレガントではない泥臭い方法なのかも知れませんが、このようなアプローチでいけば、さまざまな処理を自動化していく方法は意外とハードルが低いのかもしれませんね。