こんにちは、インフラエンジニアの川島です。
ECSの自動停止・自動起動をCloudFormationで作成したので紹介したいと思います。
以前Auroraの自動停止・自動起動をCloudFormationで作成したブログをあげたのですが、実際に開発環境で実装したところむしろコストが増加してしまいました。
何が起きていたのか原因を探ってみると、Configのコストが跳ね上がっていると判明しました。
Configのコストがなぜ上がったのかと調べたところ下記の内容が原因でした。
Aurora停止中にECSのコンテナが起動していたこと
タスクがヘルスチェック時にAuroraに対してPingし失敗すると5xxを返す実装になっていたこと
ECSサービスをALBに紐づけていたこと
ECSのコンテナが起動 → Auroraが起動していないためヘルスチェックが通らない → コンテナ再起動 →ヘルスチェックを実行し通らないためコンテナ再起動
という形でAuroraが停止していることでコンテナが再起動を延々と繰り返していました、これだけならば問題ないのですが、Configが上記を継続的に記録していたためConfigのコストが上昇していました。
これを受けてAuroraの自動停止だけではなく、ECSの自動停止も必要だと感じたため本テンプレートの作成を行いました。
全体像
今回構築するアーキテクチャとしては下記になります。
EventBrdigeが定期起動し、LambdaがECSの停止・起動のAPIを叩く事で自動停止・起動を実装しています。

本手法はECSサービスに付けられているタグによって停止対象を判別している事が特徴的です。
そのため、停止対象が増減した場合にもECSサービスのタグを変更するだけでよく、他の手法と比べて手軽に設定することが可能となっています。
設定するパラメータと作成するリソースとしては下記になります。
| 設定するパラメータ | 内容 |
|---|---|
| ProjectName | 任意の値 |
| EnvCode | 任意の値 (dev, stg, …) |
| EventBridgeState | 自動停止・起動の有効、無効 (ENABLED, DISABLED) |
| StartSchedule | 自動起動時刻 (cron式でUTC時刻で設定) |
| StopSchedule | 自動停止時刻 (cron式でUTC時刻で設定) |
| 作成するリソース | リソース名 |
|---|---|
| Lambda用Role | LambdaRole |
| 起動Lambda | ECSAutoStartLambda |
| 起動Lambda用リソースベースポリシー | PermissionForEventsToInvokeStartLambda |
| 起動EventBridge | ECSStartEventRule |
| 停止Lambda | ECSAutoStopLambda |
| 停止Lambda用リソースベースポリシー | PermissionForEventsToInvokeStopLambda |
| 停止EventBridge | ECSStopEventRule |
テンプレート
AWSTemplateFormatVersion: 2010-09-09
Description: "Create ECSAutoStop template"
Parameters:
ProjectName:
Description: "Project name"
Type: "String"
EnvCode:
Description: "Environment type"
Type: "String"
EventBridgeState:
Description : "EventBridge state"
Type: "String"
AllowedValues:
- "ENABLED"
- "DISABLED"
StartSchedule:
Description: "Start ECS schedule (cron UTC)"
Type: "String"
StopSchedule:
Description: "Stop ECS schedule (cron UTC)"
Type: "String"
Resources:
ECSAutoStartLambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import boto3
import json
def lambda_handler(event, context):
ecs = boto3.client('ecs')
try:
# ECSクラスターARNをリストアップ
response = ecs.list_clusters()
cluster_arns = response['clusterArns']
for cluster_arn in cluster_arns:
# クラスターの詳細を取得
response = ecs.describe_clusters(clusters=[cluster_arn])
cluster = response['clusters'][0]
cluster_name = cluster['clusterName']
# ECSサービスをリストアップ
response = ecs.list_services(
cluster=cluster_arn,
launchType='FARGATE'
)
service_arns = response['serviceArns']
for service_arn in service_arns:
# サービスの詳細を取得 (タグを含む)
service_description = ecs.describe_services(
cluster=cluster_arn,
services=[service_arn],
include=['TAGS']
)['services'][0]
service_name = service_description['serviceName']
tags = service_description.get('tags', [])
auto_start_tag = next((tag for tag in tags if tag['key'] == 'autostart'), None)
if auto_start_tag and auto_start_tag['value'] == 'yes':
desired_count = 1
desired_count_tag = next((tag for tag in tags if tag['key'] == 'desiredcount'), None)
if desired_count_tag:
try:
desired_count = int(desired_count_tag['value'])
except ValueError:
log_message = {
"level": "WARNING",
"message": f"Invalid desiredcount value for service: {service_name} in cluster: {cluster_name}. Using default value 1.",
"cluster_name": cluster_name,
"service_name": service_name
}
print(json.dumps(log_message))
ecs.update_service(
cluster=cluster_arn,
service=service_arn,
desiredCount=desired_count
)
log_message = {
"level": "INFO",
"message": f"Service '{service_name}' in cluster '{cluster_name}' started with desired count: {desired_count}",
"cluster_name": cluster_name,
"service_name": service_name,
"desired_count": desired_count
}
print(json.dumps(log_message))
return {
'statusCode': 200,
'body': 'ECS services started successfully'
}
except Exception as e:
log_message = {
"level": "ERROR",
"message": f"Error: {e}",
"error": str(e)
}
print(json.dumps(log_message))
return {
'statusCode': 500,
"body": json.dumps(log_message)
}
FunctionName: !Sub "${ProjectName}-${EnvCode}-ecs-auto-start-function"
Handler: index.lambda_handler
Runtime: python3.13
Role: !GetAtt LambdaRole.Arn
Timeout: 60
ECSAutoStopLambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import boto3
import json
def lambda_handler(event, context):
ecs = boto3.client('ecs')
try:
# ECSクラスターARNをリストアップ
response = ecs.list_clusters()
cluster_arns = response['clusterArns']
for cluster_arn in cluster_arns:
# クラスターの詳細を取得
response = ecs.describe_clusters(clusters=[cluster_arn])
cluster = response['clusters'][0]
cluster_name = cluster['clusterName']
# ECSサービスをリストアップ
response = ecs.list_services(
cluster=cluster_arn,
launchType='FARGATE'
)
service_arns = response['serviceArns']
for service_arn in service_arns:
# サービスの詳細を取得 (タグを含む)
service_description = ecs.describe_services(
cluster=cluster_arn,
services=[service_arn],
include=['TAGS']
)['services'][0]
service_name = service_description['serviceName']
tags = service_description.get('tags', [])
auto_stop_tag = next((tag for tag in tags if tag['key'] == 'autostop'), None)
if auto_stop_tag and auto_stop_tag['value'] == 'yes':
ecs.update_service(
cluster=cluster_arn,
service=service_arn,
desiredCount=0
)
log_message = {
"level": "INFO",
"message": f"Service '{service_name}' in cluster '{cluster_name}' stopped",
"cluster_name": cluster_name,
"service_name": service_name
}
print(json.dumps(log_message))
return {
'statusCode': 200,
"body": json.dumps({"message": "ECS services stopped successfully"})
}
except Exception as e:
log_message = {
"level": "ERROR",
"message": f"Error: {e}",
"error": str(e)
}
print(json.dumps(log_message))
return {
'statusCode': 500,
"body": json.dumps(log_message)
}
FunctionName: !Sub "${ProjectName}-${EnvCode}-ecs-auto-stop-function"
Handler: index.lambda_handler
Runtime: python3.13
Role: !GetAtt LambdaRole.Arn
Timeout: 60
LambdaRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: ECSStartStopPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- ecs:ListClusters
- ecs:DescribeClusters
- ecs:ListServices
- ecs:DescribeServices
- ecs:UpdateService
- ecs:ListTagsForResource
Resource: "*"
PermissionForEventsToInvokeStartLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref ECSAutoStartLambda
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt ECSStartEventRule.Arn
ECSStartEventRule:
Type: "AWS::Events::Rule"
Properties:
Name: !Sub "${ProjectName}-${EnvCode}-ecs-start-rule"
Description: "Push ECSStartLambda Batch event rule"
ScheduleExpression: !Ref StartSchedule
State: !Ref EventBridgeState
Targets:
- Arn: !GetAtt ECSAutoStartLambda.Arn
Id: StartLambdaFunction
PermissionForEventsToInvokeStopLambda:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref ECSAutoStopLambda
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt ECSStopEventRule.Arn
ECSStopEventRule:
Type: "AWS::Events::Rule"
Properties:
Name: !Sub "${ProjectName}-${EnvCode}-ecs-stop-rule"
Description: "Push ECSStopLambda Batch event rule"
ScheduleExpression: !Ref StopSchedule
State: !Ref EventBridgeState
Targets:
- Arn: !GetAtt ECSAutoStopLambda.Arn
Id: StopLambdaFunction
実装方法
対象ECSにタグを設定する
停止対象のECSのサービスにタグ autostart:yes, autostop:yes を設定する。
CloudFormationでリソースを作成する
上記テンプレートをYAMLファイルとして保存し、コンソール画面からアップロードし、パラメータに適切な値を入力します。
ProjectNameとEnvCodeは任意の値を入力します。
EventBridgeStateは自動停止を有効化する場合はENABLED、無効化する場合はDISABLEDを選択します。
StartScheduleとStopScheduleにはcron式かつUTCで値を入力します。(例 : cron(15 13 ? * MON *))
cron 式と rate 式を使用して Amazon EventBridge でルールをスケジュールする - Amazon EventBridge
まとめ
今回はタグで停止対象を判別するECSの自動停止・起動を紹介いたしました。
停止対象を直接指定していないため、新しいECSのサービスを作成した時にもタグを追加するだけで自動停止・起動の対象とする事ができます。
この記事がどなたかのお役に立てば幸いです。