CloudFormationでECSのBlue/Greenデプロイができるようになったので試す
概要
これまで CodeDeploy と連携させて実現していた ECS の Blue/Green デプロイが CloudFormation にインテグレートされて、External
なデプロイメントコントローラーとして使用できるようになりました。これによって、一連のデプロイパイプラインをテンプレート上に集約して構築することができます。
プレスリリース
aws.amazon.com
ドキュメント
サンプルのテンプレートで試す
Fargate で Nginx コンテナを動かして、ALB経由で80番ポートでアクセスするアプリを作成するようです。
AWSTemplateFormatVersion: "2010-09-09" 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
VPC とサブネットをパラメータとして指定します。
作るのは面倒なのでデフォルトVPCのものを使います。
また、テンプレートには IAM リソースが含まれるので CAPABILITY_IAM
を指定する必要があります。さらにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPAND
の指定も必要です。
aws cloudformation create-stack \ --stack-name ecs-blue-green-sample \ --template-body file://./sample.cf.yml \ --parameters ParameterKey=Vpc,ParameterValue=vpc-e6023881 \ ParameterKey=Subnet1,ParameterValue=subnet-34955b1f \ ParameterKey=Subnet2,ParameterValue=subnet-8d8fb9d6 \ --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND
スタックが作成されたら、ALBのDNS名を調べてアクセスしてみます。
200が返ってくることを確認します。
curl -I $(aws elbv2 describe-load-balancers --load-balancer-arns $(aws cloudformation describe-stack-resource \ --stack-name ecs-blue-green-sample \ --logical-resource-id ExampleALB | jq -r ".StackResourceDetail.PhysicalResourceId") | jq -r ".LoadBalancers[0].DNSName" ) HTTP/1.1 200 OK Date: Sat, 23 May 2020 07:28:44 GMT Content-Type: text/html Connection: keep-alive Server: nginx/1.13.8 Expires: Sat, 23 May 2020 07:28:43 GMT Cache-Control: no-cache
ブラウザ上ではこう見えます。
テンプレートの説明は後にして、ここから実際に新しいバージョンをデプロイしてみます。
ざっくり言うと TaskDefinition に変更が加えられたらそれがフックされてデプロイが走るので、イメージのタグをlatest
からplain-text
に変更します。
Image: 'nginxdemos/hello:plain-text'
aws cloudformation update-stack \ --stack-name ecs-blue-green-sample \ --template-body file://./sample.cf.yml \ --parameters ParameterKey=Vpc,ParameterValue=vpc-e6023881 \ ParameterKey=Subnet1,ParameterValue=subnet-34955b1f \ ParameterKey=Subnet2,ParameterValue=subnet-8d8fb9d6 \ --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND
実行すると、CloudFormation のマクロが変更セットを作成して、
CodeDeploy や GreenTaskDefinition
などを作り始めます。
CodeDeploy を見て、トラフィックが新しい環境に流れ始めたことを確認します。
再びブラウザからアクセスすると、画像のようにplain-text
イメージが使われていることが確認できます。
基本的な動きはこんな感じです。
マクロとフックの仕組み
テンプレートにはTransform
とHooks
のブロックがあります。
Transform
はテンプレートに対して適用できるマクロです。
通常の宣言的な記述とは違って、このマクロでテンプレートを書き換えたりします。
Hooks
はそのマクロをいつどのように適用するかを定義するブロックです。 今回の ECS の Blue/Green デプロイの場合、TaskDefinition
またはTaskSet
に対する変更がフックされます。
ECSAttributes: TaskDefinitions: - BlueTaskDefinition - GreenTaskDefinition TaskSets: - BlueTaskSet - GreenTaskSet
最初にここで気になったのがGreenTaskDefinition
という論理IDです。
これはテンプレートには存在しませんが、マクロで作るリソースの論理IDをあらかじめ指定するという感じで動作しているみたいです。 名前を Red/Black に変えても動きました。
存在しない論理IDを参照してるようでちょっと違和感ある🤔
さっきは直接スタックの更新を走らせましたが、今度は変更セットを作ってマクロがどのような動作をするのかを見てみます。
以上のように、マクロは古いタスクを削除して、新しいGreen環境のリソースを作成するようなテンプレートを生成します。
これが裏側で起こっていることです。
Green環境が安定するまでにエラーが発生した場合は、通常どおりロールバックが行われ、Blue環境が維持されます。
ルーティング
サンプルではトラフィックが一気に切り替わります。
これはTrafficRoutingConfig: Type: AllAtOnce
によるものです。
次は1分おきに20%ずつ新しい環境へ流していく戦略を取ります。
TrafficRoutingConfig: Type: TimeBasedLinear TimeBasedLinear: StepPercentage: 20 BakeTimeMins: 1
リロードするたびに変わるのが見て取れると思います。
20%はplain-text
に流れる状態です。
他にTimeBasedCanary
という指定もできます。
これは Linear とは違って、段階的でも2段階で切り替えを行う設定のようです。
次に、テストトラフィックの指定に関してです。
時間ベースで徐々にトラフィックを切り替えるとはいえ、明示的にGreen環境の確認をしたいこともあると思います。
Optional でTestTrafficRoute
を指定することで実現できます。
8080番ポートで受けるテスト用リスナーを作成します。
ALBListenerTestTraffic: Type: 'AWS::ElasticLoadBalancingV2::Listener' Properties: DefaultActions: - Type: forward ForwardConfig: TargetGroups: - TargetGroupArn: !Ref ALBTargetGroupGreen Weight: 1 LoadBalancerArn: !Ref ExampleALB Port: 8080 Protocol: HTTP
セキュリティグループに追加するのを忘れないでください。
サンプルだとすでに8080への許可がついてますが。
なぜ使ってないのに許可してるのかわからなかったがこのためか。
サンプル分かりにくいよ😂
ルーティングの設定を追加します。
TrafficRouting: ProdTrafficRoute: Type: 'AWS::ElasticLoadBalancingV2::Listener' LogicalID: ALBListenerProdTraffic TestTrafficRoute: Type: 'AWS::ElasticLoadBalancingV2::Listener' LogicalID: ALBListenerTestTraffic
これで、トラフィックの切り替え中は、:8080
にアクセスすると必ずGreen環境にフォワードされます。
その他の制約
- デプロイ以外の変更を含んだ更新はできない
例えば、イメージの変更に加えて、ALBに新しくタグをつけたテンプレートを実行しようとすると以下のエラーが発生します。
'CodeDeployBlueGreenHook' of type AWS::CodeDeploy::BlueGreen failed with message: Additional resource diff other than ECS application related resource update is detected,CodeDeploy can't perform BlueGreen style update properly. Diff resource logical Ids: [ExampleALB]
デプロイと、それ以外の変更は分けて適用する必要があります。
NoEcho
なパラメータや、SSMなど外部サービスから動的に参照するパラメータは利用できないOutputs
,ImportValue
は利用できないネストしたスタックを含むテンプレートでは利用できない
ネストされたスタックでは利用できない
既存リソースのImportでは利用できない
所感
正直、まだ導入するには尚早かなと思う。
サポートされてないものもあるし、CodeDeployを自分で構築した方が取り回しやすいかな。
運用保守がBlue/Greenデプロイを中心に考えないといけないので、柔軟性は損なわれる。
あとマクロってちょっと怖くないですか。
宣言的記述ってどこまで言っていいのかわからない。
Terraformだったらforループ使うことはよくあると思うけど、このマクロはブラックボックスすぎて変更セット見るまでdiff分からんし中身もわからん。