はじめに
この記事では、AWS上で特定のインスタンスのタグ(例えば本番環境productionを表すprdなど)が付与されているインスタンス以外のDBインスタンスを停止するイベントを作成したのでその内容についてまとめます。
アーキテクチャ
以下の図のようにCronで特定の時間帯でEventBridgeをトリガーし、ターゲットとしてLambdaを指定します。
Lambdaでは、複雑なタグのフィルタかつDBの停止をLambda関数上で実施します。
停止対象から除外するタグキー/値は、Lambdaの環境変数上で定義します。
下記図のように、特定のタグキー/値が付与されているインスタンス以外を停止するような関数を提案します。
特定タグ以外のRDSインスタンスを抽出するjmespathの記法
現在起動中のインスタンスの状態がavailable
で且つ、タグEnvの値がprd以外のインスタンスを除外するようなjmespathの構文を作成します。
実行コマンドは以下の通りとなります。
aws rds describe-db-instances \
--query 'DBInstances[?(DBInstanceStatus==`available` && (TagList[?Key==`Env`] | [].Value != [`prd`])) ].DBInstanceIdentifier'
Terraform テンプレート
上記を実行するためのTerraformは以下のリンクの通りになります。
今回使用するランタイムはPython3.12になります。
Lambda用の関数をメンテナンスすることもあるため、source_code_hash
を入れることでLambdaのソースコードの変更を検知し、変更をapplyします。
除外対象のタグはLambdaの環境変数
-
EXCLUDE_TAG_KEY
でタグキー -
EXCLUDE_TAG_VALUE
でタグ値
をそれぞれ定義します。
##############################
# Variables
##############################
variable "pj_tags" {
type = object({
name = string
env = string
})
default = {
name = "hoge"
env = "test"
}
}
variable "iam_role_prefix" {
type = object({
is_create_iam_role = optional(bool, false)
existing_lambda_role_arn = optional(string, null)
})
}
variable "cwl_retention_indays" {
type = number
default = 30
}
variable "lambda_env_variables" {
type = object({
exclude_tag_key = optional(string, "Env")
exclude_tag_value = optional(string, "prd")
})
}
variable "events_prefix" {
type = object({
schedule_expression = optional(string, "cron(0 18 ? * * *)")
})
}
locals {
# create_iam_role = var.iam_role_prefix.is_create_iam_role ? 1 : 0
# flow_log_destination_arn = local.create_iam_role ? try(aws_cloudwatch_log_group.main[0].arn, null) : var.flow_log_destination_arn
create_iam_role = var.iam_role_prefix.is_create_iam_role ? 1 : 0
lambda_function_name = format("%s-%s-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
}
##############################
# IAM
##############################
data "aws_iam_policy_document" "lambda_assume_role" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
data "aws_iam_policy_document" "rds_stop" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
statement {
effect = "Allow"
actions = [
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:StopDBInstance",
"rds:StopDBCluster"
]
resources = ["*"]
}
}
resource "aws_iam_policy" "rds_stop" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
name = format("%s-%s-iam-policy-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
policy = data.aws_iam_policy_document.rds_stop[0].json
tags = {
Name = format("%s-%s-iam-policy-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
resource "aws_iam_role" "lambda" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
name = format("%s-%s-iam-role-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role[0].json
tags = {
Name = format("%s-%s-iam-role-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
resource "aws_iam_role_policy_attachment" "rds_stop" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
role = aws_iam_role.lambda[0].name
policy_arn = aws_iam_policy.rds_stop[0].arn
}
resource "aws_iam_role_policy_attachment" "lambda_execute" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
role = aws_iam_role.lambda[0].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
data "aws_iam_policy_document" "events_assume_role" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
data "aws_iam_policy_document" "lambda_invoke" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
statement {
effect = "Allow"
actions = [
"lambda:InvokeFunction"
]
resources = ["*"]
}
}
resource "aws_iam_policy" "events" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
name = format("%s-%s-iam-policy-events-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
policy = data.aws_iam_policy_document.lambda_invoke[0].json
tags = {
Name = format("%s-%s-iam-policy-events-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
resource "aws_iam_role" "events" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
name = format("%s-%s-iam-role-events-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
assume_role_policy = data.aws_iam_policy_document.events_assume_role[0].json
tags = {
Name = format("%s-%s-iam-role-events-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
resource "aws_iam_role_policy_attachment" "events" {
count = var.iam_role_prefix.is_create_iam_role ? 1 : 0
role = aws_iam_role.events[0].name
policy_arn = aws_iam_policy.events[0].arn
}
# CloudWatch Log Group for Lambda
resource "aws_cloudwatch_log_group" "rds_stop" {
name = "/aws/lambda/${local.lambda_function_name}"
retention_in_days = var.cwl_retention_indays
tags = {
Name = format("%s-%s-log-group-lambda-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
##############################
# Lambda
##############################
data "archive_file" "function" {
type = "zip"
source_file = "${path.module}/function/lambda_function.py"
output_path = "${path.module}/function/lambda_function.zip"
}
resource "aws_lambda_function" "rds_stop" {
filename = data.archive_file.function.output_path
source_code_hash = data.archive_file.function.output_base64sha256
function_name = local.lambda_function_name
role = var.iam_role_prefix.is_create_iam_role ? aws_iam_role.lambda[0].arn : var.iam_role_prefix.existing_lambda_role_arn
handler = "lambda_function.lambda_handler" # Replace with your handler function
runtime = "python3.12" # Choose your desired runtime
timeout = 30
memory_size = 128
logging_config {
log_format = "Text"
}
environment {
variables = {
EXCLUDE_TAG_KEY = var.lambda_env_variables.exclude_tag_key
EXCLUDE_TAG_VALUE = var.lambda_env_variables.exclude_tag_value
}
}
tags = {
Name = local.lambda_function_name
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
depends_on = [
aws_cloudwatch_log_group.rds_stop,
aws_iam_role.lambda[0],
]
}
##############################
# EventBridge (CloudWatch Events) Rule
##############################
resource "aws_cloudwatch_event_rule" "schedule" {
name = format("%s-%s-event-rule-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
description = "Schedule for RDS stop Lambda function"
schedule_expression = var.events_prefix.schedule_expression
tags = {
Name = format("%s-%s-event-rule-rdsStop-%02d", var.pj_tags.name, var.pj_tags.env, 1)
PJ = var.pj_tags.name
Env = var.pj_tags.env
}
}
##############################
# EventBridge Target
##############################
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.schedule.name
target_id = "LambdaFunction"
arn = aws_lambda_function.rds_stop.arn
role_arn = var.iam_role_prefix.is_create_iam_role ? aws_iam_role.events[0].arn : var.iam_role_prefix.existing_events_role_arn
}
##############################
# Lambda permission to allow EventBridge to invoke the function
##############################
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.rds_stop.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.schedule.arn
}
##############################
# Outputs
##############################
output "cwl_arn" {
value = aws_cloudwatch_log_group.rds_stop.arn
}
output "lambda_arn" {
value = aws_lambda_function.rds_stop.arn
}
output "lambda_code_sha256" {
value = aws_lambda_function.rds_stop.code_sha256
}
output "events_id" {
value = aws_cloudwatch_event_rule.schedule.id
}
上記の内容はGitHub上だとモジュール化されているため、呼び出す際は以下のような関数を
module "events_rds_stop" {
source = "/path/to/modules/events/rds_stop"
# プロジェクトで使用する環境識別子など
pj_tags = {
name = "hoge"
env = "test"
}
# 新規でiamロールを作成する場合、true, falseの場合、既存のロールを指定
iam_role_prefix = {
is_create_iam_role = true
}
# Lambdaに定義する環境変数(除外タグ)
lambda_env_variables = {
exclude_tag_key = "Env" # 除外タグキー
exclude_tag_value = "prd" # 除外タグ値
}
# EventBridgeでのスケジュール(cron式)
events_prefix = {
schedule_expression = "cron(0 15 ? * * *)"
}
}
Lambdaの環境変数はいかのように定義しました。
import os
import json
import jmespath
import boto3
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
tag_key = os.environ.get('EXCLUDE_TAG_KEY')
tag_value = os.environ.get('EXCLUDE_TAG_VALUE')
def lambda_handler(event, context):
# TODO implement
rds_client = boto3.client('rds')
rds_instances = rds_client.describe_db_instances()
regex = "DBInstances[?(DBInstanceStatus==`available` && (TagList[?Key==`{}`] | [].Value != [`{}`])) ].DBInstanceIdentifier".format(tag_key, tag_value)
val = jmespath.search(regex, rds_instances)
logger.info("stop rds instances. {}".format(val))
response = {
"StopDBInstances": val
}
for instance in val:
logger.info("stop rds instance {}".format(instance))
rds_client.stop_db_instance(
DBInstanceIdentifier = instance
)
return {
'statusCode': 200
}
以上です。jmespathによる構文で、シンプルにLambda関数もひょうげんすることができます。