AWS 特定のタグキー/値以外のRDSインスタンスを停止するイベントをテンプレート化

## はじめに
この記事では、AWS上で特定のインスタンスのタグ(例えば本番環境productionを表すprdなど)が付与されているインスタンス以外のDBインスタンスを停止するイベントを作成したのでその内容についてまとめます。


## アーキテクチャ
以下の図のようにCronで特定の時間帯でEventBridgeをトリガーし、ターゲットとしてLambdaを指定します。
Lambdaでは、複雑なタグのフィルタかつDBの停止をLambda関数上で実施します。
停止対象から除外するタグキー/値は、Lambdaの環境変数上で定義します。

下記図のように、特定のタグキー/値が付与されているインスタンス以外を停止するような関数を提案します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/872532/3e572fb9-4be4-4d7c-9781-f752a5b957ad.png)


## 特定タグ以外のRDSインスタンスを抽出するjmespathの記法
現在起動中のインスタンスの状態が`available`で且つ、タグEnvの値がprd以外のインスタンスを除外するようなjmespathの構文を作成します。
実行コマンドは以下の通りとなります。

```sh
aws rds describe-db-instances \
    --query 'DBInstances[?(DBInstanceStatus==`available` && (TagList[?Key==`Env`] | [].Value != [`prd`])) ].DBInstanceIdentifier'
```



## Terraform テンプレート

上記を実行するためのTerraformは以下のリンクの通りになります。 

https://github.com/atsw0q0/terraform-aws-ope-templates/tree/main/modules/events_rds_stop


今回使用するランタイムはPython3.12になります。
Lambda用の関数をメンテナンスすることもあるため、`source_code_hash`を入れることでLambdaのソースコードの変更を検知し、変更をapplyします。

除外対象のタグはLambdaの環境変数
- `EXCLUDE_TAG_KEY`でタグキー
- `EXCLUDE_TAG_VALUE`でタグ値
をそれぞれ定義します。

```hcl
##############################
# 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上だとモジュール化されているため、呼び出す際は以下のような関数を

```hcl
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の環境変数はいかのように定義しました。
```python:lambda_function.py
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関数もひょうげんすることができます。