はじめに
今回は、営業時間に応じて2つのECSタスクを自動切り替えする仕組みを構築しました。
やりたいことはシンプルで、以下の動作を自動化したいというものです。
営業時間中:App用ECSタスクへアクセス
営業時間外:閉塞(メンテナンスページ)用ECSタスクへアクセス
LBはNLBを使用します。
コスト削減のため、使用していない方のECSタスクは停止します。
上記を実現するにあたって、初めに試した設計方法では運用面でいくつか課題が見えてきました。
本記事では、実際に実装してみて分かった課題と、最終的に採用した設計について紹介します。
最終的な設計
最終的には以下の設計方法を採用しました。
App用と閉塞用のリスナーをそれぞれ作成し、営業時間に応じてリスナーのポート番号を切り替える方法です。
設計においてのポイント
今回は NLB を使用していますが、もしALBを使用する構成であれば、リスナールールによってよりシンプルに切り替えを実装できる場合があります。
また、NLB も最近のアップデートで加重ターゲットグループ(重みづけ)に対応したため、重みを 100:0 / 0:100 に変更する方式で切り替えるなど、さらに簡潔に実現できるかもしれません。
アップデート情報:AWS Network Load Balancer が加重ターゲットグループのサポートによりデプロイを簡素化(2025/11/19)
https://aws.amazon.com/jp/about-aws/whats-new/2025/11/network-load-balancer-weighted-target-groups/
当初考えていた設計と、そのデメリット
初めはNLBのリスナーを1つだけ作成し、リスナーに紐づけるターゲットグループ(TG)をApp・閉塞で切り替える方式を試していました。
こちらの方法でも切替自体はうまくいきました。
しかし、この設計には運用上の問題が2つあります。
この構成では、営業時間外はApp用ターゲットグループがLBと紐づいていない状態になるので、App用ECSタスクに対してデプロイができません。
結果として、アプリへのデプロイが営業時間内にしかできない状態になってしまいました。
そのため、構築する際は、一旦App用のターゲットグループをLBに紐づけた状態で構築した後に、閉塞用のターゲットグループに紐づけるように手動で切り替えてから閉塞用のターゲットグループを構築するという手間が発生し、IaCの観点からも非常に扱いづらい構成でした。
そのため、常に2つのターゲットグループがLBに紐づいている状態にしなければと考え、リスナーを2つ作成しポート番号を切り替える方法に辿り着きました!
実装
実装方法をTerraformコードで紹介します。
①NLB
・ターゲットグループ
App用と閉塞用のターゲットグループを作成します。
ポート番号はどちらも80番にします。
//App用のターゲットグループ
resource "aws_lb_target_group" "tg_ecs_app" {
name = "tg-ecs-app"
port = 80
protocol = "TCP"
tags = {
Name = "tg-ecs-app"
}
target_type = "ip"
vpc_id = aws_vpc.main.id
}
//閉塞用ターゲットグループ
resource "aws_lb_target_group" "tg_ecs_sorry" {
name = "tg-ecs-sorry"
port = 80
protocol = "TCP"
tags = {
Name = "tg-ecs-sorry"
}
target_type = "ip"
vpc_id = aws_vpc.main.id
}
・リスナー
App用と閉塞用のリスナーを作成します。
先ほど作成したターゲットグループをそれぞれ紐づけます。
Terraformコードでは、営業時間中(App用ECSタスクにアクセスする状態)を想定してポートを設定しておきますが、時間帯によってポート番号は切り替わるため、差分が発生しないようにportはignore_changesしておきます。
//App用のリスナー
resource "aws_lb_listener" "nlb_ecs_app" {
load_balancer_arn = aws_lb.nlb_ecs_app.arn
port = 80
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg_ecs_app.arn
}
lifecycle {
ignore_changes = [
port //営業時間で切り替わるため
]
}
}
//閉塞用のリスナー
resource "aws_lb_listener" "nlb_ecs_sorry" {
load_balancer_arn = aws_lb.nlb_ecs_app.arn
port = 8080
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg_ecs_sorry.arn
}
lifecycle {
ignore_changes = [
port //営業時間で切り替わるため
]
}
}
②Lambda
営業開始時と終了時に実行する2つのLambdaを作成します。
Lambdaで行いたいことは以下です。
【営業開始時に実行するLambda】
・App用タスク起動
・閉塞用タスク停止
・App用リスナーのポートを80に変更
・閉塞用リスナーのポートを8080に変更
【営業終了時に実行するLambda】
・閉塞用タスク起動
・App用タスク停止
・閉塞用リスナーのポートを80に変更
・App用リスナーのポートを8081に変更
初めはポート番号80と8080の交換を行うつもりでしたが、同時に同じポートは設定できないため交換時にエラーが発生してしまうので、80 と8080 or 8081を使用しています。
import boto3
import os
elbv2 = boto3.client('elbv2', region_name='ap-northeast-1')
ecs = boto3.client('ecs', region_name='ap-northeast-1')
appautoscaling = boto3.client('application-autoscaling')
LISTENER_APP_ARN = os.environ['LISTENER_APP_ARN']
LISTENER_SORRY_ARN = os.environ['LISTENER_SORRY_ARN']
CLUSTER_NAME = os.environ['CLUSTER_NAME']
APP_SERVICE_NAME = os.environ['APP_SERVICE_NAME']
SORRY_SERVICE_NAME = os.environ['SORRY_SERVICE_NAME']
def lambda_handler(event, context):
# appタスクを起動
ecs.update_service(
cluster=CLUSTER_NAME,
service=APP_SERVICE_NAME,
desiredCount=1
)
# 閉塞用リスナーのポートを8080に変更
elbv2.modify_listener(
ListenerArn=LISTENER_SORRY_ARN,
Port=8080
)
# App用リスナーのポートを80に変更
elbv2.modify_listener(
ListenerArn=LISTENER_APP_ARN,
Port=80
)
# Auto Scaling を有効化(Min=2, Max=5)
appautoscaling.register_scalable_target(
ServiceNamespace='ecs',
ResourceId=f'service/{CLUSTER_NAME}/{APP_SERVICE_NAME}',
ScalableDimension='ecs:service:DesiredCount',
MinCapacity=2,
MaxCapacity=5
)
# 閉塞タスクを停止
ecs.update_service(
cluster=CLUSTER_NAME,
service=SORRY_SERVICE_NAME,
desiredCount=0
)
return {
'statusCode': 200,
'body': 'Switched to TG_APP, started app task, stopped sorry task'
}
※営業開始時と終了時のコード構成はほとんど同じなので、営業終了時Lambdaのコード掲載は省略します。
ECSタスクにAuto Scalingを設定している場合は、ECSタスクの起動/停止の操作の際にAutoScalingの有効化/無効化も行います。
※今回はApp用タスクのみAutoScalingを設定しています。
LambdaのIAMロールには、適宜必要な権限を付与してください。
③EventBridge
営業開始時と終了時にLambdaを実行するEventBridgeルールを作成します。
cron形式で実行したい時間を指定します
# 営業開始時実行ルール(JST 05:00 → UTC 20:00)
resource "aws_cloudwatch_event_rule" "start_business_hours" {
name = "start-business-hours-rule"
schedule_expression = "cron(0 20 * * ? *)"
state = "ENABLED"
}
# ターゲット
resource "aws_cloudwatch_event_target" "start_lambda_target" {
rule = aws_cloudwatch_event_rule.start_business_hours.name
target_id = "StartBusinessHoursLambda"
arn = aws_lambda_function.business_start.arn
}
# Lambda実行許可
resource "aws_lambda_permission" "allow_eventbridge_start" {
statement_id = "AllowExecutionFromEventBridgeStart"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.business_start.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.start_business_hours.arn
}
上記の実装により、営業時間に応じた2つのECSタスクの自動切り替えが行えるようになりました。
まとめ
今回は、営業時間に応じた2つのECSタスクの自動切り替えを行ってみました!
以前に、EventBridgeとLambdaを使用してRDSの自動停止の実装をしたことがあったので、同じ要領で出来るかな~と簡単に考えていましたが、なかなか苦戦してしまいました。。。
実装自体はあまり難しくないですが、運用・管理まで考えた設計が大事だと実感しました。
今回は、リスナーのポート切り替えを利用しましたが、他にもより良い設計や運用の工夫があるかと思いますので、もしご意見や別アプローチがありましたら、コメントをいただけると嬉しいです!
We Are Hiring!


