1. はじめに
開発・検証環境のEC2やRDSを24時間365日起動したままにしておくと、実際には利用していない夜間や休日にも料金が発生します。
そこで、AWSリソースを平日の業務時間だけ自動起動し、業務終了後に自動停止する仕組みを構築しました。
今回実現した内容は、以下のとおりです。
- 平日の8:50に、その日が祝日または会社休日かをLambdaで判定する
- 営業日の場合のみ、9:00にEC2/RDSを自動起動する
- 平日の18:00にEC2/RDSを自動停止する
- EC2/RDSの状態変更を検知する
- Microsoft Teamsへ起動・停止の結果を通知する
- CloudTrailを参照し、自動実行か手動操作かを通知に表示する
- CloudTrailへの反映を待つため、SQSで通知を5分間遅延させる
単純な定時起動・停止だけではなく、祝日対応や状態変更通知、操作元の判定まで含めた構成です。
本記事では、この構成をTerraformで実装した際の設計上のポイントと、実運用で遭遇したEC2起動遅延について紹介します。
2. 全体構成
構成は、大きく以下の2つの流れに分かれます。
- EC2/RDSの自動起動・停止
- EC2/RDSの状態変更をMicrosoft Teamsへ通知
【自動起動・停止】
EventBridge Scheduler
月〜金 8:50
|
v
祝日判定 Lambda
|
+-- 営業日 ------> 9:00起動スケジュールを有効化
|
+-- 祝日・休日 --> 9:00起動スケジュールを無効化
EventBridge Scheduler
月〜金 9:00
|
| 有効な場合のみ実行
v
EC2 / RDS 起動
EventBridge Scheduler
月〜金 18:00
|
| 常時有効
v
EC2 / RDS 停止
【状態変更通知】
EC2 / RDS
|
| running / stoppedなどの状態変更
v
EventBridge Rule
|
v
SQS
|
| 5分間遅延
v
通知用 Lambda
|
| CloudTrailから操作元を確認
| Adaptive Cardを生成
v
Microsoft Teams
ポイントは、祝日判定の対象を起動スケジュールだけにしていることです。
祝日や会社休日は朝の起動を行いませんが、18:00の停止スケジュールは無効化しません。
これにより、平日の祝日に誰かが手動でEC2やRDSを起動した場合でも、18:00には自動停止されます。
2-1. 平日1日の動作フロー
通常の営業日は、以下の順番で処理されます。
| 時刻 | 実行主体 | 処理内容 |
|---|---|---|
| 8:50 | 祝日判定Lambda | 当日が祝日・会社休日かを判定し、起動スケジュールの有効・無効を切り替える |
| 9:00 | EventBridge Scheduler | 起動スケジュールが有効な場合のみ、EC2/RDSを起動する |
| 状態変更後 | EventBridge Rule | EC2/RDSの起動完了を検知する |
| 約5分後 | 通知用Lambda | CloudTrailから操作元を確認し、Teamsへ通知する |
| 18:00 | EventBridge Scheduler | EC2/RDSを停止する |
| 状態変更後 | EventBridge Rule | EC2/RDSの停止完了を検知する |
| 約5分後 | 通知用Lambda | CloudTrailから操作元を確認し、Teamsへ通知する |
祝日の場合は、8:50のLambdaが9:00の起動スケジュールを無効化するため、起動処理は行われません。
一方、18:00の停止スケジュールは通常どおり実行されます。
2-2. スケジュール設定
今回の環境では、以下のスケジュールを設定しています。
| 処理 | スケジュール |
|---|---|
| 祝日判定 | 月〜金 8:50 JST |
| 起動 | 月〜金 9:00 JST |
| 停止 | 月〜金 18:00 JST |
EventBridge Schedulerのcron式は以下です。
# 祝日判定
cron(50 8 ? * MON-FRI *)
# 起動
cron(0 9 ? * MON-FRI *)
# 停止
cron(0 18 ? * MON-FRI *)
タイムゾーンにはAsia/Tokyoを指定します。
schedule_expression_timezone = "Asia/Tokyo"
EventBridge Schedulerではタイムゾーンを明示できるため、UTCへの変換を意識せず、JSTを基準にスケジュールを定義できます。
3. 自動起動・停止の設計
ここからは、EC2/RDSを平日だけ自動で起動・停止する部分の実装を説明します。
3-1. 8:50の祝日判定Lambda
9:00の起動処理より10分前の8:50に、祝日判定用Lambdaを実行します。
Lambdaでは、主に以下の処理を行います。
- 現在の日付をJSTで取得する
-
jpholidayライブラリで日本の祝日か判定する - あらかじめ定義した会社休日に該当するか判定する
- 休日の場合は起動スケジュールを無効化する
- 営業日の場合は起動スケジュールを有効化する
処理イメージは以下のようになります。
from datetime import datetime
from zoneinfo import ZoneInfo
import jpholiday
today = datetime.now(ZoneInfo("Asia/Tokyo")).date()
company_holidays = {
"2026-12-29",
"2026-12-30",
"2026-12-31",
}
is_holiday = (
jpholiday.is_holiday(today)
or today.isoformat() in company_holidays
)
desired_state = "DISABLED" if is_holiday else "ENABLED"
# 対象となる起動スケジュールを取得し、
# EventBridge SchedulerのUpdateScheduleで
# desired_stateへ変更する
上記は処理の概要を示したサンプルです。
実際にUpdateScheduleを呼び出す場合は、状態だけではなく、既存のスケジュール式やターゲットなど、更新APIが必要とする設定も渡します。
日本の祝日と会社休日を分けて管理する
日本の祝日はjpholidayで判定できるため、設定ファイルへ毎年すべての祝日を記述する必要はありません。
環境ごとに指定するのは、年末年始や夏季休暇などの会社独自の休日だけにします。
company_holidays = [
"2026-12-29",
"2026-12-30",
"2026-12-31",
]
これにより、休日設定のメンテナンス量を減らせます。
3-2. 祝日には「起動」だけを無効化する
祝日判定Lambdaが変更するのは、9:00の起動スケジュールだけです。
18:00の停止スケジュールは常時有効にしています。
祝日判定
|
+-- 起動スケジュール: 有効・無効を切り替える
|
+-- 停止スケジュール: 変更しない
仮に祝日の昼間に担当者が手動でEC2を起動した場合、停止スケジュールまで無効化していると、そのまま夜間も起動し続ける可能性があります。
停止スケジュールを常時有効にしておけば、平日の祝日であっても18:00に停止されるため、消し忘れによる想定外の課金を防止できます。
3-3. stateをTerraformの管理対象外にする理由
EventBridge Schedulerの起動スケジュールは、祝日判定LambdaによってENABLEDまたはDISABLEDへ変更されます。
一方、Terraform側でもstateを管理していると、terraform applyを実行した際にLambdaが変更した状態を元に戻してしまう可能性があります。
そのため、起動スケジュールではstateの変更をTerraformの差分検知対象外にします。
resource "aws_scheduler_schedule" "ec2_start" {
name = "example-ec2-start"
schedule_expression = "cron(0 9 ? * MON-FRI *)"
schedule_expression_timezone = "Asia/Tokyo"
flexible_time_window {
mode = "OFF"
}
target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
role_arn = var.scheduler_role_arn
input = jsonencode({
InstanceIds = [var.ec2_instance_id]
})
}
lifecycle {
ignore_changes = [state]
}
}
この構成では、以下のように役割を分けています。
- スケジュール式やターゲットなどの構成はTerraformが管理する
- 日々の
ENABLED・DISABLEDの切り替えはLambdaが管理する
3-4. EventBridge SchedulerからAWS APIを直接実行する
EC2/RDSの起動・停止では、起動処理用のLambdaを別途用意せず、EventBridge SchedulerからAWS APIを直接呼び出しています。
EC2
aws-sdk:ec2:startInstances
aws-sdk:ec2:stopInstances
RDS
aws-sdk:rds:startDBInstance
aws-sdk:rds:stopDBInstance
EC2起動スケジュールのターゲット例は以下です。
target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
role_arn = var.scheduler_role_arn
input = jsonencode({
InstanceIds = [var.ec2_instance_id]
})
}
単純な起動・停止処理はEventBridge Schedulerへ任せ、祝日判定のような条件分岐が必要な処理だけLambdaで実装しています。
単純なAPI実行
|
v
EventBridge Scheduler
条件判定を伴う処理
|
v
Lambda
起動・停止処理をすべてLambdaへ実装する方式と比較すると、コード量や保守対象を減らせます。
3-5. 対象EC2の指定方法(Nameタグ)
EC2のインスタンスIDをTerraformへ直接記述するのではなく、Nameタグを利用して対象インスタンスを検索しています。
これにより、環境ごとの指定値を以下のようにできます。
ec2_instance_names = [
"app-dev-ec2",
"batch-dev-ec2",
]
インスタンスIDをコードへ直接記述する必要がなくなり、設定内容も読みやすくなります。
ただし、データソースで取得した結果が0件だった場合、そのまま空のターゲットを持つスケジュールが作成されないように注意が必要です。
Terraformのpostconditionなどを利用し、対象が見つからなければ構築時点でエラーにします。
lifecycle {
postcondition {
condition = length(self.ids) > 0
error_message = "対象となるEC2インスタンスが見つかりません。"
}
}
設定ミスをapply時に検知することで、
スケジュールは存在するが、実際には何も起動・停止されていない
という状態を防げます。
4. 状態変更通知の設計
ここからは、EC2/RDSの状態変更を検知し、Microsoft Teamsへ通知する部分の実装を説明します。
4-1. EC2/RDSの状態変更をEventBridge Ruleで検知する
自動起動・停止とは別に、EC2/RDSの状態変更をEventBridge Ruleで監視しています。
EC2
対象EC2インスタンスが以下の状態になった場合にイベントを発火します。
running
stopped
EC2のイベントパターン例は以下です。
{
"source": [
"aws.ec2"
],
"detail-type": [
"EC2 Instance State-change Notification"
],
"detail": {
"state": [
"running",
"stopped"
]
}
}
RDS
RDSについては、起動完了・停止完了を示すEvent IDを監視します。
| Event ID | 公式ドキュメント上のメッセージ | カテゴリ | 本構成での扱い |
|---|---|---|---|
RDS-EVENT-0088 |
DB instance started. | notification | 起動完了として通知 |
RDS-EVENT-0087 |
DB instance stopped. | notification | 停止完了として通知 |
Event IDは名前から意味を推測しづらく、選定を誤りやすい部分です。特に以下は混同しやすいため、監視対象からは外しています。
| Event ID | 公式ドキュメント上のメッセージ | 補足 |
|---|---|---|
RDS-EVENT-0006 |
DB instance restarted. | 「停止」ではなく再起動を示すイベント。停止完了の検知には使えない |
RDS-EVENT-0004 |
DB instance shutdown. | カテゴリはavailability。インスタンスのシャットダウン時に発生するが、停止完了はRDS-EVENT-0087で検知できるため未使用 |
RDS-EVENT-0154 |
DB instance is being started due to it exceeding the maximum allowed time being stopped. | RDSを停止できる最大期間(7日)を超えたため、AWS側が自動的に起動を開始したことを示す通知。通常の起動完了ではない |
RDS-EVENT-0154を起動完了として扱うと、7日上限による自動起動の際に「起動完了」と通知され、そのあと実際の起動完了であるRDS-EVENT-0088でも通知されて二重になります。
7日上限による自動起動そのものは運用上把握したい情報なので、監視するのであれば起動完了とは別のメッセージとして通知するほうが分かりやすくなります。
Event IDごとの正確なメッセージとカテゴリは、公式ドキュメントで確認できます。
4-2. EventBridge Ruleから直接Lambdaを呼ばない理由
状態変更を検知した後は、以下のようにEventBridge Ruleから直接Lambdaを実行する構成も考えられます。
EventBridge Rule
|
v
Lambda
|
v
Microsoft Teams
しかし、今回の構成では間にSQSを挟んでいます。
EventBridge Rule
|
v
SQS
|
| 5分間遅延
v
Lambda
|
v
Microsoft Teams
理由は、CloudTrailへの操作履歴の反映にタイムラグがあるためです。
4-3. CloudTrailによる操作元の判定
Teamsの通知には、以下のような情報を表示します。
EC2インスタンス状態変更
インスタンス名: example-ec2
インスタンスID: i-xxxxxxxxxxxxxxxxx
状態: 停止
トリガー: EventBridge Scheduler(自動)
時刻(JST): 2026-02-24 18:00:32
特に重要なのが、以下の項目です。
トリガー: EventBridge Scheduler(自動)
スケジュールによる自動実行なのか、ユーザーやIAMロールによる手動操作なのかを判定するため、通知用LambdaからCloudTrailのLookupEventsを実行します。
大まかな判定イメージは以下です。
CloudTrail LookupEvents
|
v
該当するStart/Stopイベントを検索
|
+-- Scheduler用ロール
| |
| v
| EventBridge Scheduler(自動)
|
+-- その他のIAMロール・ユーザー
| |
| v
| 手動(ロール名・ユーザー名)
|
+-- イベントが見つからない
|
v
不明
4-4. 状態変更直後はCloudTrailから取得できない場合がある
EC2/RDSの状態変更を検知してすぐにLambdaを実行すると、対象となるAPI操作がまだCloudTrailへ反映されていない場合があります。
例えば、以下の流れです。
EventBridge Scheduler
|
| StartInstances
v
EC2
|
| running
v
EventBridge Rule
|
v
Lambda
|
| CloudTrailを検索
v
対象イベントがまだ見つからない
実際にはEventBridge Schedulerによる自動起動だったとしても、CloudTrailに対象イベントが反映されていなければ、通知上は次のようになります。
トリガー: 不明
通知をすぐに届けることはできますが、「誰が操作したのか」という情報の精度が下がってしまいます。
4-5. SQSによる5分間の遅延
そこで、EventBridge Ruleと通知用Lambdaの間にSQSを追加しました。
EventBridge Rule
|
| 状態変更イベント
v
SQS
|
| delay_seconds = 300
v
通知用Lambda
|
| CloudTrail LookupEvents
v
Microsoft Teams
SQSでは、以下のように5分間の遅延を設定します。
resource "aws_sqs_queue" "notification_delay" {
name = "instance-state-notification-delay"
delay_seconds = 300
}
処理の流れは以下です。
- EC2/RDSの状態が変化する
- EventBridge Ruleが状態変更を検知する
- イベントをSQSへ投入する
- SQSで5分間待機する
- Lambdaを実行する
- CloudTrailから対象操作を検索する
- 自動実行か手動操作かを判定する
- Microsoft Teamsへ通知する
通知速度より通知内容の正確性を優先する
この構成では、Microsoft Teamsへの通知が約5分遅れます。
ただし、今回の通知はリアルタイムの障害検知を目的としたものではありません。
目的は、以下を運用者が把握することです。
- EC2/RDSが起動または停止したこと
- 自動スケジュールによる操作か
- 担当者による手動操作か
- 手動操作の場合は誰が実行したか
そのため、
すぐに通知されるが、操作元が「不明」になる可能性がある
よりも、
約5分遅れるが、操作元を判定できる可能性が高い
ことを優先しました。
SQSは単なるイベントの中継ではなく、CloudTrailへの反映を待つためのバッファとして利用しています。
4-6. 通知用Lambdaの処理
通知用Lambdaでは、主に以下の処理を行います。
- 受信したイベントがEC2かRDSかを判別する
- EC2の場合は
DescribeInstancesでNameタグを取得する - RDSの場合はEvent IDを起動・停止へ変換する
- CloudTrailの
LookupEventsで操作元を判定する - イベント時刻をUTCからJSTへ変換する
- Adaptive Cardを生成する
- Microsoft TeamsのWebhookへPOSTする
AWSから受信したイベントをそのまま通知するのではなく、運用担当者が見て分かりやすい情報へ変換しています。
4-7. Microsoft Teamsへの通知例
EC2の場合は、以下のような情報を表示します。
EC2インスタンス状態変更
インスタンス名: example-ec2
インスタンスID: i-xxxxxxxxxxxxxxxxx
状態: 停止
トリガー: EventBridge Scheduler(自動)
時刻(JST): 2026-02-24 18:00:32
RDSの場合は以下のようになります。
RDSインスタンス状態変更
DB識別子: example-db
状態: 停止
トリガー: EventBridge Scheduler(自動)
時刻(JST): 2026-02-24 18:05:12
手動操作の場合は、CloudTrailから取得したIAMロール名やユーザー名を表示します。
トリガー: 手動(example-operator-role)
これにより、Teamsの通知を見るだけで、想定どおり自動制御されているのか、誰かが手動で操作したのかを判断できます。
4-8. API DestinationではなくLambdaを利用した理由
当初は、EventBridgeのAPI Destinationを利用してTeamsへ通知する方式も検討しました。
しかし、Input Transformerだけでは通知内容の加工に制約があります。
今回表示したかった情報は以下です。
- EC2のNameタグ
- RDSのEvent IDを日本語化した状態
- JSTへ変換した時刻
- CloudTrailから取得した操作元
- 起動と停止を視覚的に判別できる表示
- EC2とRDSで異なる通知レイアウト
Lambdaを挟むことで、AWSイベントを単純に転送するのではなく、運用者が必要とする情報へ加工してから通知できるようになりました。
5. 権限と機密情報の管理
自動起動・停止と通知の仕組みを支える、IAMロールとWebhook URLの扱いについて整理します。
5-1. IAMロールの構成
用途ごとにIAMロールを分け、必要最小限の権限を付与します。
EventBridge Scheduler用ロール
EC2/RDSの起動・停止に必要な権限を付与します。
ec2:StartInstances
ec2:StopInstances
rds:StartDBInstance
rds:StopDBInstance
Resourceには対象となるEC2インスタンスやRDS DBインスタンスを指定し、影響範囲を限定します。
祝日判定Lambda用ロール
起動スケジュールの状態を確認・更新するための権限を付与します。
scheduler:GetSchedule
scheduler:UpdateSchedule
更新対象は起動スケジュールだけに限定します。
通知用Lambdaロール
主に以下の権限が必要です。
ec2:DescribeInstances
cloudtrail:LookupEvents
ssm:GetParameter
これに加えて、CloudWatch Logsへのログ書き込み権限を付与します。
5-2. Webhook URLをコードに直接書かない
Microsoft TeamsのWebhook URLなどの機密情報を、TerraformやTerragruntへ直接記述するのは避けます。
AWS Systems Manager Parameter Store
|
v
SecureString
|
v
通知用Lambda
Webhook URLは、SSM Parameter StoreのSecureStringとして管理し、通知用Lambdaから実行時に取得します。
例えば、Terraformでは以下のようにパラメータを定義できます。
resource "aws_ssm_parameter" "teams_webhook_url" {
name = "/example/teams/webhook-url"
type = "SecureString"
value = var.teams_webhook_url
}
ただし、Terraformのstateに値を含めたくない場合は、パラメータ自体をTerraformで作成した後、値は別の安全な方法で登録する運用も検討します。
Webhook URLには認証に利用される情報が含まれるため、Gitリポジトリへ平文でコミットしないようにします。
過去にWebhook URLをGitへコミットしてしまった場合は、SSM Parameter Storeへ移行するだけではなく、既存Webhook URLのローテーションも実施します。
6. 実際のコスト削減効果
平日9:00から18:00まで稼働する場合、1週間の稼働時間は以下です。
9時間 × 5日 = 45時間
常時稼働の場合は、1週間で168時間です。
24時間 × 7日 = 168時間
単純に稼働時間だけを比較すると、コンピューティング費用の削減率の目安は以下になります。
1 - 45 / 168 ≒ 73%
ただし、この削減率がAWS全体の請求額へそのまま反映されるわけではありません。
停止中も以下のような費用は発生します。
- EBS
- S3
- スナップショット
- Elastic IPの条件に応じた料金
- NAT Gateway
- ALB
- データ転送
- その他の固定費
実際の検証環境では、導入前後のAWS全体のコストが以下のように変化しました。
| 月 | AWSコスト |
|---|---|
| 2026年1月 | $2,302.89 |
| 2026年2月 | $1,885.62 |
| 差額 | -$417.27 |
| 削減率 | 18.12% |
AWS全体のコストにはEC2/RDS以外のサービスも含まれるため、差額のすべてが自動起動・停止によるものとは断定できません。
ただし、EC2の稼働時間と関連コストが減少していることから、業務時間外の停止がコスト削減に寄与したと考えられます。
6-1. EC2/RDSに絞ったコスト推移
AWS全体の請求額には他サービスの変動も含まれるため、Cost ExplorerでEC2/RDSだけに絞った推移も合わせて確認します。
サービス単位に絞ったグラフを載せることで、稼働時間の削減が実際の請求額へ反映されていることを確認しやすくなります。
7. 実運用で発生した「9時に起動しない」問題
この仕組みを運用していると、平日9:00に起動するよう設定したEC2について、Teamsへの起動通知が9:30を過ぎてから届く事象が発生しました。
最初は、以下の通知経路で遅延していると考えました。
EventBridge Rule
|
v
SQS
|
v
Lambda
|
v
Microsoft Teams
しかし、通知に表示している時刻とTeamsへの投稿時刻を比較すると、以下のようになっていました。
EC2がrunningになった時刻: 09:32頃
Teamsへ投稿された時刻 : 09:37頃
差は約5分であり、SQSのdelay_seconds = 300と一致します。
つまり、通知経路は設計どおりに動作していました。
遅れていたのは通知ではなく、EC2そのものの起動でした。
7-1. 原因はEC2のキャパシティ不足だった
CloudTrailからStartInstancesの履歴を確認すると、以下のように複数回失敗していました。
09:00:49 Server.InsufficientInstanceCapacity
09:01:44 Server.InsufficientInstanceCapacity
09:03:45 Server.InsufficientInstanceCapacity
09:07:28 Server.InsufficientInstanceCapacity
09:15:11 Server.InsufficientInstanceCapacity
09:23:34 Server.InsufficientInstanceCapacity
09:32:32 成功
EventBridge Schedulerは9:00台に正しく実行されていましたが、StartInstancesがAWS側のキャパシティ不足によって拒否されていました。
その後、EventBridge Schedulerがリトライし、最終的に約32分後に起動しています。
9:00
EventBridge Schedulerが実行
|
v
StartInstances
|
v
Server.InsufficientInstanceCapacity
|
v
リトライ
|
v
再度失敗
|
...
|
v
9:32
StartInstances成功
対象インスタンスが旧世代のインスタンスタイプであり、同一のアベイラビリティゾーンへ集中していたことも、キャパシティを確保しづらくした要因と考えられました。
7-2. 起動遅延の調査に通知設計が役立った
今回の調査では、Teams通知に以下の情報を含めていたことが役立ちました。
- EC2が
runningになったイベント時刻 - EventBridge Schedulerによる自動実行であること
- Teamsへ実際に投稿された時刻
これによって、
状態変更からTeams通知までの遅延
|
v
約5分
|
v
SQSの設計どおり
と判断できました。
その結果、通知経路ではなく、EC2の起動API側に原因があると切り分けられました。
8. 運用して分かった改善ポイント
8-1. Schedulerのリトライと失敗を可視化する
EventBridge Schedulerのターゲットには、リトライポリシーとDLQを設定することを検討します。
target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
role_arn = var.scheduler_role_arn
input = jsonencode({
InstanceIds = [var.ec2_instance_id]
})
retry_policy {
maximum_event_age_in_seconds = 1800
maximum_retry_attempts = 10
}
dead_letter_config {
arn = var.scheduler_dlq_arn
}
}
リトライの上限を明示することで、想定以上に長時間リトライし続けることを防げます。
また、AWS/Scheduler名前空間のTargetErrorCountをCloudWatch Alarmで監視すれば、以下のようなエラーを早期に検知できます。
Server.InsufficientInstanceCapacity- 対象リソースの指定誤り
- IAM権限不足
- APIパラメータの不備
8-2. 祝日判定Lambdaも監視する
祝日判定Lambdaが失敗すると、起動スケジュールが前回の状態のまま残る可能性があります。
例えば、祝日にDISABLEDとなった後、次の営業日にLambdaが失敗すると、9:00の起動スケジュールが無効のままになる可能性があります。
そのため、以下を実施します。
- Lambdaの
ErrorsへCloudWatch Alarmを設定する - 更新したスケジュール名と状態をログへ出力する
- 更新対象数が想定と一致するか検証する
- 必要に応じて失敗通知をTeamsなどへ送信する
8-3. 空のターゲットを作成しない
Nameタグから対象EC2を検索する場合は、0件であればTerraformのapplyを失敗させます。
空のInstanceIdsを持つスケジュールが作成されると、毎日決まった時刻に起動APIが失敗し続ける可能性があります。
8-4. 起動遅延を通知から判別できるようにする
通常は9:00付近に起動した場合、スケジュール起因と判断できます。
しかし、Schedulerのリトライによって30分以上遅れて起動した場合、単純な時刻比較だけでは判定できません。
CloudTrailの情報を利用し、通知に以下のような表示を追加すると、原因を把握しやすくなります。
トリガー: EventBridge Scheduler(リトライにより遅延)
8-5. 旧世代のインスタンスタイプを見直す
キャパシティ不足が発生した場合は、旧世代のインスタンスタイプから現行世代への変更を検討します。
変更前には、AMIやOSがENA、NVMeなどの要件に対応しているか確認が必要です。
8-6. 起動時刻を前倒しする
9:00までに確実に利用可能な状態にしたい場合は、起動スケジュールを8:30などへ前倒しし、リトライのための時間を確保する方法もあります。
ただし、起動時間を早めた分だけ料金も増えるため、求める可用性とコストのバランスを考える必要があります。
9. この構成で意識したこと
今回の構成では、各サービスの役割をできるだけ単純にしています。
| サービス | 役割 |
|---|---|
| EventBridge Scheduler | 定刻でLambdaや起動・停止APIを実行する |
| 祝日判定Lambda | 祝日・会社休日を判定し、起動スケジュールを切り替える |
| EventBridge Rule | EC2/RDSの状態変更を検知する |
| SQS | CloudTrailへの反映を待つため、イベントを5分間遅延させる |
| 通知用Lambda | 状態変更イベントとCloudTrailを加工し、Teamsへ通知する |
| CloudTrail | 起動・停止APIの実行元を確認する |
| SSM Parameter Store | Teams Webhook URLを暗号化して管理する |
| Microsoft Teams | 運用担当者へ起動・停止結果を通知する |
単純な起動・停止はEventBridge Schedulerへ任せ、判断やデータ加工が必要な処理だけLambdaへ実装する構成です。
10. まとめ
今回は、EventBridge Schedulerを中心に、EC2/RDSの祝日対応を含む自動起動・停止とMicrosoft Teamsへの状態変更通知を構築しました。
構成をまとめると以下のようになります。
【祝日判定】
EventBridge Scheduler
月〜金 8:50
|
v
祝日判定Lambda
|
+-- 営業日: 起動スケジュールを有効化
|
+-- 休日 : 起動スケジュールを無効化
【自動起動・停止】
EventBridge Scheduler
月〜金 9:00
|
| 有効な場合のみ
v
EC2 / RDS 起動
EventBridge Scheduler
月〜金 18:00
|
| 常時有効
v
EC2 / RDS 停止
【状態変更通知】
EC2 / RDS
|
v
EventBridge Rule
|
v
SQS
|
| 5分間遅延
v
通知用Lambda
|
+-- DescribeInstances
|
+-- CloudTrail LookupEvents
|
+-- Adaptive Card生成
|
v
Microsoft Teams
実装して特に重要だったポイントは以下です。
- 月〜金の8:50に祝日判定Lambdaを実行する
-
jpholidayで日本の祝日を判定する - 年末年始などの会社休日を別途設定できるようにする
- 休日は9:00の起動スケジュールだけを無効化する
- 18:00の停止スケジュールは常時有効にする
- 単純な起動・停止はEventBridge SchedulerからAPIを直接呼び出す
- EC2/RDSの状態変更をEventBridge Ruleで検知する
- CloudTrailへの反映を待つため、SQSで通知を5分間遅延させる
- CloudTrailから自動実行か手動操作かを判定する
- Webhook URLはSSM Parameter StoreのSecureStringで管理する
- Schedulerや祝日判定Lambdaの失敗をCloudWatchで監視する
- 対象リソースが見つからない場合はTerraformの構築時点でエラーにする
最初は単純な定時起動・停止として始めた仕組みでしたが、実際に運用すると、次のような観点も重要になりました。
- 今日は本当に起動すべき日なのか
- 想定した時刻に起動したのか
- 起動・停止は自動実行だったのか
- 誰かが手動で操作したのか
- API実行が失敗した場合に気付けるのか
- 休日に手動起動されたリソースが夜間も残らないか
AWSのマネージドサービスを組み合わせることで、起動・停止処理そのものは比較的シンプルに実装できます。
一方で、祝日制御、失敗の可視化、通知内容の正確性、機密情報の管理まで含めて設計することで、実運用で使いやすい仕組みになると感じました。


