💰

EventBridgeSchedulerでEC2とRDSを停止する

2024/05/15に公開

背景

検証環境やバッチ処理のみを実行しているリソースの夜間停止するために、EventBridge Schedulerを使用することにしました。

個人開発の成果物とかもこれで管理できたらコストカットできてとても良いですね〜。

作成物の概要

  • 平日のAM08:00に起動
  • 平日のPM23:59に停止
  • 実装はTerraform
  • 対象リソースはEC2とRDS
    • タグを付与して対象のインスタンスを特定する

EC2実装

IAM作成

resource "aws_iam_role" "this" {
  name = "eventbridge-scheduler-role"
  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "scheduler.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
	    }
    ]
  })
}

resource "aws_iam_role_policy" "this" {
  name = "ec2-start-stop-policy"
  role = aws_iam_role.this.name

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "ec2:StartInstances",
          "ec2:StopInstances"
        ],
        "Resource": "*"
      }
    ]
  })
}

対象のEC2はタグで抽出、dataで一括で取得する

data "aws_instances" "this" {
  filter {
    name   = "tag:Target"
    values = ["true"]
  }
}

EventBrigdeSchedulerを作成

resource "aws_scheduler_schedule_group" "this" {
  name = "ec2-off-hours-shutdown"
}

resource "aws_scheduler_schedule" "start" {
  name                         = "ec2-start"
  group_name                   = aws_scheduler_schedule_group.this.name
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(0 8 ? * MON-FRI *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
    role_arn = aws_iam_role.this.arn
    # 複数のインスタンスIDを指定出来るので、同じ時間に停止するなら1つのスケジューラーのみで対応できる
    input = jsonencode({
           "InstanceIds": ${jsonencode(instance_ids)}
        })

    retry_policy {
      maximum_event_age_in_seconds = 3600
    }
  }
}

resource "aws_scheduler_schedule" "stop" {
  name                         = "ec2-stop"
  group_name                   = aws_scheduler_schedule_group.this.name
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(59 23 ? * MON-FRI *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
    role_arn = aws_iam_role.this.arn
    input = jsonencode({
           "InstanceIds": ${jsonencode(instance_ids)}
        })
  }
}

RDS実装

IAMをTerraformで作成

resource "aws_iam_role" "this" {
  name = "eventbridge-scheduler-role"
  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
            "Service": "scheduler.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
   })
}

resource "aws_iam_role_policy" "this" {
  name = "ec2-start-stop-policy"
  role = aws_iam_role.this.name

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": [
          "rds:StartDBInstance",
          "rds:StopDBInstance"
         ],
         "Resource": [
           "*"
         ],
         "Effect": "Allow"
      }
    ]
  })
}

対象のEC2はタグで抽出、dataで一括で取得する

data "aws_db_instances" "this" {
  tags = {
    Target = "true"
  }
}

EventBrigdeSchedulerをTerraformで作成

resource "aws_scheduler_schedule_group" "this" {
  name = "rds-off-hours-shutdown"
}

resource "aws_scheduler_schedule" "start" {
  # RDSの再開は対象を1つのリソースしか選択できないため、dataで取得した対象の数だけループしてそれぞれ作成する
  count                        = length(data.aws_db_instances.this.instance_identifiers)
  name                         = "rds-start-for-${data.aws_db_instances.this.instance_identifiers[count.index]}"
  group_name                   = aws_scheduler_schedule_group.this.name
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(0 8 ? * MON-FRI *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:rds:startDBInstance"
    role_arn = aws_iam_role.this.arn
    input = jsonencode({
      "DbInstanceIdentifier": "${data.aws_db_instances.this.instance_identifiers[count.index]}"
    })

    retry_policy {
      maximum_event_age_in_seconds = 3600
    }
  }
}

resource "aws_scheduler_schedule" "stop" {
  count                        = length(data.aws_db_instances.this.instance_identifiers)
  name                         = "rds-stop-for-${data.aws_db_instances.this.instance_identifiers[count.index]}"
  group_name                   = aws_scheduler_schedule_group.this.name
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(59 23 ? * MON-FRI *)"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:rds:stopDBInstance"
    role_arn = aws_iam_role.this.arn
    input = jsonencode({
      "DbInstanceIdentifier": "${data.aws_db_instances.this.instance_identifiers[count.index]}"
    })
  }
}

確認方法

CloudTrailで、イベントソースとイベント名で絞り込んで実行ログを確認

  • EC2
    • イベントソース
      • ec2.amazonaws.com
    • イベント名
      • StartInstances
      • StopInstances
  • RDS
    • イベントソース
      • rds.amazonaws.com
    • イベント名
      • StartDBInstance
      • StopDBInstance

課題

dataを使って対象を取得しているため、RDSのようにインスタンスごとにスケジューラーを作成すると、apply以降に作成されたインスタンスが対象から漏れてしまいます。

現状では、GitHub Actionsのcron機能を使用してplanを実行し、差分があればSlack通知を行うようにしていますが、より良い方法がないかと考えています。

Discussion