この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんにちは、虎塚です。
最近の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は実現できます! しかも、DNSを使ったBlue-Green切り替えと違って、ダウンタイムをゼロにできるのです。今回は、その手順をご提案します。
概要
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点がポイントですね。これさえ守れば、サービスの提供を中断することなく、BlueとGreenを切り替えてリリースすることができます!
おわりに
CloudFormationでもBlue-Green Deploymentを実現する方法をご説明しました。スタックアップデートが2回必要なのは面倒ですが、サービスの継続提供ができるのは大きなメリットではないでしょうか。
この方法では、ステージング環境のほうで一時的なアクセス断が避けられないので、ELBのattach、detachのほうがきっと使い勝手がよい場面も多いでしょう。しかし、こうした使い方ができるのもAWSの面白いところだと思います。
それでは、また。