2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECSのネイティブBlue/Greenデプロイを触ってみた

2
Last updated at Posted at 2025-11-24

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デプロイを試していきます。
ecs_native_blue_green.drawio.png

設定

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でリソースを作成していきます。

コンテナイメージを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サービスの更新を行います。
サービスの更新を開始すると以下のように新しいデプロイが実行されます。
スクリーンショット 2025-11-24 223248.png

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

しばらくするとデプロイが完了して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で管理してると少し面倒なことが起きそうな予感がしてます)

デプロイ中に本番リスナーとテストリスナーを確認するとちゃんと異なるターゲットグループにトラフィックを流していることが確認できます。
スクリーンショット 2025-11-24 231656.png

スクリーンショット 2025-11-24 231834.png

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よりも柔軟性が多少減った (ここら辺は今後のアップデートに期待) ように見えますが、設定自体はシンプルでコンポーネントを減らせる部分が魅力的だと思います。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?