2025年7月頃に発表されたECSのネイティブBlue/Greenデプロイをようやく触ってみたのでブログに残します。
ECSのネイティブBlue/Greenデプロイとは
今までECSへのBlue/Greenデプロイを行うにはCodeDeployが必要でした。
ネイティブBlue/Greenデプロイの場合CodeDeployが不要になるため設定時に追加のコンポーネントが不要になり、シンプルな構成とすることができます。
また、CodeDeployのBlue/Greenデプロイを使用している場合、Service Connectが使用できなかったりとBlue/Greenデプロイを使用するための制限がいくつかありました。
ネイティブBlue/Greenデプロイであればそこら辺の制約が少なく柔軟に対応することが可能になっています。
今回作成する構成
簡易的な構成図ですが、以下の構成を作成してBlue/Greenデプロイを試していきます。

設定
ECRとコンテナイメージの準備
ECRは以下のCloudFormationテンプレートで作成してください。
デプロイ方法などは以下のブログを参考にしてください。
https://qiita.com/kobayashi_0226/items/ba67bab2645405257bd2#3-ecr%E3%81%A8%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E4%BD%9C%E6%88%90
AWSTemplateFormatVersion: "2010-09-09"
Description: ECR
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
Resources:
# ------------------------------------------------------------#
# ECR
# ------------------------------------------------------------#
ECR:
Type: AWS::ECR::Repository
Properties:
EmptyOnDelete: true
EncryptionConfiguration:
EncryptionType: AES256
RepositoryName: !Sub ecr-${env}
Dockerfileは以下のものを使用します。
HTMLファイルは好きなものを使用してください。
FROM public.ecr.aws/docker/library/httpd:2.4
COPY ./html/ /usr/local/apache2/htdocs/
ECS周りのリソース作成
以下のドキュメントを参考にしつつCloudFormationでリソースを作成していきます。
- Amazon ECS ブルー/グリーンデプロイメントの作成 - Amazon Elastic Container Service
- ロードバランサー用の Amazon ECS インフラストラクチャの IAM ロール - Amazon Elastic Container Service
コンテナイメージをECRへプッシュしたら以下のCloudFormationテンプレートを使用してリソースを作成してください。
AWSTemplateFormatVersion: "2010-09-09"
Description: ECS
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
- Label:
default: Parameters for Network
Parameters:
- VPCCIDR
- PublicSubnet01CIDR
- PublicSubnet02CIDR
- PrivateSubnet01CIDR
- PrivateSubnet02CIDR
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
VPCCIDR:
Default: 192.168.0.0/16
Type: String
PublicSubnet01CIDR:
Default: 192.168.0.0/24
Type: String
PublicSubnet02CIDR:
Default: 192.168.1.0/24
Type: String
PrivateSubnet01CIDR:
Default: 192.168.2.0/24
Type: String
PrivateSubnet02CIDR:
Default: 192.168.3.0/24
Type: String
Resources:
# ------------------------------------------------------------#
# CloudWatch Logs
# ------------------------------------------------------------#
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/ecs-${env}-log"
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
RoleName: !Sub iam-${env}-ecs-tast-execution-role
ECSLBRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers
RoleName: !Sub iam-${env}-ecs-lb-role
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub vpc-${env}
# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub igw-${env}
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------#
PublicSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PublicSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub1
VpcId: !Ref VPC
PublicSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PublicSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub2
VpcId: !Ref VPC
PrivateSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PrivateSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv1
VpcId: !Ref VPC
PrivateSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PrivateSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv2
VpcId: !Ref VPC
# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------#
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-pub
PublicRouteTableRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet01
PublicRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet02
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-prv
PrivateRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet01
PrivateRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet02
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
ALBSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for alb
GroupName: !Sub securitygroup-${env}-alb
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 80
- FromPort: 8080
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 8080
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-alb
VpcId: !Ref VPC
ECSSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for ecs
GroupName: !Sub securitygroup-${env}-ecs
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
SourceSecurityGroupId: !Ref ALBSG
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-ecs
VpcId: !Ref VPC
VPCEndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for vpc endpoint
GroupName: !Sub securitygroup-${env}-vpc-endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 443
IpProtocol: tcp
SourceSecurityGroupId: !Ref ECSSG
ToPort: 443
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-vpc-endpoint
VpcId: !Ref VPC
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
ECRdkrEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
ECRapiEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
LogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
SecurityGroupIds:
- !Ref VPCEndpointSG
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: false
Name: !Sub alb-${env}-ecs
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSG
Subnets:
- !Ref PublicSubnet01
- !Ref PublicSubnet02
Tags:
- Key: Name
Value: !Sub alb-${env}-ecs
Type: application
TargetGroup1:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-01
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-01
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
TargetGroup2:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-02
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-02
TargetType: ip
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
ALBHTTPListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
StatusCode: 403
Type: fixed-response
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
ALBHTTPListenerRule1:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup1
Type: forward
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- "/*"
ListenerArn: !Ref ALBHTTPListener1
Priority: 1
ALBHTTPListener2:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
StatusCode: 403
Type: fixed-response
LoadBalancerArn: !Ref ALB
Port: 8080
Protocol: HTTP
ALBHTTPListenerRule2:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup2
Type: forward
Conditions:
- Field: path-pattern
PathPatternConfig:
Values:
- "/*"
ListenerArn: !Ref ALBHTTPListener2
Priority: 1
# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------#
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
CapacityProviders:
- FARGATE
ClusterName: !Sub ecs-${env}-cluster
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 1
ECSTaskDef:
Type: AWS::ECS::TaskDefinition
Properties:
ContainerDefinitions:
- Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-${env}:latest
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref "AWS::Region"
awslogs-stream-prefix: !Sub ecs-${env}-log
Name: !Sub task-${env}
PortMappings:
- ContainerPort: 80
HostPort: 80
Cpu: 256
ExecutionRoleArn: !Ref TaskExecutionRole
Family: !Sub task-${env}
Memory: 512
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener1
- ALBHTTPListener2
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LoadBalancers:
- AdvancedConfiguration:
AlternateTargetGroupArn: !Ref TargetGroup2
ProductionListenerRule: !Ref ALBHTTPListenerRule1
RoleArn: !GetAtt ECSLBRole.Arn
TestListenerRule: !Ref ALBHTTPListenerRule2
ContainerName: !Sub task-${env}
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
ServiceName: !Sub service-${env}
TaskDefinition: !Ref ECSTaskDef
PlatformVersion: LATEST
DeploymentController:
Type: ECS
DeploymentConfiguration:
Strategy: BLUE_GREEN
上記で重要なのはIAMロールとECSサービス部分になると思います。
IAMロールではECSからALBの操作を行うためにAmazonECSInfrastructureRolePolicyForLoadBalancersというマネージドポリシーを設定しています。
ECSサービスではデプロイメントコントローラでECSを設定してデプロイ戦略でBLUE_GREENを設定しています。
LoadBalancersの部分では本番リスナーやテストリスナーを設定しており、リスナールールのARNを指定するためにALBではAWS::ElasticLoadBalancingV2::ListenerRuleを使用してデフォルト以外のリスナールールを追加しています。
動きを見てみる
タスク定義を更新してデプロイしてみる
CloudFormationデプロイ後、ALBのDNS名から正常にアクセスできることが確認できたらタスク定義を更新してECSサービスの更新を行います。
サービスの更新を開始すると以下のように新しいデプロイが実行されます。

進行中のデプロイ欄では現在どのターゲットグループにどの環境が紐づいているのか確認することができます。
ちなみにCodeDeployのように徐々にトラフィックをGreen環境に移していくというオプションは現在 (2025/11/24) なさそうでした。

しばらくするとデプロイが完了してALBのDNS名からアクセスすると更新が完了していることが確認できます。
Green環境のテストはどうやるのか
CodeDeployを使用しているときはBlue環境からGreen環境に切り替えるまでの時間を指定できたのですが、ECSのネイティブBlue/Greenデプロイではそのような設定が現在ないようです。
その代わりLambdaを紐づけてデプロイ時に実行してテストするような機能が存在します。
デプロイのワークフローは以下のドキュメントに記載されており、現在ステータスが10個存在します。
今回はGreen環境ができてBlue環境から切り替える前にテストがしたいのでPOST_TEST_TRAFFIC_SHIFTを使用すればよさそうです。(ドキュメントを読み違えてなければ正しいはず)
テストトラフィックの移行が完了しました。グリーンサービスリビジョンにより、テストトラフィックの 100% が処理されます。
テスト用のLambdaは以下のAWSブログを参考に作成します。
Lambdaのコードは以下のようにしてみました。
シンプルにGETリクエストを送ってステータスコードを確認して200~299であればレスポンスとして"hookStatus": "SUCCEEDED"を返しています。
途中で60秒スリープしていますがこの間にブラウザからテストリスナーと本番リスナーにアクセスしてみます。(実際の運用環境では不要だと思いますが今回は検証のために入れてます)
import json
import urllib3
import logging
import base64
import os
import time
# ログ記録を設定します
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# HTTP クライアントを初期化します
http = urllib3.PoolManager()
def lambda_handler(event, context):
logger.info(f"Event: {json.dumps(event)}")
logger.info(f"Context: {context}")
try:
# 実際のシナリオでは、テストエンドポイント URL を作成します
test_endpoint = os.getenv("APP_URL")
# Getリクエストを実行
response = http.request('GET', "http://" + test_endpoint + ":8080")
logger.info(f"GET / response status: {response.status}")
# レスポンスに OK ステータスコード (200~299 の範囲) が含まれているかどうかを確認します
time.sleep(60)
if 200 <= response.status < 300:
logger.info("Get request success - received OK status code")
return {
"hookStatus": "SUCCEEDED"
}
else:
logger.error(f"Get request failed - status code: {response.status}")
return {
"hookStatus": "FAILED"
}
except Exception as error:
logger.error(f"Get request failed failed: {str(error)}")
return {
"hookStatus": "FAILED"
}
CloudFormationテンプレートを更新してLambdaリソースなどを含めます。(変更追加した部分のみ記載しています)
ECSLambdaInvokeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ecs.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub iam-${env}-ecs-lambda-invoke-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: 'lambda:InvokeFunction'
Resource: '*'
RoleName: !Sub iam-${env}-ecs-lambda-invoke-role
ECSService:
Type: AWS::ECS::Service
DependsOn:
- ALBHTTPListener1
- ALBHTTPListener2
Properties:
Cluster: !Ref ECSCluster
DesiredCount: 1
LoadBalancers:
- AdvancedConfiguration:
AlternateTargetGroupArn: !Ref TargetGroup2
ProductionListenerRule: !Ref ALBHTTPListenerRule1
RoleArn: !GetAtt ECSLBRole.Arn
TestListenerRule: !Ref ALBHTTPListenerRule2
ContainerName: !Sub task-${env}
ContainerPort: 80
TargetGroupArn: !Ref TargetGroup1
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSSG
Subnets:
- !Ref PrivateSubnet01
- !Ref PrivateSubnet02
ServiceName: !Sub service-${env}
TaskDefinition: !Ref ECSTaskDef
PlatformVersion: LATEST
DeploymentController:
Type: ECS
DeploymentConfiguration:
Strategy: BLUE_GREEN
LifecycleHooks:
- HookTargetArn: !GetAtt Lambda.Arn
LifecycleStages:
- POST_TEST_TRAFFIC_SHIFT
RoleArn: !GetAtt ECSLambdaInvokeRole.Arn
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
LambdaIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub policy-${env}-lambda
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
Lambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import json
import urllib3
import logging
import base64
import os
import time
# ログ記録を設定します
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# HTTP クライアントを初期化します
http = urllib3.PoolManager()
def lambda_handler(event, context):
logger.info(f"Event: {json.dumps(event)}")
logger.info(f"Context: {context}")
try:
# 実際のシナリオでは、テストエンドポイント URL を作成します
test_endpoint = os.getenv("APP_URL")
# Getリクエストを実行
response = http.request('GET', "http://" + test_endpoint + ":8080")
logger.info(f"GET / response status: {response.status}")
# レスポンスに OK ステータスコード (200~299 の範囲) が含まれているかどうかを確認します
time.sleep(60)
if 200 <= response.status < 300:
logger.info("Get request success - received OK status code")
return {
"hookStatus": "SUCCEEDED"
}
else:
logger.error(f"Get request failed - status code: {response.status}")
return {
"hookStatus": "FAILED"
}
except Exception as error:
logger.error(f"Get request failed failed: {str(error)}")
return {
"hookStatus": "FAILED"
}
Environment:
Variables:
APP_URL: !GetAtt ALB.DNSName
FunctionName: !Sub lambda-${env}-ecs-hook
Handler: index.lambda_handler
Role: !GetAtt LambdaIAMRole.Arn
Runtime: python3.13
Timeout: 90
CloudFormationスタックを更新後、再度ECSサービス更新します。(上記の更新を入れたところECSサービスが参照していたタスク定義が一つ前に戻ったのでIaCで管理してると少し面倒なことが起きそうな予感がしてます)
デプロイ中に本番リスナーとテストリスナーを確認するとちゃんと異なるターゲットグループにトラフィックを流していることが確認できます。

CloudWatch LogsでLambdaのログを確認するとリクエストが送られていることも確認できます。
[DEBUG] 2025-11-24T14:11:25.356Z 12345678-1234-1234-1234-123456789012 http://alb-dev-ecs-123456.ap-northeast-1.elb.amazonaws.com:8080 "GET / HTTP/1.1" 200 255
CI/CDに組み込む場合
CodePipelineを使用している場合は以下のブログで紹介されているようにデプロイステージでECSアクションを選択すると使用できるようです。
GitHub Actionsの場合はaws-actions/amazon-ecs-deploy-task-definition@v2を使用してデプロイすることが可能です。
サンプルですが以下のようなActionsファイルでデプロイができました。
OIDCやIAMの設定はこちらのブログを参考にしてください。
name: ECS deploy
on:
pull_request:
branches:
- main
types: [closed]
env:
AWS_ACCOUNT_ID: AWSアカウントID
TASK_DEF: "task-dev"
ECS_SERVICE: "service-dev"
ECS_CLUSTER: "ecs-dev-cluster"
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
defaults:
run:
working-directory: ./
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/role-dev-github-oidc-ecs-native-blue-green-001"
aws-region: "ap-northeast-1"
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v2
id: login-ecr
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: "ecr-dev"
IMAGE_TAG: ${{ github.sha }}
run: |
docker build . --tag ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
echo "image=${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}" >> $GITHUB_OUTPUT
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition ${{ env.TASK_DEF }} --query taskDefinition > task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.TASK_DEF }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
最後に
CodeDeployよりも柔軟性が多少減った (ここら辺は今後のアップデートに期待) ように見えますが、設定自体はシンプルでコンポーネントを減らせる部分が魅力的だと思います。
