Auto Scaling環境のBlue-Green DeploymentをCloudFormationのスタックアップデートで実現する

AWS 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環境への経路を削除します。

1回目のスタックアップデートによる変更

この変更には、Green側のAuto Scaling Groupの再生成が伴います。

次に、ELB-BからBlue環境への経路を追加します。同時に、ELB-AからBlue環境への経路を削除します。

2回目のスタックアップデートによる変更

この変更には、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です。

1回目のアップデートの時のBlue側ELB

ちなみに、スタックアップデート完了後のCloudFormationダッシュボードでイベントを確認すると、次のようになります。Green側のAuto Scaling Groupだけが変更されています。

Green側だけ更新された

最後に、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の面白いところだと思います。

それでは、また。