この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おはようございます、もきゅりんです。
皆さん、Blue/Green デプロイメントしてますか?
Blue/Green デプロイメントとは、はすでに理解されていることを前提として話を進めます。
本稿は、CloudFormationとAWS CLIでFargateのBlue/Green Deployment環境を構築する #Fargate | DevelopersIO の内容をカスタムリソースを利用して CloudFormation 一撃で構築するものになります。
構成図は以下です。
プレースホルダを用いて以下のBlue/Green デプロイメントを実現します。
プレースホルダ
とは?という方は、 チュートリアル: ソースと ECS と CodeDeploy 間のデプロイでパイプラインを作成するAmazon ECR - AWS CodePipeline をご参照下さい。
なお、本稿の別アプローチで AWS CloudFormation を使用して CodeDeploy による ECS ブルー/グリーンデプロイを実行する - AWS CloudFormation の方法でも CloudFormation 一撃で ECS の Blue/Green デプロイメント環境を構築することもできます。
これらの違いについては、[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築するで説明していますので、興味がある方はご参照下さい。
カスタムリソースとは
カスタムリソースとは?という人は カスタムリソース - AWS CloudFormation をご参照下さい。
カスタムリソースを使用すると、テンプレートにカスタムのプロビジョニングロジックを記述し、ユーザーがスタックを作成、更新(カスタムリソースを変更した場合)、削除するたびに AWS CloudFormation がそれを実行します。たとえば、AWS CloudFormation のリソースタイプとして使用できないリソースを含める必要があるとします。それらのリソースは、カスタム リソースを使用して含めることができます。この方法により、すべての関連リソースを 1 つのスタックで管理できます。
本稿では、AWS Lambda-backed カスタムリソース - AWS CloudFormation を使って、AWS CloudFormation とLambda 関数との組み合わせで環境を構築します。
なんでそんなことわざわざするの?
現状(2021/3/10)、CloudFormation による CodeDeploy の ECS Blue/Green Deployment Group 作成は非対応のためです。
CloudFormation と AWS CLI で作れますが、どうせなら CFn 一撃で作りたいよね、ということで作った次第です。
以下の図でいうと、赤枠の部分を Lambda のカスタムリソースが作成します。
前提条件
ベースは CloudformationでFargateを構築する | DevelopersIO のブログなので、こちらと同様です。
- 利用予定のアカウントで、AWS CLIが利用可能
- VPC、ALB Security Group、ECS Task Security Groupが構築済み
- Fargateを配置するサブネットは、NATゲートウェイ等経由でインターネット通信可能であること
ついでだったのでALBログS3バケットはリソースに加えています。
Cloudformationテンプレート
注記
Lambda 関数は Python で記載しているので、必要に応じて、CodeDeploy — Boto3 Docs 1.17.23 documentation を確認の上、パラメータを微調整してご利用頂ければと思います。
CodeDeployのデプロイメントグループのデプロイ設定は下記です。
- トラフィックを再ルーティングさせるのは5分後です。
- デプロイ設定は
CodeDeployDefault.ECSLinear10PercentEvery1Minutes
です。 - 元のリビジョンの終了は30分後です。
- デプロイが失敗したらロールバックします。
sample.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Fargate and ALB and CodeDeploy for Blue/Green Create.
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: 'Project Name Prefix'
Parameters:
- ProjectName
- Label:
default: 'InternetALB Configuration'
Parameters:
- InternetALBName
- TargetGroupName1
- TargetGroupName2
- Label:
default: 'Fargate for ECS Configuration'
Parameters:
- ECSClusterName
- ECSTaskName
- ECSTaskCPUUnit
- ECSTaskMemory
- ECSContainerName
- ECSImageName
- ECSServiceName
- ECSTaskDesiredCount
- Label:
default: 'Netowork Configuration'
Parameters:
- VpcId
- ALBSecurityGroupId
- ALBSubnetId1
- ALBSubnetId2
- ECSSecurityGroupId
- ECSSubnetId1
- ECSSubnetId2
- Label:
default: 'Scaling Configuration'
Parameters:
- ServiceScaleEvaluationPeriods
- ServiceCpuScaleOutThreshold
- ServiceCpuScaleInThreshold
- TaskMinContainerCount
- TaskMaxContainerCount
- Label:
default: 'CodeDeploy Configuration'
Parameters:
- CodeDeployAppName
- CodeDeployDeploymentGroupName
ParameterLabels:
InternetALBName:
default: 'InternetALBName'
TargetGroupName1:
default: 'TargetGroupName1'
TargetGroupName2:
default: 'TargetGroupName2'
ECSClusterName:
default: 'ECSClusterName'
ECSTaskName:
default: 'ECSTaskName'
ECSTaskCPUUnit:
default: 'ECSTaskCPUUnit'
ECSTaskMemory:
default: 'ECSTaskMemory'
ECSContainerName:
default: 'ECSContainerName'
ECSImageName:
default: 'ECSImageName'
ECSServiceName:
default: 'ECSServiceName'
ECSTaskDesiredCount:
default: 'ECSTaskDesiredCount'
CodeDeployAppName:
default: 'CodeDeployAppName'
CodeDeployDeploymentGroupName:
default: 'CodeDeployDeploymentGroupName'
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
ProjectName:
Default: sample-fargate
Type: String
#VPCID
VpcId:
Description: 'VPC ID'
Type: AWS::EC2::VPC::Id
#ALBSecurity Group
ALBSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
#ALBSubnet1
ALBSubnetId1:
Description: 'ALB Subnet 1st'
Type: AWS::EC2::Subnet::Id
#ALBSubnet2
ALBSubnetId2:
Description: 'ALB Subnet 2st'
Type: AWS::EC2::Subnet::Id
#ECSSecurity Group
ECSSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
#ECSSubnet1
ECSSubnetId1:
Description: 'ECS Subnet 1st'
Type: AWS::EC2::Subnet::Id
#ECSSubnet2
ECSSubnetId2:
Description: 'ECS Subnet 2st'
Type: AWS::EC2::Subnet::Id
#InternetALB
InternetALBName:
Type: String
Default: 'alb'
#TargetGroupName1
TargetGroupName1:
Type: String
Default: 'tg1'
#TargetGroupName2
TargetGroupName2:
Type: String
Default: 'tg2'
#ECSClusterName
ECSClusterName:
Type: String
Default: 'cluster'
#ECSTaskName
ECSTaskName:
Type: String
Default: 'task'
#ECSTaskCPUUnit
ECSTaskCPUUnit:
AllowedValues: [256, 512, 1024, 2048, 4096]
Type: String
Default: 256
#ECSTaskMemory
ECSTaskMemory:
AllowedValues: [256, 512, 1024, 2048, 4096]
Type: String
Default: 512
#ECSContainerName
ECSContainerName:
Type: String
Default: 'container'
#ECSImageName
ECSImageName:
Type: String
Default: 'nginxdemos/hello:latest'
#ECSServiceName
ECSServiceName:
Type: String
Default: 'service'
CodeDeployAppName:
Type: String
Default: 'app'
CodeDeployDeploymentGroupName:
Type: String
Default: 'dg'
#ECSTaskDesiredCount
ECSTaskDesiredCount:
Type: Number
Default: 1
# Scaling params
ServiceScaleEvaluationPeriods:
Description: The number of periods over which data is compared to the specified threshold
Type: Number
Default: 2
MinValue: 2
ServiceCpuScaleOutThreshold:
Type: Number
Description: Average CPU value to trigger auto scaling out
Default: 50
MinValue: 0
MaxValue: 100
ConstraintDescription: Value must be between 0 and 100
ServiceCpuScaleInThreshold:
Type: Number
Description: Average CPU value to trigger auto scaling in
Default: 25
MinValue: 0
MaxValue: 100
ConstraintDescription: Value must be between 0 and 100
TaskMinContainerCount:
Type: Number
Description: Minimum number of containers to run for the service
Default: 1
MinValue: 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
MinValue: 1
ConstraintDescription: Value must be at least one
Resources:
# ------------------------------------------------------------#
# Target Group
# ------------------------------------------------------------#
TargetGroup1:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
VpcId: !Ref VpcId
Name: !Sub '${ProjectName}-${TargetGroupName1}'
Protocol: HTTP
Port: 80
TargetType: ip
TargetGroup2:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
VpcId: !Ref VpcId
Name: !Sub '${ProjectName}-${TargetGroupName2}'
Protocol: HTTP
Port: 80
TargetType: ip
# ------------------------------------------------------------#
# Internet ALB
# ------------------------------------------------------------#
InternetALB:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
Name: !Sub '${ProjectName}-${InternetALBName}'
Tags:
- Key: Name
Value: !Sub '${ProjectName}-${InternetALBName}'
Scheme: 'internet-facing'
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 'alb-log-${AWS::AccountId}'
SecurityGroups:
- !Ref ALBSecurityGroupId
Subnets:
- !Ref ALBSubnetId1
- !Ref ALBSubnetId2
ALBListener1:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup1
Type: forward
LoadBalancerArn: !Ref InternetALB
Port: 80
Protocol: HTTP
ALBListener2:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup2
Type: forward
LoadBalancerArn: !Ref InternetALB
Port: 8080
Protocol: HTTP
# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------#
ECSCluster:
Type: 'AWS::ECS::Cluster'
Properties:
ClusterName: !Sub '${ProjectName}-${ECSClusterName}'
# ------------------------------------------------------------#
# ECS LogGroup
# ------------------------------------------------------------#
ECSLogGroup:
Type: 'AWS::Logs::LogGroup'
Properties:
LogGroupName: !Sub '/ecs/logs/${ProjectName}-ecs-group'
# ------------------------------------------------------------#
# ECS Task Execution Role
# ------------------------------------------------------------#
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ProjectName}-ECSTaskExecutionRolePolicy'
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
# ------------------------------------------------------------#
# ECS TaskDefinition
# ------------------------------------------------------------#
ECSTaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
Properties:
Cpu: !Ref ECSTaskCPUUnit
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
Family: !Sub '${ProjectName}-${ECSTaskName}'
Memory: !Ref ECSTaskMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
#ContainerDefinitions
ContainerDefinitions:
- Name: !Sub '${ProjectName}-${ECSContainerName}'
Image: !Ref ECSImageName
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: !Ref ProjectName
MemoryReservation: 128
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
# ------------------------------------------------------------#
# ECS Service
# ------------------------------------------------------------#
ECSService:
Type: AWS::ECS::Service
DependsOn: ALBListener1
Properties:
Cluster: !Ref ECSCluster
DesiredCount: !Ref ECSTaskDesiredCount
DeploymentController:
Type: CODE_DEPLOY
LaunchType: FARGATE
LoadBalancers:
- TargetGroupArn: !Ref TargetGroup1
ContainerPort: 80
ContainerName: !Sub '${ProjectName}-${ECSContainerName}'
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref ECSSecurityGroupId
Subnets:
- !Ref ECSSubnetId1
- !Ref ECSSubnetId2
ServiceName: !Sub '${ProjectName}-${ECSServiceName}'
TaskDefinition: !Ref ECSTaskDefinition
# ------------------------------------------------------------#
# Auto Scaling Service
# ------------------------------------------------------------#
ServiceAutoScalingRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: application-autoscaling.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: !Sub '${ProjectName}-${ECSContainerName}-autoscaling'
PolicyDocument:
Statement:
- Effect: Allow
Action:
- application-autoscaling:*
- cloudwatch:DescribeAlarms
- cloudwatch:PutMetricAlarm
- ecs:DescribeServices
- ecs:UpdateService
Resource: '*'
ServiceScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MinCapacity: !Ref TaskMinContainerCount
MaxCapacity: !Ref TaskMaxContainerCount
ResourceId: !Sub
- service/${EcsClusterName}/${EcsDefaultServiceName}
- EcsClusterName: !Ref ECSCluster
EcsDefaultServiceName: !Sub '${ProjectName}-${ECSServiceName}'
RoleARN: !GetAtt ServiceAutoScalingRole.Arn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
DependsOn:
- ECSService
ServiceScaleOutPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub '${ProjectName}-${ECSServiceName}-ScaleOutPolicy'
PolicyType: StepScaling
ScalingTargetId: !Ref ServiceScalingTarget
StepScalingPolicyConfiguration:
AdjustmentType: ChangeInCapacity
Cooldown: 60
MetricAggregationType: Average
StepAdjustments:
- ScalingAdjustment: 1
MetricIntervalLowerBound: 0
ServiceScaleInPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub '${ProjectName}-${ECSServiceName}-ScaleInPolicy'
PolicyType: StepScaling
ScalingTargetId: !Ref ServiceScalingTarget
StepScalingPolicyConfiguration:
AdjustmentType: ChangeInCapacity
Cooldown: 60
MetricAggregationType: Average
StepAdjustments:
- ScalingAdjustment: -1
MetricIntervalUpperBound: 0
ServiceScaleOutAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${ProjectName}-${ECSServiceName}-ScaleOutAlarm'
EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
Statistic: Average
TreatMissingData: notBreaching
Threshold: !Ref ServiceCpuScaleOutThreshold
AlarmDescription: Alarm to add capacity if CPU is high
Period: 60
AlarmActions:
- !Ref ServiceScaleOutPolicy
Namespace: AWS/ECS
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !Sub '${ProjectName}-${ECSServiceName}'
ComparisonOperator: GreaterThanThreshold
MetricName: CPUUtilization
DependsOn:
- ECSService
ServiceScaleInAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${ProjectName}-${ECSServiceName}-ScaleInAlarm'
EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
Statistic: Average
TreatMissingData: notBreaching
Threshold: !Ref ServiceCpuScaleInThreshold
AlarmDescription: Alarm to reduce capacity if container CPU is low
Period: 300
AlarmActions:
- !Ref ServiceScaleInPolicy
Namespace: AWS/ECS
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !Sub '${ProjectName}-${ECSServiceName}'
ComparisonOperator: LessThanThreshold
MetricName: CPUUtilization
DependsOn:
- ECSService
# ------------------------------------------------------------#
# BlueGreen CodeDeploy Role
# ------------------------------------------------------------#
CodeDeployRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ProjectName}-CodeDeployRole'
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codedeploy.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
LambdaFunction:
Type: 'AWS::Lambda::Function'
DeletionPolicy: 'Delete'
Properties:
Code:
ZipFile: |
import boto3
import json
import logging
import cfnresponse
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = boto3.client('codedeploy')
def lambda_handler(event, context):
appName = event['ResourceProperties']['appName']
deploymentGroup = event['ResourceProperties']['deploymentGroup']
clusterName = event['ResourceProperties']['ECSClusterName']
serviceName = event['ResourceProperties']['ECSServiceName']
print('REQUEST RECEIVED:\n' + json.dumps(event))
responseData = {}
try:
res = client.create_application(
applicationName=appName,
computePlatform='ECS'
)
logger.info(res)
logger.info("SUCCESS: CodeDeploy Application created.")
res = client.create_deployment_group(
applicationName=appName,
deploymentGroupName=deploymentGroup,
deploymentConfigName='CodeDeployDefault.ECSLinear10PercentEvery1Minutes',
serviceRoleArn=event['ResourceProperties']['CodeDeployServiceRoleArn'],
autoRollbackConfiguration={
'enabled': True,
'events': [
'DEPLOYMENT_FAILURE',
]
},
deploymentStyle={
'deploymentType': 'BLUE_GREEN',
'deploymentOption': 'WITH_TRAFFIC_CONTROL'
},
blueGreenDeploymentConfiguration={
'terminateBlueInstancesOnDeploymentSuccess': {
'action': 'TERMINATE',
'terminationWaitTimeInMinutes': 30
},
'deploymentReadyOption': {
'actionOnTimeout': 'STOP_DEPLOYMENT',
'waitTimeInMinutes': 5
}
},
loadBalancerInfo={
'targetGroupPairInfoList': [
{
'targetGroups': [
{
'name': event['ResourceProperties']['TargetGroup1']
},
{
'name': event['ResourceProperties']['TargetGroup2']
},
],
'prodTrafficRoute': {
'listenerArns': [
event['ResourceProperties']['ALBListener1'],
]
},
'testTrafficRoute': {
'listenerArns': [
event['ResourceProperties']['ALBListener2'],
]
}
},
]
},
ecsServices=[
{
'serviceName': serviceName,
'clusterName': clusterName
},
]
)
except ClientError as e:
logger.error("ERROR: Something error!")
logger.error(e)
responseData = {'error': str(e)}
cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
else:
logger.info(res)
logger.info(
"SUCCESS: CodeDeploy Application and DeploymentGroup created.")
return cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
Handler: index.lambda_handler
Role: !GetAtt LambdaRole.Arn
Runtime: python3.8
Timeout: 10
# ------------------------------------------------------------#
# Custom Resource
# ------------------------------------------------------------#
CreateCodeDeploy:
Type: Custom::CreateCodeDeploy
DependsOn:
- ECSService
Properties:
ServiceToken: !GetAtt LambdaFunction.Arn
Region: !Ref AWS::Region
ECSClusterName: !Sub '${ProjectName}-${ECSClusterName}'
ECSServiceName: !Sub '${ProjectName}-${ECSServiceName}'
CodeDeployServiceRoleArn: !GetAtt CodeDeployRole.Arn
TargetGroup1: !Sub '${ProjectName}-${TargetGroupName1}'
TargetGroup2: !Sub '${ProjectName}-${TargetGroupName2}'
ALBListener1: !Ref ALBListener1
ALBListener2: !Ref ALBListener2
appName: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployAppName}'
deploymentGroup: !Sub '${ProjectName}-${ECSClusterName}-${ECSServiceName}-${CodeDeployDeploymentGroupName}'
# ------------------------------------------------------------#
# IAMRole For CustomResource Lambda
# ------------------------------------------------------------#
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/AWSCodeDeployFullAccess
LambdaPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: LambdaPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:*
- logs:*
Resource: '*'
- Effect: Allow
Resource: '*'
Action:
- iam:PassRole
Condition:
StringEqualsIfExists:
iam:PassedToService:
- codedeploy.amazonaws.com
Roles:
- !Ref LambdaRole
# ------------------------------------------------------------#
# Log Bucket
# ------------------------------------------------------------#
logsBacket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'alb-log-${AWS::AccountId}'
AccessControl: LogDeliveryWrite
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: 'AES256'
PublicAccessBlockConfiguration:
BlockPublicAcls: TRUE
BlockPublicPolicy: TRUE
IgnorePublicAcls: TRUE
RestrictPublicBuckets: TRUE
LifecycleConfiguration:
Rules:
- Id: 'Delete-After-400days'
Status: Enabled
ExpirationInDays: 400
logsBucketPolicy:
Type: AWS::S3::BucketPolicy
DependsOn: logsBacket
Properties:
Bucket: !Sub 'alb-log-${AWS::AccountId}'
PolicyDocument:
Statement:
- Action:
- 's3:PutObject'
Effect: 'Allow'
Resource:
- Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Sub 'alb-log-${AWS::AccountId}'
- '/*'
Principal:
AWS: '582318560864'
- Action:
- 's3:PutObject'
Effect: 'Allow'
Resource:
- Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Sub 'alb-log-${AWS::AccountId}'
- '/*'
Principal:
Service: 'delivery.logs.amazonaws.com'
Condition:
StringEquals:
's3:x-amz-acl':
- 'bucket-owner-full-control'
- Action:
- 's3:GetBucketAcl'
Effect: 'Allow'
Resource:
Fn::Join:
- ''
- - 'arn:aws:s3:::'
- !Sub 'alb-log-${AWS::AccountId}'
Principal:
Service: 'delivery.logs.amazonaws.com'
CodePipeline 構築を合わせてどうぞ
[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築する では、CodeCommit を利用した プレースホルダを利用した ECS Blue/Green の CodePipeline を CloudFormation で構築しています。
(Fargateを構築するパラメータと多少異なるので調整が必要です。)
CodeCommit に必要な Dockerfile(+その他) , appspec.yaml , taskdef.jsonを格納して実行します。
手動承認ステージはコメントアウトしていますので、利用する場合は必要な設定を確認の上、ご利用下さい。
codepipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
NameTagPrefix:
Type: String
Default: test
Description: Prefix of Name tags.
ServiceName:
Type: String
Default: myapp
Description: Prefix of Service tags.
CodeCommitRepositoryName:
Type: String
CodeDeployAppName:
Type: String
CodeDeployDGName:
Type: String
ContainerName:
Type: String
ECRName:
Type: String
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
# IAM Roles
# ------------------------------------------------------------#
# CodeWatchEventを実行できるIAMRole
CloudwatchEventRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CloudWatchEventRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: CloudWatchEventsPipelineExecution
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: codepipeline:StartPipelineExecution
Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
# CodeBuildに適用するIAMRole
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodeBuildServiceNameRole
Path: /
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SampleCodeBuildAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Resource: '*'
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Effect: Allow
Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketAcl
- s3:GetBucketLocation
- Effect: Allow
Action:
- codebuild:CreateReportGroup
- codebuild:CreateReport
- codebuild:UpdateReport
- codebuild:BatchPutTestCases
- codebuild:BatchPutCodeCoverages
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: '*'
# CodePipelineに適用するIAMRole
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodePipelineServiceRole
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SamplePipeline
PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- iam:PassRole
Resource: '*'
Effect: Allow
Condition:
StringEqualsIfExists:
iam:PassedToService:
- ecs-tasks.amazonaws.com
- Resource:
- !Sub arn:aws:s3:::${ArtifactBucket}/*
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
- s3:GetBucketVersioning
- Action:
- codecommit:CancelUploadArchive
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:GetRepository
- codecommit:GetUploadArchiveStatus
- codecommit:UploadArchive
Resource: '*'
Effect: Allow
- Action:
- codedeploy:CreateDeployment
- codedeploy:GetApplication
- codedeploy:GetApplicationRevision
- codedeploy:GetDeployment
- codedeploy:GetDeploymentConfig
- codedeploy:RegisterApplicationRevision
- codedeploy:*
Resource: '*'
Effect: Allow
- Action:
- elasticbeanstalk:*
- ec2:*
- elasticloadbalancing:*
- autoscaling:*
- cloudwatch:*
- sns:*
- cloudformation:*
- rds:*
- sqs:*
- ecs:*
Resource: '*'
Effect: Allow
- Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
- codebuild:BatchGetBuildBatches
- codebuild:StartBuildBatch
Resource: '*'
Effect: Allow
# S3Bucket
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
# CloudWatchEventの実行ルール
AmazonCloudWatchEventRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- Fn::Join:
- ''
- - 'arn:aws:codecommit:'
- !Ref 'AWS::Region'
- ':'
- !Ref 'AWS::AccountId'
- ':'
- !Ref CodeCommitRepositoryName
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- master
Targets:
- Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
RoleArn: !GetAtt CloudwatchEventRole.Arn
Id: codepipeline-AppPipeline
# CodeBuild
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
ServiceRole: !Ref CodeBuildServiceRole
Artifacts:
Type: CODEPIPELINE
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR...
- $(aws ecr get-login --no-include-email)
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
- docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing imageDetail json...
- echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json
artifacts:
files: imageDetail.json
Environment:
PrivilegedMode: true
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:4.0
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: REPOSITORY_URI
Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
- Name: ContainerName
Value: !Ref ContainerName
- Name: DOCKER_BUILDKIT
Value: '1'
Name: !Ref AWS::StackName
# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------#
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt CodePipelineServiceRole.Arn
Name: !Sub ${NameTagPrefix}-${ServiceName}-pipeline
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucket
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: AWS
Version: '1'
Provider: CodeCommit
Configuration:
RepositoryName: !Ref CodeCommitRepositoryName
PollForSourceChanges: false
BranchName: master
RunOrder: 1
OutputArtifacts:
- Name: App
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Version: '1'
Provider: CodeBuild
Configuration:
ProjectName: !Ref CodeBuildProject
RunOrder: 1
InputArtifacts:
- Name: App
OutputArtifacts:
- Name: BuildOutput
# - Name: Approval
# Actions:
# - Name: Manual_Approval
# ActionTypeId:
# Category: Approval
# Owner: AWS
# Version: '1'
# Provider: Manual
# Configuration:
# CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?'
# NotificationArn: arn:aws:sns:ap-northeast-1:xxxxxxxx:hogehoge
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Version: '1'
Provider: CodeDeployToECS
Configuration:
AppSpecTemplateArtifact: App
AppSpecTemplatePath: appspec.yaml
TaskDefinitionTemplateArtifact: App
TaskDefinitionTemplatePath: taskdef.json
ApplicationName: !Ref CodeDeployAppName
DeploymentGroupName: !Ref CodeDeployDGName
Image1ArtifactName: BuildOutput
Image1ContainerName: IMAGE1_NAME
RunOrder: 1
InputArtifacts:
- Name: App
- Name: BuildOutput
Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
PipelinelogicalID:
Description: logical ID.
Value: !Ref Pipeline
最後に
前回カスタムリソースを使ったのは、今は AWS のアップデートによって不要となったCloudFormation一撃でAWS Transfer for SFTP のパブリックアクセスを特定IP に限定する | DevelopersIO でした。
このテンプレートもそのうち無用になるかと思っています。
とはいえ、個人的には機会があればカスタムリソースを使ってみたいと思います。
以上です。
どなたかのどなたかのお役に立てば幸いです。