ECS&RDSの起動と停止をLambda無しで自動化してみた

✨目的や背景

AWSのコスト削減のために、本番環境以外のECSとRDSについて、平日夜間と休日は自動で停止するようにしたい。というありふれた目的です。 実はその仕組みはLambdaで構築済みなのですが、最近はLambdaでコードを書かなくても実現ができそうだったので試してみました。

🏗アーキテクチャ

EventBridgeスケジューラを使用して、ECSとRDSを直接停止・起動をさせます。めちゃくちゃシンプルです。 シンプルすぎて図を載せるのもはばかられるのですが一応載せます...。

試したところ結構良さそうだったので、今までのLambda方式から移行しちゃいました。

具体的なTerraformコード

IAM Role

ECS ServiceとRDS用の権限を付与しています。

resource "aws_iam_role" "turn_off_on" {
  name = "turn-off-on-role"

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

resource "aws_iam_policy" "turn_off_on" {
  name = "turn-off-on-policy"

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

resource "aws_iam_role_policy_attachment" "turn_off_on" {
  role       = aws_iam_role.turn_off_on.name
  policy_arn = aws_iam_policy.turn_off_on.arn
}

Scheduler

停止用と起動用としてそれぞれスケジュールを作成する必要があります。具体的には以下のような内容のスケジュールを作ってみます。

種別 対象 内容
停止 ECS Service 平日22:00に0台にする
起動 ECS Service 平日09:00に2台にする

ECS Cluster名とECS Service名を指定する必要がありますが、ここではvariableにしておきます。

variable "ecs_cluster_name" { type = string }
variable "ecs_service_name" { type = string }

EventBridgeスケジューラは、タイムゾーンを指定できて直感的にcron式を書けるから好きです。

# -----------------------------------------------------------------------------
#  Turn off the ECS service
# -----------------------------------------------------------------------------
resource "aws_scheduler_schedule" "turn_off_ecs" {
  name                         = "turn-off-ecs-schedule"
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(0 22 ? * MON-FRI *)"

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ecs:updateService"
    role_arn = aws_iam_role.turn_off_on.arn

    input = jsonencode({
      Cluster      = var.ecs_cluster_name
      Service      = var.ecs_service_name
      DesiredCount = 0
    })
  }

  flexible_time_window {
    mode = "OFF"
  }
}

# -----------------------------------------------------------------------------
#  Turn on the ECS service
# -----------------------------------------------------------------------------
resource "aws_scheduler_schedule" "turn_on_ecs" {
  name                         = "turn-on-ecs-schedule"
  schedule_expression_timezone = "Asia/Tokyo"
  schedule_expression          = "cron(0 9 ? * MON-FRI *)"

  target {
    arn      = "arn:aws:scheduler:::aws-sdk:ecs:updateService"
    role_arn = aws_iam_role.turn_off_on.arn

    input = jsonencode({
      Cluster      = var.ecs_cluster_name
      Service      = var.ecs_service_name
      DesiredCount = 2
    })
  }

  flexible_time_window {
    mode = "OFF"
  }
}

🤔 検討した別の方法(またなぜその方法にしなかったのか?)

ECSのスケジュールベースでのオートスケーリング機能

概要

Application Auto Scalingをaws cliを使って設定することで、ECSのサービスもスケジュールベースでオートスケーリングさせることができます。オートスケールで0台設定することにより自動停止・起動を実現します。

Why not?

一部のECSではこの仕組みを使っていましたが、スケジュールベースの設定はマネコンから設定変更はおろか確認することすらできません。
この仕組みが後々忘れ去れた際、「どうやって自動停止・起動しているのだろう?」となったりして調査をすることになると思うのですが、マネコンから確認ができないとこの仕組みに辿り着くのが難しいと思われるため、採用しませんでした。
というか今回実際「どうやって自動停止・起動しているのだろう?」となって実際に調査に時間がかかりました...

またこの仕組みはECSでは使用できますがRDSでは使用できません。同じ目的の場合は同じ仕組みで実現したいため、採用しませんでした。

EventBridgeスケジューラ + Lambda

概要

ECSやRDSを停止・起動させるLambdaを作成し、EventBridgeスケジューラが決まった時間にそのLambdaをトリガーすることで自動停止・起動を実現します。
なおLambdaをトリガーできればよいので、EventBridgeの代わりにCloudWatch Eventsでも可能です。

Why not?

EventBridgeスケジューラだけで実現する方法と比較して、Lambdaでコードを書く必要があり、またそのコードを保守する必要があるため採用しませんでした。(使っている言語のEOL対応など)

ただしこの方法には対象のECS ServiceやRDSが増えたとしても、EventBridgeスケジューラやLambda自体を増やす必要が無いというメリットがあります。(Lambda内でfor文でまわせば)
逆にEventBridgeスケジューラだけで実現する方法だと、ECS ServiceやRDSが増えるごとに、EventBridgeスケジューラを作る必要があります。 そのためマイクロサービス化していてECS Serviceがいっぱいある場合などだと、Lambdaを使ったほうが逆にシンプルかもしれません。

📝 まとめ

いかがでしたでしょうか?私はEventBridgeスケジューラ単体で、このようなことができるとは今まで知りませんでした。調べれば、既存のLambdaをもっと置き換えできるかもしれないな〜と思いました。