こんにちは。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構築のポイント
- private subnetからはNAT Gatewayを通じてインターネット経由でECS・ECRにアクセスする想定
- ビルドやECSタスクの更新を頻繁に行う環境であれば、NAT Gatewayの転送料金を防ぐためにVPCエンドポイント経由とするなどの対策が必要
- 参考:そのトラフィック、NATゲートウェイを通す必要ありますか?適切な経路で不要なデータ処理料金は削減しましょう
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で構築する方法を紹介しました。
試したことのない方はぜひ一度やってみてください!
本ブログがどなたかのお役に立てば幸いです。