背景
元よりコスト削減のため、検証環境を営業時間外に止める運用をしていました。
前の運用では Instance Scheduler on AWS と EventBridge Scheduler を組み合わせてEC2/RDS/ECSの起動・停止を行なっていました。(Instance Schedulerについてはこちらの記事で詳しく説明しています。)
実際には多数の事業サービスがあり、検証環境を使う時期・使わない時期もサービスごとにバラバラです。そのため、稼働時間はサービスごとに管理したいのですが、既存の構成では次の問題がありました。
| サービス名 | 使用用途 | 運用上の問題 |
| Instance Scheduler | EC2とRDSの起動・停止のスケジュール管理 | ・タグ(スケジュール)を起動させる環境/しない環境ごとに分けて付与 ・RDSの起動に時間がかかるため、RDS専用のタグを作り、先に起動していた →タグの数が多くなり複雑化 ・日本の祝日を考慮した停止ができない(祝日カレンダーの機能がない) |
| EventBridge Scheduler | ECSの起動・停止スケジュール管理 | ・ECSサービスの数×起動/停止の分だけスケジュールが必要 ・日本の祝日を考慮した停止ができない(祝日カレンダーの機能がない) |
そこで、サービス単位で稼働時間を一元管理でき、祝日停止にも対応した仕組みを作成することにしました。
全体構成

- 定期実行:EventBridge Schedulerから5分ごとにLambdaを呼び、「今動いているべきか」を判定して起動・停止します。
- 手動実行:Step Functionsに
{"service": "...", "action": "start" | "end"}を渡すと、サービス単位で起動・停止します。 - 設定:すべてDynamoDBに置いているので、タグを付け替える必要はありません。
DynamoDBのテーブル設計
設定テーブル(パーティションキー: service)
「サービス(=システム×環境)」ごとの稼働ルールです。
| 属性 | 例 | 意味 |
|---|---|---|
| service | dev-system-a | 環境名-システム名(リソースに付けたシステム名のタグに合わせる) |
| enable | true | false にすると、常に停止を要求する |
| manualMode | false | 手動で起動している間は true(定期実行で止められない) |
| weekdays | MON,TUE,WED,THU,FRI | 稼働する曜日(MON〜SUN)。対象外の曜日は停止する |
| skipHolidays | true | 祝日は停止する |
| start / end | 8:30 / 21:00 | 稼働時間。start が空なら自動では起動しない |
リソーステーブル(パーティションキー: service、ソートキー: arn、GSI: arn)
制御するリソースを1件ずつ登録します。リソースの種類ごとに、固有の属性を持たせています。
属性
EC2
Aurora
ECS
説明
service
dev-system-a
パーティションキー。設定テーブルのサービス名
arn
arn:aws:ec2:<region>:<account-id>:instance/i-xxxxxxxx
arn:aws:rds:<region>:<account-id>:cluster:example-cluster
arn:aws:ecs:<region>:<account-id>:cluster/example-cluster
ソートキー(GSIのキー)
resourceType
EC2
RDS_CLUSTER
ECS
リソースの種類
instanceId
i-xxxxxxxx
—
—
起動・停止APIに渡す識別子
dbClusterIdentifier
—
example-cluster
—
clusterName
—
—
example-cluster
serviceName
—
—
{ "example-api-service": { "desiredCount": 1 } }
起動時に戻す desiredCount(ECSサービスごと)
status
off
サービスが起動(on)/停止(off)のどちらを求めているか
start / end
空
値があればリソース個別の時刻。空なら設定テーブルの値を使う
祝日テーブル(パーティションキー: date)
{ "date": "2027-03-22", "description": "振替休日" }
判定ロジックのポイント
1. 時刻の決め方(リソース側が優先、RDSは時刻をずらす)
start = parse_hhmm(instance_record.get("start"))
if start is None:
start = parse_hhmm(service_default.get("start"))
if is_rds:
start = shift_time(start, -RDS_START_OFFSET_MINUTES) # 30分早く起動
AuroraはEC2やECSより起動/停止に時間がかかります。そのため、サービスの設定をそのまま使うときは「起動を30分早く、停止を15分遅く」しました。
例えばサービスに 8:30-21:00 と設定すると、EC2は 8:30〜21:00、Auroraは 8:00〜21:15 で動きます。
start / end の組み合わせによる動きは、Instance Schedulerと同じにしました。
| 設定 | 動き |
|---|---|
| start と end の両方 | 時間内なら起動、時間外なら停止 |
| end だけ | 自動では起動しない。end を過ぎたら停止(ステージング向け) |
| start だけ | start を過ぎたら起動。自動では停止しない |
| どちらもなし | 何もしない |
2. 共有リソースは「起動優先」でまとめて判定する
1つのリソースを複数のサービスで共有することがあります。そこで、5分ごとの判定では ARN単位でまとめて1回だけ 判定しています。
どれか1つのサービスが起動を求めていれば、そのリソースは止めません。
if any(v == START for _, _, v in votes):
desired = START
elif any(v == STOP for _, _, v in votes):
desired = STOP
else:
desired = None # 誰も要求していない → 触らない
ECSは、同じクラスタに複数サービス分のレコードがある場合、serviceName をマージします。desiredCount がレコードごとに違うときは、大きい方を採用します。
さらに、ECSは「desiredCount が登録値以上なら起動済み」とみなし、タスク数を減らさないようにしています。Application Auto Scaling で負荷に応じて増えたタスクを、5分ごとの判定で登録値に戻してしまわないためです。タスク数を変えるのは、登録値を下回っているときに登録値まで上げる場合と、停止時に0にする場合だけです。
count = svc.get("desiredCount")
in_sync = count >= target if should_be_active else count == 0 # 起動中は「以上」で一致とみなす
3. 祝日判定は「コードで計算する祝日」と「テーブルに登録する祝日」の2本立て
- 固定日の祝日(元日、建国記念の日など)と、成人の日・海の日・敬老の日・スポーツの日(第n月曜日)は、コードで判定します。
- 春分の日・秋分の日・振替休日・国民の休日は年によって日付が変わるので、祝日テーブルに登録します。
春分の日・秋分の日は前年の2月に官報で公表されるので、毎年祝日テーブルに追加する運用にしています。
登録が必要な日は、内閣府が公開している祝日CSVとコードの判定を突き合わせると、機械的に洗い出せます。
4. 停止中のRDSも、メンテナンスウィンドウの間は起動する
Auroraには、次の2つの仕様があります。
- 停止できるのは最長7日間で、7日を過ぎると自動で起動する。
- 停止中はメンテナンスが適用されず、次に起動したときに適用される。
そこで、RDSの PreferredMaintenanceWindow の時間帯に入っていれば、enable の設定に関係なく起動を要求するようにしました。
メンテナンスウィンドウの時間帯を過ぎ、RDSの状態が available に戻ると、通常の起動・停止の判定に戻ります。
5. 手動実行と manualMode
Step Functionsの入口では、まず入力をチェックします(service と action があり、action が start または end であること)。
- 手動起動(
start):自分のサービスのレコードだけを起動し、設定テーブルのmanualMode=trueを立てます。これで、定期実行の判定で止められなくなります。 - 手動停止(
end):manualModeを外した前提で、定期実行と同じ判定をします。同じリソースを使っている他のサービスが起動を求めていれば止めません。結果には、止めなかった理由(例:start: dev-system-b)が残ります。 - 失敗したとき:起動・停止に失敗したリソースが1つでもあれば、Step Functionsの実行を失敗にして気づけるようにしています。
まとめ
- タグではなくDynamoDBで設定を管理すると、EC2・RDS・ECSを「サービス」単位でまとめて制御できます。
- 共有リソースは「起動優先」でまとめて判定し、RDSは時刻のずらしとメンテナンスウィンドウを考慮しました。
- 祝日は、計算できるものはコード、計算できないものはテーブルで管理しています。
参考
- Instance Scheduler on AWS: https://docs.aws.amazon.com/solutions/latest/instance-scheduler-on-aws/solution-overview.html
- Aurora DB クラスターの停止と起動: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-cluster-stop-start.html
- 内閣府「国民の祝日について」: https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html
ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。
- 選考ではありません
- 履歴書不要
- 技術の話が中心
- 所要時間30分程度
- オンラインOK