背景
前回の投稿にも書きましたが、2020/5/19にCloudFormationでCodeDeployのECS/FargateにおけるBlue/Greenデプロイがサポートされたらしいので、ちょっと調べてみました。
https://qiita.com/yusuke-ka/items/593eba9a303bb4878506
https://aws.amazon.com/jp/about-aws/whats-new/2020/05/aws-cloudformation-now-supports-blue-green-deployments-for-amazon-ecs/
公式のユーザーガイド
まずは公式のユーザーガイドを読んでみた。(現時点では英語しかない。。)
気になったのはココ。
「In order to perform ECS blue/green deployment using CodeDeploy through CloudFormation, your template needs to include the resources that model your deployment, such as an Amazon ECS service and load balancer.」
翻訳すると、
「CloudFormationを介して、CodeDeployを使用してECSブルー/グリーンデプロイを実行するには、Amazon ECSサービスやロードバランサーなど、デプロイをモデル化するリソースをテンプレートに含める必要があります。」
つまり、ECSサービスやALB等は、事前に作成しておいたり、他のスタックで作成したものを使うことはできず、同じテンプレート内で定義する必要があるということでしょうか。
テンプレートのサンプルを見てみても、各リソースを指定する部分は、Arnを指定する感じではなく、直接リソース定義名を指定しているように見える。
Parameters:
...
Transform:
- 'AWS::CodeDeployBlueGreen'
Hooks:
CodeDeployBlueGreenHook:
Properties:
TrafficRoutingConfig:
Type: AllAtOnce
Applications:
- Target:
Type: 'AWS::ECS::Service'
LogicalID: ECSDemoService
ECSAttributes:
TaskDefinitions:
- BlueTaskDefinition
- GreenTaskDefinition
TaskSets:
- BlueTaskSet
- GreenTaskSet
TrafficRouting:
ProdTrafficRoute:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
LogicalID: ALBListenerProdTraffic
TargetGroups:
- ALBTargetGroupBlue # 直接リソースの定義名が指定されている
- ALBTargetGroupGreen
Type: 'AWS::CodeDeploy::BlueGreen'
Resources:
...
ALBTargetGroupBlue:
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
...
...
...
公式ユーザーガイドの説明においても、それぞれ「Resource Logical ID」を指定しろと書いてあって、「Logical ID」の説明には「英数字(A-Za-z0-9)で、テンプレート内で一意である必要があります。」とあることからも、テンプレート内に定義しなければならないことは確定っぽい。
前回の記事のやつは、ネットワーク系、ECS系、CI/CD系のような区分でスタックを分けていたので、これを利用するなら、構成を見直す必要がありそう。
前回作成したテンプレートをちょこっと書き換えるだけで済むかと思っていたが、結構大変そう。
戦略としては、前回のを書き換えるより、公式のサンプルをベースにECS/FargateのBlue/Greenデプロイのテンプレートを新たに作成して、そこにPipelineやらCodeBuildを組み込んでいくほうがよさげ。
あと、前回はバックエンドのサービスとフロントエンドのサービスを同じテンプレートに含めたが、同じテンプレート内に定義しないといけないリソースが増えるので、今回はそれぞれ別のテンプレートにしたほうが良いかもしれない。(横割りから縦割りへ)
ただ、縦割りにするとなると、もともとALBやクラスターはフロントエンドとバックエンドで共有していたが、公式のユーザーガイドによると、少なくともALBはRequiredになっていて同じテンプレートに含める必要がありそうなので、ALBの作成を別のスタックに分離して、フロントエンドとバックエンドで共有するのは無理かもしれない。
AWS公式のサンプルを試してみる
とりあえず、AWSのサンプルを試してみる。
公式のサンプルを以下のファイルに書き出してみた。
Parameters:
Vpc:
Type: "AWS::EC2::VPC::Id"
Subnet1:
Type: "AWS::EC2::Subnet::Id"
Subnet2:
Type: "AWS::EC2::Subnet::Id"
Transform:
- "AWS::CodeDeployBlueGreen"
Hooks:
CodeDeployBlueGreenHook:
Properties:
TrafficRoutingConfig:
Type: AllAtOnce
Applications:
- Target:
Type: "AWS::ECS::Service"
LogicalID: ECSDemoService
ECSAttributes:
TaskDefinitions:
- BlueTaskDefinition
- GreenTaskDefinition
TaskSets:
- BlueTaskSet
- GreenTaskSet
TrafficRouting:
ProdTrafficRoute:
Type: "AWS::ElasticLoadBalancingV2::Listener"
LogicalID: ALBListenerProdTraffic
TargetGroups:
- ALBTargetGroupBlue
- ALBTargetGroupGreen
Type: "AWS::CodeDeploy::BlueGreen"
Resources:
ExampleSecurityGroup:
Type: "AWS::EC2::SecurityGroup"
Properties:
GroupDescription: Security group for ec2 access
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0
ALBTargetGroupBlue:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
HealthCheckIntervalSeconds: 5
HealthCheckPath: /
HealthCheckPort: "80"
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 2
HealthyThresholdCount: 2
Matcher:
HttpCode: "200"
Port: 80
Protocol: HTTP
Tags:
- Key: Group
Value: Example
TargetType: ip
UnhealthyThresholdCount: 4
VpcId: !Ref Vpc
ALBTargetGroupGreen:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
HealthCheckIntervalSeconds: 5
HealthCheckPath: /
HealthCheckPort: "80"
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 2
HealthyThresholdCount: 2
Matcher:
HttpCode: "200"
Port: 80
Protocol: HTTP
Tags:
- Key: Group
Value: Example
TargetType: ip
UnhealthyThresholdCount: 4
VpcId: !Ref Vpc
ExampleALB:
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
Properties:
Scheme: internet-facing
SecurityGroups:
- !Ref ExampleSecurityGroup
Subnets:
- !Ref Subnet1
- !Ref Subnet2
Tags:
- Key: Group
Value: Example
Type: application
IpAddressType: ipv4
ALBListenerProdTraffic:
Type: "AWS::ElasticLoadBalancingV2::Listener"
Properties:
DefaultActions:
- Type: forward
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref ALBTargetGroupBlue
Weight: 1
LoadBalancerArn: !Ref ExampleALB
Port: 80
Protocol: HTTP
ALBListenerProdRule:
Type: "AWS::ElasticLoadBalancingV2::ListenerRule"
Properties:
Actions:
- Type: forward
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref ALBTargetGroupBlue
Weight: 1
Conditions:
- Field: http-header
HttpHeaderConfig:
HttpHeaderName: User-Agent
Values:
- Mozilla
ListenerArn: !Ref ALBListenerProdTraffic
Priority: 1
ECSTaskExecutionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Sid: ""
Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
BlueTaskDefinition:
Type: "AWS::ECS::TaskDefinition"
Properties:
ExecutionRoleArn: !Ref ECSTaskExecutionRole
ContainerDefinitions:
- Name: DemoApp
Image: "nginxdemos/hello:latest"
Essential: true
PortMappings:
- HostPort: 80
Protocol: tcp
ContainerPort: 80
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
Cpu: "256"
Memory: "512"
Family: ecs-demo
ECSDemoCluster:
Type: "AWS::ECS::Cluster"
Properties: {}
ECSDemoService:
Type: "AWS::ECS::Service"
Properties:
Cluster: !Ref ECSDemoCluster
DesiredCount: 1
DeploymentController:
Type: EXTERNAL
BlueTaskSet:
Type: "AWS::ECS::TaskSet"
Properties:
Cluster: !Ref ECSDemoCluster
LaunchType: FARGATE
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: ENABLED
SecurityGroups:
- !Ref ExampleSecurityGroup
Subnets:
- !Ref Subnet1
- !Ref Subnet2
PlatformVersion: 1.3.0
Scale:
Unit: PERCENT
Value: 1
Service: !Ref ECSDemoService
TaskDefinition: !Ref BlueTaskDefinition
LoadBalancers:
- ContainerName: DemoApp
ContainerPort: 80
TargetGroupArn: !Ref ALBTargetGroupBlue
PrimaryTaskSet:
Type: "AWS::ECS::PrimaryTaskSet"
Properties:
Cluster: !Ref ECSDemoCluster
Service: !Ref ECSDemoService
TaskSetId: !GetAtt
- BlueTaskSet
- Id
マネジメントコンソールで上記ファイルを読み込んで、スタックを作成。
Subnet1、Subnet2、Vpcのパラメータはもともと自分の環境にあるやつを選択。
そのまま作成しようとすると、「Requires capabilities : [CAPABILITY_IAM]」とか「Requires capabilities : [CAPABILITY_AUTO_EXPAND]」のエラーがでて作成できなかったため、以下にチェックを入れて再実行。
- AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。
- AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。
- AWS CloudFormation によって、次の機能が要求される場合があることを承認します: CAPABILITY_AUTO_EXPAND
今度は、問題なくスタック作成が完了。
ただ、ここで想定外の現象が起こっていた。
CodeDeployのところに、アプリケーションやデプロイグループが作成されていなかった。
失敗したのかと思ったが、どうやら違うらしい。
よくよく読んでみると、
「次のECSリソースの交換が必要なプロパティを更新するスタック更新を実行すると、CloudFormationはグリーンデプロイを開始します。
AWS :: ECS :: TaskDefinition
AWS :: ECS :: TaskSet
」
と書いてある。
つまり、update-stackしたタイミングでCodeDeployのBlue/Greenデプロイがトリガーされる模様。
テンプレートのタスク定義を変更してみる(イメージ名を変更)。
...
Resources:
...
BlueTaskDefinition:
...
Properties:
...
ContainerDefinitions:
- Name: DemoApp
Image: "nginxdemos/hello:plain-text"
...
...
...
マネジメントコンソールで上記ファイルを読み込んで、スタックを更新。
CodeDeployのアプリケーションは空のままだったが、デプロイ履歴を見ると、デプロイが始まっていた。
詳細はこんな感じ。
ブラウザでALBにアクセスしてみると、テキストベースのnginxのデモページが表示された。
再度イメージ名をnginxdemos/hello:latestに戻し、スタックを更新して確認。
問題なくBlue/Greenデプロイが始まり、今度はHTMLのページが表示された。
さいごに
公式のサンプルでECS/FargateのBlue/Greenデプロイを試してみた。
元々は、これをベースにPipelineやらCodeBuildを組み込んで、コミットからの自動デプロイを実現しようかと思っていたが、実際には想像していたものとは違っていた。
今回のものは、ECS/FargateにおけるBlue/Greenデプロイ用のCodeDeploy(デプロイグループ)を作成するためのCloudFormationの仕様が追加になった訳ではなく、CloudFormationのスタック更新でタスク定義を変更することで、自動的にCodeDeployのBlue/Greenデプロイが実行される感じだった。
実際、下記のサイトにある「AWS CloudFormation supports blue/green deployments on the AWS Lambda compute platform only.」の文言は消えていない。
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-loadbalancerinfo.html
CodePipelineのデプロイステージでデプロイプロバイダーをAWS CloudFormationしてスタックを更新してやるような感じにすれば、全部CloudFormationに置き換えできそうな気もするけど、元のままのほうがシンプルなので、上の文言が消えるまで様子見かな。