ECS Fargateのデプロイ環境をCFnでサクッと構築してみた(Rollingアップデート編)

Fargateのデプロイに慣れよう第一弾、Rollingアップデート編です!

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。AWS事業本部トクヤマシュンです。

ECS Fargateのデプロイ方法には、RollingアップデートとBlue/Greenデプロイメントの2種類があります。
それぞれの違いはドキュメントに記載がありますが、実際に触ってみないと中々理解しづらいものです。

そこで今回はCloudFormationを使って両方のデプロイ環境をサクっと構築するためのテンプレートをご紹介します。

長くなってしまったため、本エントリではRollingアップデートを紹介します。
興味のある方は、是非一度ご自身の環境で試してみてください。

Blue/Greenデプロイメントは次のエントリに記載があるので、興味のある方は確認してみてください。

構築する環境

今回の構成は次の図の通りです。

CFnテンプレートなどのソースコード

構築用のCFnテンプレートやパイプラインソースとして利用するDockerファイルを下記GitHubに格納していますので、構築の際はご参考ください。

CFn構築の際はCLIを使うことをオススメします。
parameters/配下に変数ファイルを準備していますので、各自の環境に合わせて適宜設定してください。
リソース名はプレフィックスをSystemName-Environmentで統一しています。
デフォルトでは、SystemName=system, Environment=devとしています。

次章からはCFnテンプレートを確認します。

CFnテンプレートは折りたたんで記載してありますので、ご注意ください

VPC構築

VPCスタック作成

次のCFnテンプレートを使ってスタックを作成し、VPCを構築します。
(VPCはRollingアップデート、Blue/Greenデプロイメント共通です)

vpc.yaml(クリックで展開)

vpc.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  VPC Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "Netowork Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - PublicSubnet1CIDR
          - PublicSubnet2CIDR
          - PrivateSubnet1CIDR
          - PrivateSubnet2CIDR

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String
  VPCCIDR:
    Type: String
    Default: "10.10.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1a"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  PublicSubnet1CIDR:
    Type: String
    Default: "10.10.0.0/24"
  PublicSubnet2CIDR:
    Type: String
    Default: "10.10.1.0/24"
  PrivateSubnet1CIDR:
    Type: String
    Default: "10.10.2.0/24"
  PrivateSubnet2CIDR:
    Type: String
    Default: "10.10.3.0/24"


Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-vpc"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-igw"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  NAT Gateway
  # ------------------------------------------------------------#
  NGW1: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NGW1EIP.AllocationId 
      SubnetId: !Ref PublicSubnet1
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-ngw-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  NGW1EIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-nat-eip-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
    

  NGW2: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NGW2EIP.AllocationId 
      SubnetId: !Ref PublicSubnet2
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-ngw-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  NGW2EIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-nat-eip-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  PublicSubnet
  # ------------------------------------------------------------#
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-public-subnet-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-public-subnet-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Public RouteTable
  # ------------------------------------------------------------#
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-public-rt-1"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  PublicRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-public-rt-2"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

  # ------------------------------------------------------------#
  #  Public Routing
  # ------------------------------------------------------------#
  PublicRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  PublicRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable2
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  Public RouteTable Association
  # ------------------------------------------------------------#
  PublicSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      SubnetId: !Ref PublicSubnet1

  PublicSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable2
      SubnetId: !Ref PublicSubnet2

  # ------------------------------------------------------------#
  #  PrivateSubnet
  # ------------------------------------------------------------#
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-private-subnet-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
 
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-private-subnet-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Private RouteTable
  # ------------------------------------------------------------#
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-private-rt-1"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-private-rt-2"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Private Routing
  # ------------------------------------------------------------#
  PrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NGW1

  PrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NGW2
  # ------------------------------------------------------------#
  #  Private RouteTable Association
  # ------------------------------------------------------------#
  PrivateSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2

VPC構築のポイント

ECR構築

Rollingアップデート用ECRスタック作成

次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のECRを構築します。  

ecr-rolling.yaml(クリックで展開)

ecr-rolling.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  ECR Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

Resources:
  # ------------------------------------------------------------#
  #  ECR
  # ------------------------------------------------------------#
  RollingECR:
    Type: AWS::ECR::Repository
    Properties: 
      RepositoryName: !Sub "${SystemName}-${Environment}-rolling-repo"
      EncryptionConfiguration: 
        EncryptionType: "AES256"
      ImageScanningConfiguration: 
        ScanOnPush: true
      ImageTagMutability: IMMUTABLE
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-repo"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

ECR構築のポイント

  • リポジトリはSSE-S3(AES256)によるサーバー側暗号化を実施
  • push時にイメージスキャンを行う
  • タグはIMMUTABLEとする

ECS構築

Rolling アップデート用ECS

次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のECSを構築します。  

ecs-rolling.yaml

ecs-rolling.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  ECS and ALB Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "Netowork Configuration"
        Parameters:
          - VpcId
          - ALBSubnetId1
          - ALBSubnetId2
          - ECSSubnetId1
          - ECSSubnetId2
          - ALBAllowInboundIP
      - Label:
          default: "Fargate Configuration"
        Parameters:
          - ECSImage
          - ECSTaskCPUUnit
          - ECSTaskMemory
          - ECSTaskDesiredCount
      - Label:
          default: "Scaling Configuration"
        Parameters:
          - ServiceScaleCpuTarget
          - ServiceScaleInCooldown
          - ServiceScaleOutCooldown
          - TaskMinContainerCount
          - TaskMaxContainerCount

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

#VPCID
  VpcId:
    Description : "VPC ID"
    Type: AWS::EC2::VPC::Id
    Default: "vpc-"


#ALBSubnet1
  ALBSubnetId1:
    Description : "ALB Subnet 1st"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-public1Id"

#ALBSubnet2
  ALBSubnetId2:
    Description : "ALB Subnet 2nd"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-public2Id"

#ALBAllowInboundIpAddress
  ALBAllowInboundIP:
    Description : "ALB Subnet 2nd"
    Type: String
    Default: "xxx.xxx.xxx.xxx/32"

#ECSSubnet1
  ECSSubnetId1:
    Description : "ECS Subnet 1st"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-private1Id"

#ECSSubnet2
  ECSSubnetId2:
    Description : "ECS Subnet 2nd"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-private2Id"

#ECSTaskCPUUnit
  ECSTaskCPUUnit:
    Type: String
    Default: "256"

#ECSTaskMemory
  ECSTaskMemory:
    Type: String
    Default: "512"

#ECSImage
  ECSImage:
    Type: String
    Default: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/system-dev-rolling-repo:latest"

#ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Type: Number
    Default: 1

# Scaling params
  ServiceScaleCpuTarget:
    Description: Target Tracking Scaling CPU Target
    Type: Number
    Default: 70

  ServiceScaleInCooldown:
    Type: Number
    Description: Target Tracking Scale In Cooldown seconds
    Default: 180

  ServiceScaleOutCooldown:
    Type: Number
    Description: Target Tracking Scale Out Cooldown seconds
    Default: 60

  TaskMinContainerCount:
    Type: Number
    Description: Minimum number of containers to run for the service
    Default: 1
    ConstraintDescription: Value must be at least one

  TaskMaxContainerCount:
    Type: Number
    Description: Maximum number of containers to run for the service when auto scaling out
    Default: 2
    ConstraintDescription: Value must be at least one

Resources:

  # ------------------------------------------------------------#
  #  Security Group
  # ------------------------------------------------------------#
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ALB Security Group
      GroupName: !Sub "${SystemName}-${Environment}-rolling-alb-sg"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref ALBAllowInboundIP
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-alb-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Task Security Group
      GroupName: !Sub "${SystemName}-${Environment}-rolling-task-sg"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSecurityGroup
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-task-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  ALB
# ------------------------------------------------------------#
  ALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: !Sub "${SystemName}-${Environment}-rolling-alb"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
        - Key: "access_logs.s3.enabled"
          Value: true
        - Key: "access_logs.s3.bucket"
          Value: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
      Scheme: "internet-facing"
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets:
        - !Ref ALBSubnetId1
        - !Ref ALBSubnetId2
      Type: "application"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-alb"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  ALBListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

  TargetGroup:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckPath: /
      VpcId: !Ref VpcId
      Name: !Sub "${SystemName}-${Environment}-rolling-tg"
      Protocol: HTTP
      Port: 80
      TargetType: ip

# ------------------------------------------------------------#
#  ALB Log S3 Bucket
# ------------------------------------------------------------#
  ALBLogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${SystemName}-${Environment}-rolling-alb-log-lifecycle"
            Status: Enabled
            ExpirationInDays: 400
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  logsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: ALBLogBucket
    Properties:
      Bucket: !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
      PolicyDocument:
        Statement:
          - Action:
              - 's3:PutObject'
            Effect: 'Allow'
            Resource:
              - Fn::Join:
                  - ''
                  - - "arn:aws:s3:::"
                    - !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
                    - "/*"
            Principal:
              AWS: '582318560864'
          - Action:
              - 's3:GetBucketAcl'
            Effect: 'Allow'
            Resource:
              Fn::Join:
                  - ''
                  - - "arn:aws:s3:::"
                    - !Sub "${SystemName}-${Environment}-rolling-alb-log-bucket-${AWS::AccountId}"
            Principal:
              Service: 'logdelivery.elb.amazonaws.com'

# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------#
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${SystemName}-${Environment}-rolling-cluster"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-cluster"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  ECS LogGroup
# ------------------------------------------------------------#
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "${SystemName}-${Environment}-rolling-cluster-log"

# ------------------------------------------------------------#
#  ECS Task Execution Role
# ------------------------------------------------------------#
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-rolling-task-execution-role"
      Path: /
      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
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-task-execution-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  ECS TaskDefinition
# ------------------------------------------------------------#
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      Family: !Sub "${SystemName}-${Environment}-rolling-task-definition"
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: app-container
          Image: !Ref ECSImage
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref SystemName
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
          ReadonlyRootFilesystem: false
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-task-definition"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  ECS Service
# ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListener
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      DeploymentConfiguration:
          DeploymentCircuitBreaker: 
            Enable: true
            Rollback: true
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !Ref TargetGroup
          ContainerPort: 80
          ContainerName: !Sub "app-container"
      NetworkConfiguration:
        AwsvpcConfiguration:
            AssignPublicIp: DISABLED
            SecurityGroups:
              - !Ref TaskSecurityGroup
            Subnets:
              - !Ref ECSSubnetId1
              - !Ref ECSSubnetId2
      PlatformVersion: 1.4.0
      ServiceName: !Sub "${SystemName}-${Environment}-rolling-service"
      TaskDefinition: !Ref ECSTaskDefinition
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-service"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  Auto Scaling Service
# ------------------------------------------------------------#
  ServiceAutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-rolling-autoscaling-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: application-autoscaling.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-autoscaling-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
  ServiceAutoScalingPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-rolling-autoscaling-policy"
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - application-autoscaling:*
              - cloudwatch:DescribeAlarms
              - cloudwatch:PutMetricAlarm
              - ecs:DescribeServices
              - ecs:UpdateService
            Resource: '*'
      Roles:
        - !Ref ServiceAutoScalingRole
  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: !Ref TaskMinContainerCount
      MaxCapacity: !Ref TaskMaxContainerCount
      ResourceId: 
        !Join
        - '/'
        - - 'service'
          - !Ref ECSCluster
          - !GetAtt ECSService.Name
      RoleARN: !GetAtt ServiceAutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
    DependsOn:
      - ECSService
      - ServiceAutoScalingRole

  ServiceScalingPolicyCPU:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-rolling-target-tracking-scaling-cpu"
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: !Ref ServiceScaleCpuTarget
        ScaleInCooldown: !Ref ServiceScaleInCooldown
        ScaleOutCooldown: !Ref ServiceScaleOutCooldown
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization
    DependsOn: ServiceScalingTarget

ECS構築のポイント

  • ALBは特定のソースIPからのみアクセス可能な想定
    • 変数ALBAllowInboundIPでセキュリティグループのインバウンドルールで許可するIPアドレスを指定
  • ALBのアクセスログはS3に出力
    • SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定
  • ECSサービスのAutoScalingはCPUの追跡ポリシーを設定
    • 閾値は変数で設定可能。変数値のデフォルトには70%を設定
  • Rollingアップデートではデプロイサーキットブレイカーを有効化

CI/CD環境構築

Rollingアップデート用CI/CDスタック作成

次のCFnテンプレートを使ってスタックを作成し、Rollingアップデート用のCI/CD環境を構築します。  

cicd-rolling.yaml(クリックで展開)

cicd-rolling.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: 
  Rolling Update For ECS Fargate with GitHub

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "VPC Configuration"
        Parameters:
          - VpcId
          - CodeBuildSubnetId1
          - CodeBuildSubnetId2
      - Label:
          default: "ECR・ECS Configuration"
        Parameters:
          - ECSClusterName
          - ECSServiceName
          - ECSTaskContainerName
          - ECRName
      - Label:
          default: "GitHub Configuration"
        Parameters:
          - GitHubOrganizationName
          - GitHubRepositoryName
          - GitHubBranchName

Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

  ECSClusterName:
    Default: system-dev-rolling-cluster
    Description : "CI/CD Deploy ECS Cluster"
    Type: String
  ECSServiceName:
    Default: system-dev-rolling-service
    Description : "CI/CD Deploy ECS Service"
    Type: String
  ECSTaskContainerName:
    Default: app-container
    Description : "CI/CD Deploy ECS Task Container"
    Type: String
  ECRName:
    Default: system-dev-repo
    Description : "ECR Repository Name"
    Type: String
  GitHubOrganizationName:
    Default: GitHubOrganizationName
    Description : "CI/CD GitHub Organization"
    Type: String
  GitHubRepositoryName:
    Default: GitHubRepositoryName
    Description : "CI/CD GitHub Repository"
    Type: String
  GitHubBranchName:
    Default: GitHubBranchName
    Description : "CI/CD GitHub Branch"
    Type: String
  VpcId:
    Default: vpc-
    Description : "VPC ID"
    Type: AWS::EC2::VPC::Id
  CodeBuildSubnetId1:
    Default: subnet-private1Id
    Description : "Private Subnet 1st"
    Type: AWS::EC2::Subnet::Id
  CodeBuildSubnetId2:
    Default: subnet-private2Id
    Description : "Private Subnet 2nd"
    Type: AWS::EC2::Subnet::Id

Resources:
# ------------------------------------------------------------#
#  Security Group
# ------------------------------------------------------------#

  CodeBuildSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Rolling Update CodeBuild Security Group
      GroupName: !Sub "${SystemName}-${Environment}-rolling-code-build-sg"
      VpcId: !Ref VpcId
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-code-build-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  CICD Role
# ------------------------------------------------------------#
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
  CodeBuildServicePolicy:
    Type: AWS::IAM::Policy
    DependsOn: CodeBuildServiceRole
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-rolling-cicd-build-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Resource: "*"
            Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
          - Resource: !Sub arn:aws:s3:::${RollingArtifactBucket}/*
            Effect: Allow
            Action:
              - s3:GetObject
              - s3:PutObject
              - s3:GetObjectVersion
              - s3:GetBucketAcl
              - s3:GetBucketLocation
          - Resource: "*"
            Effect: Allow
            Action:
              - ecr:GetAuthorizationToken
              - ecr:BatchCheckLayerAvailability
              - ecr:GetDownloadUrlForLayer
              - ecr:GetRepositoryPolicy
              - ecr:DescribeRepositories
              - ecr:ListImages
              - ecr:DescribeImages
              - ecr:BatchGetImage
              - ecr:InitiateLayerUpload
              - ecr:UploadLayerPart
              - ecr:CompleteLayerUpload
              - ecr:PutImage
          - Resource: "*"
            Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeDhcpOptions
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
              - ec2:DescribeSubnets
              - ec2:DescribeSecurityGroups
              - ec2:DescribeVpcs
          - Resource: "*"
            Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeDhcpOptions
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
              - ec2:DescribeSubnets
              - ec2:DescribeSecurityGroups
              - ec2:DescribeVpcs
              - ec2:CreateNetworkInterfacePermission
          - Resource: "*"
            Effect: Allow
            Action:
              - codestar-connections:UseConnection
      Roles:
        - !Ref CodeBuildServiceRole

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline-role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
  CodePipelineServicePolicy:
    Type: AWS::IAM::Policy
    DependsOn: CodePipelineServiceRole
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline-policy"
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Resource:
              - !Sub arn:aws:s3:::${RollingArtifactBucket}/*
            Effect: Allow
            Action:
              - s3:PutObject
              - s3:GetObject
              - s3:GetObjectVersion
              - s3:GetBucketVersioning
          - Resource: "*"
            Effect: Allow
            Action:
              - codecommit:GetRepository
              - codecommit:ListBranches
              - codecommit:GetUploadArchiveStatus
              - codecommit:UploadArchive
              - codecommit:CancelUploadArchive
              - codedeploy:CreateDeployment
              - codedeploy:GetApplication
              - codedeploy:GetApplicationRevision
              - codedeploy:GetDeployment
              - codedeploy:GetDeploymentConfig
              - codedeploy:RegisterApplicationRevision
              - codebuild:StartBuild
              - codebuild:StopBuild
              - codebuild:BatchGet*
              - codebuild:Get*
              - codebuild:List*
              - codecommit:GetBranch
              - codecommit:GetCommit
              - s3:*
              - ecs:*
              - elasticloadbalancing:*
              - autoscaling:*
              - iam:PassRole
              - codestar-connections:UseConnection
      Roles:
        - !Ref CodePipelineServiceRole
# ------------------------------------------------------------#
#  Rolling Artifact S3 Bucket
# ------------------------------------------------------------#
  RollingArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${SystemName}-${Environment}-rolling-cicd-artifact-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${SystemName}-${Environment}-rolling-cicd-build-artifact-lifecycle"
            Status: Enabled
            ExpirationInDays: 1827
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-cicd-artifact-${AWS::AccountId}"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  GitHub Connection
# ------------------------------------------------------------#
  GitHubConnection:
    Type: AWS::CodeStarConnections::Connection
    Properties:
      ConnectionName: !Sub "${SystemName}-${Environment}-rolling-cicd-github"
      ProviderType: GitHub
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-cicd-github"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  Code Build LogGroup
# ------------------------------------------------------------#
  CodeBuildLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "${SystemName}-${Environment}-rolling-cicd-code-build"


# ------------------------------------------------------------#
#  Code Build
# ------------------------------------------------------------#
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    DependsOn: CodeBuildServicePolicy
    Properties:
      Name: !Sub "${SystemName}-${Environment}-rolling-cicd-code-build"
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: ECSTaskContainerName
            Value: !Ref ECSTaskContainerName
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          env:
            variables:
              DOCKER_BUILDKIT: "1"
          phases:
            install:
              runtime-versions:
                docker: 19
            pre_build:
              commands:
                - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
                - echo Logging in to ECR
                - aws ecr --region ${AWS_REGION} get-login-password | docker login --username AWS --password-stdin https://${REPOSITORY_URI}
                - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
                - IMAGE_TAG=${COMMIT_HASH:=latest}
            build:
              commands:
                - echo Build started on `date`
                - docker image build -t ${REPOSITORY_URI}:${COMMIT_HASH} .
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images
                - docker image push ${REPOSITORY_URI}:${COMMIT_HASH}
                - printf '[{"name":"%s","imageUri":"%s"}]' ${ECSTaskContainerName} ${REPOSITORY_URI}:${COMMIT_HASH} > imagedefinitions.json
          artifacts:
              files: imagedefinitions.json      
      ServiceRole: !Ref CodeBuildServiceRole
      Cache: 
        Modes: 
          - "LOCAL_DOCKER_LAYER_CACHE"
          - "LOCAL_SOURCE_CACHE"
        Type: "LOCAL"
      LogsConfig: 
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CodeBuildLogGroup
      Visibility: "PRIVATE"
      TimeoutInMinutes: 60
      QueuedTimeoutInMinutes: 480
      VpcConfig:
        SecurityGroupIds: 
          - !Ref CodeBuildSecurityGroup
        Subnets: 
          - !Ref CodeBuildSubnetId1
          - !Ref CodeBuildSubnetId2
        VpcId: !Ref VpcId
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-cicd-build"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  Code Pipeline
# ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    DependsOn: CodePipelineServicePolicy
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline"
      ArtifactStore:
        Type: S3
        Location: !Ref RollingArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeStarSourceConnection
              Configuration:
                FullRepositoryId: !Sub
                  - ${GitHubOrganizationName}/${GitHubRepositoryName}
                  - GitHubOrganizationName: !Ref GitHubOrganizationName
                    GitHubRepositoryName: !Ref GitHubRepositoryName
                ConnectionArn: !Ref GitHubConnection
                BranchName: !Ref GitHubBranchName
                OutputArtifactFormat: CODEBUILD_CLONE_REF
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceArtifact
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: ECS
              Configuration:
                ClusterName: !Ref ECSClusterName
                ServiceName: !Ref ECSServiceName
                FileName: imagedefinitions.json
              RunOrder: 1
              InputArtifacts:
                - Name: BuildArtifact
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-rolling-cicd-pipeline"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

CI/CD環境構築のポイント

  • ソースアクションにはGitHubバージョン2を指定
    • このため、初回のパイプラインは必ず失敗
    • コンソール画面での接続設定後、ソースアクションの実行が可能になる
  • アーティファクトはS3に出力
    • SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定

動作確認

それでは構築の動作確認をします。

### CFn構築

下記の順番に実行します。

  • vpc.yaml
  • ecr-rolling.yaml
    • 構築後、latestタグを付与したイメージをpushすること
  • ecs-rolling.yaml
    • vpcの値に合わせてパラメータファイルを更新すること
  • cicd-rolling.yaml
    • vpc・ecsの値に合わせてパラメータファイルを更新すること

AWS CLIで下記のコマンドを実行することで、スタックを構築します。

aws cloudformation create-stack \
--stack-name スタック名 \
--template-body file://./テンプレート名 \
--capabilities CAPABILITY_NAMED_IAM \
--parameters file://./parameters/パラメータ名 \
--tags Key=Environment,Value=<Environmentに設定する変数値> \
--tags Key=SystemName,Value=<SystemNameに設定する変数値>

GitHubのリポジトリ構築

Rollingアップデート用のリポジトリを作成し、サンプルソースGitHub/フォルダの内容をアップロードします。

AWSとGitHubとの接続

作成したCI/CDパイプラインはGitHubとの接続ができずに初回実行は失敗しています。

コンソール画面からGitHubとの接続の設定を行います。

新しいアプリをインストールする、をクリックします。

GitHubの画面でさきほど作成したリポジトリを選択してSaveします

接続のステータスが利用可能になっていることを確認します。

Rollingアップデートの実行

index.htmlを次のように更新し、GitHubのmainブランチにpushします。

index.html(クリックで展開)

index.html

<!DOCTYPE html>

<head>
  <meta charset="utf-8" />
  <title>testページ</title>
</head>

<body>
  <h1>Rolling Update Demo</h1>
</body>

</html>

pushを契機としてパイプラインが起動されます。

しばらく待つと、ビルドも完了します。
ECRを確認すると、さきほどpushしたGitHubのコミットIDをタグとする新たなイメージが作成されています。

Rollingアップデートの途中は、新たなタスク定義のリビジョンが作られて、そこからタスクが起動されます。
その結果、一時的に新旧2つのバージョンのタスクが存在します。

しばらく待つとRollingアップデートが完了し、古いテスク定義を持つタスクは停止、削除されます。

ブラウザからALBのエンドポイントにアクセスすると、GitHubにpushしたindex.htmlの値に表示が変わっており、更新が完了していることが確認できます。

作成リソースの削除

作成時とは逆の順番で、CloudFormationスタックを削除し、リソースを削除してください。

最後に

ECS Fargateのデプロイについて、Rollingアップデート環境をCFnで構築する方法を紹介しました。
試したことのない方はぜひ一度やってみてください!
本ブログがどなたかのお役に立てば幸いです。