開発環境のRDSやECSは、誰も使っていない夜間や休日も動き続け、その時間の料金もかかります。平日の日中だけ使う環境なら、使わない時間に止めるだけでインスタンスの料金を大きく減らせます。
この記事では、EventBridge SchedulerとApplication Auto Scalingを使い、RDSとECSを決まった時刻に自動で停止・起動する仕組みをTerraformで作ります。
どれくらい安くなるのか
平日の8時から20時まで動かすとすると、稼働時間は1週間(168時間)のうち60時間です。
- 止めない場合:168時間分の料金
- 平日8〜20時だけ動かす場合:60時間分の料金(約36%)
つまり、RDSのインスタンス料金とFargateのタスク料金は、約6割減らせる計算です。
ただし、次の料金は止めている間もかかります。
- RDSのストレージとバックアップ
- NATゲートウェイ、ALB
- Elastic IPなど、時間単位で課金されるリソース
全体像
今回作るものは次の4つです。
- RDSの停止スケジュール:毎日20時にRDSを停止する(EventBridge Scheduler)
- RDSの起動スケジュール:平日7時45分にRDSを起動する(EventBridge Scheduler)
- ECSの停止スケジュール:毎日20時にタスク数を0にする(Application Auto Scaling)
- ECSの起動スケジュール:平日8時にタスク数を元に戻す(Application Auto Scaling)
EventBridge Schedulerは、Lambdaを書かなくてもAWSのAPIを直接呼び出せます。RDSの停止・起動はこれで十分です。
ECSは、EventBridge SchedulerからUpdateServiceを呼んでタスク数を0にする方法もあります。ただ、**ECSサービスにオートスケーリングを設定していると、最小タスク数まで自動で戻されてしまいます。**そこで、ECSはApplication Auto Scalingの「スケジュールに基づくスケーリング」で、最小・最大タスク数ごと切り替えます。
変数の定義
対象のRDSとECSは、既存のものを名前で指定します。
variable "rds_instance_id" {
type = string
}
variable "ecs_cluster_name" {
type = string
}
variable "ecs_service_name" {
type = string
}
variable "ecs_min_capacity" {
type = number
default = 1
}
variable "ecs_max_capacity" {
type = number
default = 2
}
EventBridge Scheduler用のIAMロール
EventBridge SchedulerがRDSを停止・起動するためのIAMロールを作ります。対象のRDSインスタンスに限定して、停止と起動の権限だけを与えます。
data "aws_db_instance" "dev" {
db_instance_identifier = var.rds_instance_id
}
resource "aws_iam_role" "scheduler" {
name = "dev-rds-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" "scheduler" {
name = "dev-rds-stop-start"
role = aws_iam_role.scheduler.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["rds:StopDBInstance", "rds:StartDBInstance"]
Resource = data.aws_db_instance.dev.db_instance_arn
}]
})
}
信頼ポリシーのPrincipalをscheduler.amazonaws.comにすることで、EventBridge Schedulerだけがこのロールを使えるようにしています。
RDSの停止・起動スケジュール
resource "aws_scheduler_schedule" "rds_stop" {
name = "dev-rds-stop"
schedule_expression = "cron(0 20 * * ? *)"
schedule_expression_timezone = "Asia/Tokyo"
flexible_time_window {
mode = "OFF"
}
target {
arn = "arn:aws:scheduler:::aws-sdk:rds:stopDBInstance"
role_arn = aws_iam_role.scheduler.arn
input = jsonencode({
DbInstanceIdentifier = var.rds_instance_id
})
retry_policy {
maximum_retry_attempts = 0
}
}
}
resource "aws_scheduler_schedule" "rds_start" {
name = "dev-rds-start"
schedule_expression = "cron(45 7 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Tokyo"
flexible_time_window {
mode = "OFF"
}
target {
arn = "arn:aws:scheduler:::aws-sdk:rds:startDBInstance"
role_arn = aws_iam_role.scheduler.arn
input = jsonencode({
DbInstanceIdentifier = var.rds_instance_id
})
retry_policy {
maximum_retry_attempts = 0
}
}
}
ポイントは次の3つです。
- target.arn:「arn:aws:scheduler:::aws-sdk:サービス名:API名」の形で、AWSのAPIを直接呼び出せる。inputにはそのAPIのパラメータをJSONで渡す
- schedule_expression_timezone:Asia/Tokyoを指定すると、cron式を日本時間で書ける。UTCへの換算が不要になり、読み間違いも減る
- retry_policy:すでに停止しているRDSに停止APIを呼ぶとエラーになる。何度も再試行させる意味はないので、再試行は0回にしている
ECSの停止・起動スケジュール
ECSサービスをApplication Auto Scalingの対象として登録し、時刻に応じて最小・最大タスク数を切り替えます。
resource "aws_appautoscaling_target" "ecs" {
service_namespace = "ecs"
resource_id = "service/${var.ecs_cluster_name}/${var.ecs_service_name}"
scalable_dimension = "ecs:service:DesiredCount"
min_capacity = var.ecs_min_capacity
max_capacity = var.ecs_max_capacity
}
resource "aws_appautoscaling_scheduled_action" "ecs_stop" {
name = "dev-ecs-stop"
service_namespace = aws_appautoscaling_target.ecs.service_namespace
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
schedule = "cron(0 20 * * ? *)"
timezone = "Asia/Tokyo"
scalable_target_action {
min_capacity = 0
max_capacity = 0
}
}
resource "aws_appautoscaling_scheduled_action" "ecs_start" {
name = "dev-ecs-start"
service_namespace = aws_appautoscaling_target.ecs.service_namespace
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
schedule = "cron(0 8 ? * MON-FRI *)"
timezone = "Asia/Tokyo"
scalable_target_action {
min_capacity = var.ecs_min_capacity
max_capacity = var.ecs_max_capacity
}
}
最小・最大をどちらも0にすると、タスク数は0になります。朝は元の値に戻すので、オートスケーリングもそのまま使えます。
なお、すでに同じECSサービスをaws_appautoscaling_targetで登録している場合は、新しく作らずに既存のリソースを参照してください。
落とし穴1:RDSは停止して7日たつと自動で起動する
RDSは、停止してから7日たつと自動的に起動します。メンテナンスを適用するための仕様で、止めっぱなしにはできません。
この記事の構成では、**停止スケジュールを「平日だけ」ではなく「毎日」にしています。**連休中に自動起動しても、その日の20時にまた停止されます。
落とし穴2:Terraformがタスク数を元に戻してしまう
aws_ecs_serviceでdesired_countを指定していると、夜間にタスク数が0になった状態でterraform applyを実行したとき、Terraformが元の値に戻そうとします。
これを防ぐには、ECSサービス側でdesired_countの差分を無視させます。
resource "aws_ecs_service" "app" {
# ...(他の設定は省略)
lifecycle {
ignore_changes = [desired_count]
}
}
落とし穴3:起動直後はすぐに使えない
RDSの起動には数分かかります。ECSのタスクがRDSより先に起動すると、DB接続に失敗してタスクが再起動を繰り返すことがあります。
この記事のコードでは、RDSの起動をECSより15分早い7時45分にしています。
まとめ
- 使わない時間帯にRDSとECSを止めると、インスタンス料金を大きく減らせる
- RDSはEventBridge SchedulerからAPIを直接呼び、ECSはApplication Auto Scalingで最小・最大タスク数ごと切り替える
- RDSの7日自動起動に備えて、停止スケジュールは毎日実行する
- Terraformとスケジュールが同じ値を取り合わないよう、desired_countはignore_changesに入れる
開発環境のコストは、毎月少しずつ積み上がります。一度仕組みを作っておけば、あとは何もしなくても効果が続きます。