はじめに
Jenkins Agent用のSpotFleetをCloudFormationで管理している環境で、スタック更新時に誤って50台のEC2インスタンスが起動してしまいました。
本記事では、なぜこの問題が発生したのか、どう対処したのか、そして再発防止のために何をすべきかを記録します。
環境
- AWS Region: us-west-2 (Oregon)
- CloudFormation
- Jenkins EC2 Fleet Plugin
- SpotFleet (c8i.xlarge, t3.nano/small/medium/large)
問題の発生
発生状況
CloudFormationでSpotFleetの設定変更(複数インスタンスサイズパターンの追加)を実施し、スタック更新を実行しました。
約40分後にAWSコンソールを確認したところ、50台のEC2インスタンス(10台 × 5つのSpotFleet)が起動していました。
想定していた動作
本来、Jenkins EC2 Fleet Pluginが動的にTargetCapacityを管理する設計でした:
- ジョブキューにジョブがあれば、必要な分だけTargetCapacityを増やす
- ジョブがなければ、0に戻す
つまり、スタック作成・更新直後は何も起動しないはずでした。
原因
誤った設定
CloudFormationテンプレートで、以下のように設定していました:
Parameters:
MaxTargetCapacity:
Type: Number
Default: 10
Resources:
JenkinsAgentSpotFleet:
Type: AWS::EC2::SpotFleet
Properties:
SpotFleetRequestConfigData:
TargetCapacity: !Ref MaxTargetCapacity # ← 問題の設定
IamFleetRole: !GetAtt SpotFleetRole.Arn
AllocationStrategy: lowestPrice
# ... その他の設定
TargetCapacity: !Ref MaxTargetCapacity により、スタック作成・更新時に各SpotFleetが10台ずつ起動する設定になっていました。
正しい設定
Jenkins EC2 Fleet PluginがTargetCapacityを管理する場合、CloudFormation側の初期値は0にすべきでした:
Resources:
JenkinsAgentSpotFleet:
Type: AWS::EC2::SpotFleet
Properties:
SpotFleetRequestConfigData:
TargetCapacity: 0 # Jenkins EC2 Fleet Plugin に管理を委ねる
IamFleetRole: !GetAtt SpotFleetRole.Arn
AllocationStrategy: lowestPrice
# ... その他の設定
対処法
1. SpotFleetのTargetCapacityを0に設定
まず、AWS CLIで直接SpotFleetのTargetCapacityを0に設定しました:
# SpotFleetリクエストIDを取得
aws ec2 describe-spot-fleet-requests \
--query 'SpotFleetRequestConfigs[*].[SpotFleetRequestId,SpotFleetRequestState]' \
--output table
# 各SpotFleetのTargetCapacityを0に設定
aws ec2 modify-spot-fleet-request \
--spot-fleet-request-id sfr-xxxxxxxxx \
--target-capacity 0
5つのSpotFleetすべてに対して実行しました。
2. 起動中のインスタンスを終了
TargetCapacityを0にしても、既に起動しているインスタンスはすぐには停止しないため、手動で終了させました:
# 起動中のインスタンスIDを取得
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query 'Reservations[*].Instances[*].[InstanceId,InstanceType]' \
--output table
# インスタンスを一括終了
aws ec2 terminate-instances \
--instance-ids i-xxxxxxxxx i-yyyyyyyyy i-zzzzzzzzz ...
3. CloudFormationテンプレートを修正
Resources:
JenkinsAgentSpotFleet:
Type: AWS::EC2::SpotFleet
Properties:
SpotFleetRequestConfigData:
TargetCapacity: 0 # Jenkins EC2 Fleet Plugin に管理を委ねる
IamFleetRole: !GetAtt SpotFleetRole.Arn
AllocationStrategy: lowestPrice
LaunchTemplateConfigs:
- LaunchTemplateSpecification:
LaunchTemplateId: !Ref JenkinsAgentLaunchTemplate
Version: !GetAtt JenkinsAgentLaunchTemplate.LatestVersionNumber
Overrides:
- InstanceType: c8i.xlarge
- InstanceType: c7i.xlarge
- InstanceType: c6i.xlarge
コメントも追加して、なぜ0なのかを明記しました。
4. スタック更新を再実行
修正したテンプレートでスタック更新を実行し、インスタンスが起動しないことを確認しました。
コスト影響
約50分間(気づくまで40分 + 対応10分)、50台が稼働しました。
推定コスト(Spot料金):約 $1.41 USD
内訳:
- c8i.xlarge × 10台:$0.57
- t3.nano × 10台:$0.009
- t3.small × 10台:$0.037
- t3.medium × 10台:$0.073
- t3.large × 10台:$0.147
Spot料金のため、オンデマンドと比較して大幅に安く済みました。
再発防止策
今回の問題は、複数のチェックポイントをすり抜けた結果でした。
1. デフォルト値を「安全側」に設定
容量・サイズに関するパラメータは、デフォルト値を0または最小値にします:
Resources:
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: 0
MaxSize: 10
DesiredCapacity: 0 # 初期状態では何も起動しない
2. 重要パラメータのレビュー徹底
差分が大きい場合でも、以下のパラメータは必ず確認します:
- TargetCapacity
- MinSize / MaxSize / DesiredCapacity
- その他、インスタンス数に影響するパラメータ
3. スタック更新完了の通知設定
EventBridgeとSNSで、スタック更新完了を自動通知します:
Resources:
NotificationTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: your-email@example.com
Protocol: email
StackUpdateNotificationRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.cloudformation
detail-type:
- CloudFormation Stack Status Change
detail:
stack-id:
- !Ref AWS::StackId
status-details:
status:
- UPDATE_COMPLETE
- UPDATE_ROLLBACK_COMPLETE
Targets:
- Arn: !Ref NotificationTopic
Id: NotificationTarget
4. 確認の習慣化
通知が来たら、すぐにAWSコンソールまたはCLIで確認する習慣を作ります。
まとめ
- Jenkins EC2 Fleet Pluginと連携するSpotFleetの初期TargetCapacityは0にする
- デフォルト値は「安全側」に倒す(何もしない状態が安全)
- スタック更新完了の通知を設定し、確認を忘れない仕組みを作る
- 複数のチェックポイントを設けることで、一つが失敗しても他でカバーできる