Auto Scaling環境のBlue-Green DeploymentをCloudFormationのスタックアップデートで実現する
はじめに
こんにちは、虎塚です。
最近のAWSアップデートで、ELBをAuto Scaling Groupにattachしたり、Auto Scaling Groupからdetachしたりできるようになりました。
CloudFormation職人の皆さんはご存じだと思いますが、この機能は残念ながらCloudFormationで利用できません。現時点では、Management ConsoleやAWS APIからだけ利用できます。
じつは、Auto ScalingからEC2をattach、detachする機能も同様です。CloudFormation テンプレートの表現力では、「Auto Scaling Groupから何かを付け外しする」ことを記述するのは、難しいのかもしれません。
しかし、あきらめるのは早いです。手順をちょっと工夫すれば、CloudFormationとAuto Scalingを併用したBlue-Green Deploymentは実現できます! 今回は、その手順をご提案します。
概要
Auto Scaling Groupには、以前からELBを紐づけることができました。CloudFormationでは、AWS::AutoScaling::AutoScalingGroupのLoadBalancerNames属性で実現します。
"FooAutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "AvailabilityZones": [ "ap-northeast-1a" ], [...] "LaunchConfigurationName": { "Ref": "FooLaunchConfiguration" }, "LoadBalancerNames" : [ { "Ref" : "FooELB" } ], [...] "VPCZoneIdentifier" : [ "subnet-99999999" ] } },
この記述を使うと、次のようなことができます。
- 複数のELBを1つのAuto Scaling Groupにつける
- 1つのELBを複数のAuto Scaling Groupにつける
- CloudFormationのスタックアップデートを使って、ELBを付け替える
つまり、Auto Scaling GroupのLoadBalancerNamesを変更すると,Auto Scaling Group自体が新規に作り直されます。Auto Scaling Groupが削除されると、配下で稼働しているEC2インスタンスは一緒に削除されてしまいます。
まとめると、ELBの付け外しはCloudFormationのスタックアップデートで以前から可能でしたが、Auto Scaling Groupの作り直しを必要とする変更なので、インスタンスの破棄と再生成を伴うという制約がありました。
この制約を逆手にとって、スタックのアップデートを2回に分けることで実現するのが、CloudFormationとAuto Scalingを使ったBlue-Green Deploymentです。
スタックアップデートによるBlue-Green Deployment
前提として、1つのCloudFormationで、次のようにBlue環境とGreen環境を作成しているものとします。
上の図で、いまはBlue環境が本番環境で、Green環境がステージング環境だとします。本番環境にアクセス可能な状態を維持したまま、Green環境を新しい本番環境に、Blue環境をステージング環境になるように切り替えるのがゴールです。
まず、ELB-AからGreen環境への経路を追加します。同時に、ELB-BからGreen環境への経路を削除します。
この変更には、Green側のAuto Scaling Groupの再生成が伴います。
次に、ELB-BからBlue環境への経路を追加します。同時に、ELB-AからBlue環境への経路を削除します。
この変更には、Blue側のAuto Scaling Groupの再生成が伴います。
これだけです! 本番環境へのアクセスを維持したまま、BlueとGreenを切り替えることができました。
実践
まず、前提となるBlue環境とGreen環境を作成します。今回は、各環境で1つのAvailability Zoneにインスタンスを1つだけ立ち上げることにします。
最初にスタックを作成するテンプレートは、次のとおりです。
{ "Description" : "a Blue and a Green Environments", "Parameters" : { "BasedAmi" : { "Description" : "AMI ID to use", "Type" : "String" } }, "Resources" : { "BlueELB" : { "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", "Properties" : { "Listeners" : [ { "LoadBalancerPort" : "80", "InstancePort" : "80", "Protocol" : "HTTP" }], "HealthCheck" : { "Target" : "TCP:80", "HealthyThreshold" : "2", "UnhealthyThreshold" : "5", "Interval" : "30", "Timeout" : "5" }, "SecurityGroups" : [ "sg-00000000", "sg-11111111" ], "Subnets" : [ "subnet-99999999" ] } }, "GreenELB" : { "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", "Properties" : { "Listeners" : [ { "LoadBalancerPort" : "80", "InstancePort" : "80", "Protocol" : "HTTP" }], "HealthCheck" : { "Target" : "TCP:80", "HealthyThreshold" : "2", "UnhealthyThreshold" : "5", "Interval" : "30", "Timeout" : "5" }, "SecurityGroups" : [ "sg-00000000", "sg-11111111" ], "Subnets" : [ "subnet-99999999" ] } }, "WebServerLaunchConfiguration" : { "Type": "AWS::AutoScaling::LaunchConfiguration", "Properties": { "AssociatePublicIpAddress" : "true", "IamInstanceProfile" : "insntance-role", "ImageId" : { "Ref" : "BasedAmi" }, "InstanceType" : "t2.micro", "SecurityGroups" : [ "sg-00000000", "sg-22222222" ], "KeyName" : "your-key-name" } }, "BlueAutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "AvailabilityZones": [ "ap-northeast-1a" ], "HealthCheckGracePeriod" : "300", "HealthCheckType" : "ELB", "LaunchConfigurationName": { "Ref": "WebServerLaunchConfiguration" }, "LoadBalancerNames" : [ { "Ref" : "BlueELB" } ], "MinSize": "1", "MaxSize": "1", "Tags" : [{ "Key" : "Name", "Value" : { "Fn::Join" : [ "-" , [ { "Ref" : "AWS::StackName" }, "blue" ]]}, "PropagateAtLaunch" : "true" }], "VPCZoneIdentifier" : [ "subnet-99999999" ] } }, "GreenAutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { "AvailabilityZones": [ "ap-northeast-1a" ], "HealthCheckGracePeriod" : "300", "HealthCheckType" : "ELB", "LaunchConfigurationName": { "Ref": "WebServerLaunchConfiguration" }, "LoadBalancerNames" : [ { "Ref" : "GreenELB" } ], "MinSize": "1", "MaxSize": "1", "Tags" : [{ "Key" : "Name", "Value" : { "Fn::Join" : [ "-" , [ { "Ref" : "AWS::StackName" }, "green" ]]}, "PropagateAtLaunch" : "true" }], "VPCZoneIdentifier" : [ "subnet-99999999" ] } } }, "Outputs": { "BlueELB" : { "Description" : "DNS Name of ELB in Blue Environment", "Value" : { "Fn::GetAtt" : [ "BlueELB", "DNSName" ] } }, "GreenELB" : { "Description" : "DNS Name of ELB in Green Environment", "Value" : { "Fn::GetAtt" : [ "GreenELB", "DNSName" ] } } } }
次に、1回目のスタックアップデートをおこない、ELB-AからGreen環境への経路を追加します。同時に、ELB-BからGreen環境への経路を削除します。最初のテンプレートからの差分は、次の部分だけです。
"GreenAutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { [...] "LoadBalancerNames" : [ { "Ref" : "BlueELB" } ], [...] } }
上記では、Green側のAuto Scaling Groupの属性を変更して、ELB-A(Blue側ELB)を追加し、ELB-B(Green側ELB)を削除しています。
インスタンスが再起動されるのはGreen側で、Blue側は変更されません。最初のままのBlue側のインスタンスが、本番環境へのアクセスを受け止めます。
この時のアップデートで、Blue側ELBの状態は次のようになります。配下に2つのインスタンスがあります。片方はBlue環境、もう片方はGreen環境のEC2です。
ちなみに、スタックアップデート完了後のCloudFormationダッシュボードでイベントを確認すると、次のようになります。Green側のAuto Scaling Groupだけが変更されています。
最後に、2回目のスタックアップデートをおこない、ELB-BからBlue環境への経路を追加します。同時に、ELB-AからBlue環境への経路を削除します。最初のテンプレートからの差分は、次の部分だけです。
"BlueAutoScalingGroup": { "Type": "AWS::AutoScaling::AutoScalingGroup", "Properties": { [...] "LoadBalancerNames" : [ { "Ref" : "GreenELB" } ], [...] } },
上記では、Green側のAuto Scaling Groupの属性を変更して、ELB-B(Green側ELB)を追加し、ELB-A(Blue側ELB)を削除しています。
インスタンスが再起動されるのはBlue側で、Green側は変更されません。ELB-Aからくる本番環境へのアクセスは、Green側のインスタンスが受け止めてくれます。
以上で完了です。
- スタックアップデートする時に、片方のAuto Scaling Groupが変更されないようにすること
- 変更されない側のAuto Scaling Groupに、本番アクセスを割り振ること
この2点がポイントですね。
おわりに
2回のスタックアップデートで、CloudFormationでもBlue-Green Deploymentを実現する方法をご説明しました。
この方法では、ステージング環境のほうで一時的なアクセス断が避けられないので、ELBのattach、detachのほうがきっと使い勝手がよいでしょう。しかし、こうした使い方ができるのもAWSの面白いところだと思います。
それでは、また。