Elastic Beanstalkのデプロイポリシー:Rolling with an additional batch

Elastic Beanstalkのデプロイポリシー:Rolling with an additional batch

Elastic Beanstalkのデプロイポリシー:Rolling with an additional batch

Elastic Beanstalkでは、さまざまなデプロイポリシーが用意されています。

https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.deploy-existing-version.html

今回はRolling with an additional batch (追加バッチによるローリング)の挙動を検証します。

Rolling with additional batch (追加バッチによるローリング) – 可用性の低下を回避します。ただし、[Rolling (ローリング)] 方法よりもデプロイ時間が長くなります。デプロイ全体で同じ帯域幅を保持する必要がある場合に適しています。この方法では、Elastic Beanstalk はインスタンスの追加バッチを起動し、ローリングデプロイを実行します。追加バッチの起動に時間をかけることで、デプロイ全体で同じ帯域幅が確実に保持されます。

デプロイポリシーの選択

構築する環境

Diagram of Elastic Beanstalk deployment policy: Rolling with an additional batch.

基本的な構成は以下のページと同様です。

あわせて読みたい
CloudFormationを使用して、Elastic BeanstalkのALB環境を構築 以下のページで、Elastic Beanstalkの最小構成をご紹介しました。 https://awstut.com/2023/10/01/introduction-to-elastic-beanstalk-with-cloudformation 今回は上記...

具体的には、ALBおよびAuto Scalingグループで構成されたWebサーバー環境を作成します。
プラットフォームはPython3.8を選択します。

上記のページと異なる点は、Elastic Beanstalkにデプロイポリシー:Rolling with an additional batchを設定していることです。
この構成においては、Auto Scalingグループ内のインスタンスサイズは2です。
デプロイポリシーのバッチサイズタイプをFixed、バッチサイズを1とすることで、デプロイ中は一時的にインスタンス数が3となります。
デプロイ完了後は、再びインスタンス数が2に戻ります。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-dva/tree/main/01/003

テンプレートファイルのポイント解説

本ページは、Elastic Beanstalkのデプロイポリシー(Rolling with an additional batch)の挙動を確認することを目的としています。

Elastic BeanstalkでALB環境を構築する方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationを使用して、Elastic BeanstalkのALB環境を構築 以下のページで、Elastic Beanstalkの最小構成をご紹介しました。 https://awstut.com/2023/10/01/introduction-to-elastic-beanstalk-with-cloudformation 今回は上記...

Elastic Beanstalk

Resources:
  Application:
    Type: AWS::ElasticBeanstalk::Application
    Properties:
      ApplicationName: !Sub "${Prefix}-application"

  ApplicationVersion1:
    Type: AWS::ElasticBeanstalk::ApplicationVersion
    Properties:
      ApplicationName: !Ref Application
      SourceBundle:
        S3Bucket: !Ref BucketName
        S3Key: !Ref SourceBundleName1

  ApplicationVersion2:
    Type: AWS::ElasticBeanstalk::ApplicationVersion
    Properties:
      ApplicationName: !Ref Application
      SourceBundle:
        S3Bucket: !Ref BucketName
        S3Key: !Ref SourceBundleName2

  Environment:
    Type: AWS::ElasticBeanstalk::Environment
    Properties:
      ApplicationName: !Ref Application
      CNAMEPrefix: !Ref Prefix
      EnvironmentName: !Sub "${Prefix}-env"
      TemplateName: !Ref ConfigurationTemplate
      Tier:
        Name: WebServer
        Type: Standard
      VersionLabel: !Ref ApplicationVersion1

  ConfigurationTemplate:
    Type: AWS::ElasticBeanstalk::ConfigurationTemplate
    Properties:
      ApplicationName: !Ref Application
      OptionSettings:
        - Namespace: aws:autoscaling:asg
          OptionName: MaxSize
          Value: !Ref MaxSize
        - Namespace: aws:autoscaling:asg
          OptionName: MinSize
          Value: !Ref MinSize
        - Namespace: aws:autoscaling:launchconfiguration
          OptionName: IamInstanceProfile
          Value: !Ref InstanceProfile
        - Namespace: aws:ec2:instances
          OptionName: InstanceTypes
          Value: !Ref InstanceType
        - Namespace: aws:ec2:instances
          OptionName: SupportedArchitectures
          Value: !Ref InstanceArchitecture
        - Namespace: aws:ec2:vpc
          OptionName: VPCId
          Value: !Ref VPC
        - Namespace: aws:ec2:vpc
          OptionName: Subnets
          Value: !Join
            - ","
            - - !Ref PrivateSubnet1
              - !Ref PrivateSubnet2
        - Namespace: aws:ec2:vpc
          OptionName: ELBSubnets
          Value: !Join
            - ","
            - - !Ref PublicSubnet1
              - !Ref PublicSubnet2
        - Namespace: aws:elasticbeanstalk:command
          OptionName: DeploymentPolicy
          Value: !Ref DeploymentPolicy
        - Namespace: aws:elasticbeanstalk:command
          OptionName: BatchSizeType
          Value: !Ref BatchSizeType
        - Namespace: aws:elasticbeanstalk:command
          OptionName: BatchSize
          Value: !Ref BatchSize
        - Namespace: aws:elasticbeanstalk:environment
          OptionName: EnvironmentType
          Value: !Ref EnvironmentType
        - Namespace: aws:elasticbeanstalk:environment
          OptionName: ServiceRole
          Value: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/aws-elasticbeanstalk-service-role"
        - Namespace: aws:elasticbeanstalk:environment
          OptionName: LoadBalancerType
          Value: !Ref LoadBalancerType
      SolutionStackName: !Ref SolutionStackName

  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref InstanceRole

  InstanceRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier
Code language: YAML (yaml)

ポイントは2つです。

1点目はアプリケーションバージョンを2つ作成する点です。
1つ目(ApplicationVersion1)はElastic Beanstalkの初期構築時に参照します。
2つ目(ApplicationVersion2)はアップデート時に参照することになります。
両者とも、後述のCodeBuildプロジェクトでビルドされたソースバンドルを使用して作成します。

2点目はConfigurationTemplateのデプロイポリシー用のパラメータです。
OptionSettingsプロパティにおける名前空間aws:elasticbeanstalk:commandに関するパラメータが特にポイントです。
DeploymentPolicyはデプロイポリシーを指定するものです。
今回はRolling with an additional batchを選択するために、「RollingWithAdditionalBatch」を指定します。
BatchSizeおよびBatchSizeTypeにそれぞれ「1」そして「Fixed」を指定します。
これで1台ずつデプロイするという挙動となります。

(参考) CodeBuildプロジェクト

Resources:
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: NO_ARTIFACTS
      Cache:
        Type: NO_CACHE
      Environment:
        ComputeType: !Ref ProjectEnvironmentComputeType
        EnvironmentVariables:
          - Name: BUCKET_NAME
            Type: PLAINTEXT
            Value: !Ref BucketName
          - Name: SOURCE_BUNDLE_NAME1
            Type: PLAINTEXT
            Value: !Ref SourceBundleName1
          - Name: SOURCE_BUNDLE_NAME2
            Type: PLAINTEXT
            Value: !Ref SourceBundleName2
          - Name: SOURCE_FILE_NAME
            Type: PLAINTEXT
            Value: !Ref SourceFileName
        Image: !Ref ProjectEnvironmentImage
        ImagePullCredentialsType: CODEBUILD
        Type: !Ref ProjectEnvironmentType
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: DISABLED
        S3Logs:
          Status: DISABLED
      Name: !Ref Prefix
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        Type: NO_SOURCE
        BuildSpec: !Sub |
          version: 0.2

          phases:
            pre_build:
              commands:
                - mkdir app1
                - mkdir app2
                - |
                  cat << EOF > app1/$SOURCE_FILE_NAME
                  import datetime
                  import subprocess

                  def application(environ, start_response):
                      result = subprocess.run(
                        ['ec2-metadata', '-i'],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        encoding='utf-8'
                      )
                      start_response("200 OK", [
                          ("Content-Type", "text/html")
                      ])
                      return [bytes(result.stdout, 'utf-8')]
                  EOF
                - |
                  cat << EOF > app2/$SOURCE_FILE_NAME
                  import datetime
                  import subprocess

                  def application(environ, start_response):
                      result = subprocess.run(
                        ['ec2-metadata', '-i'],
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        encoding='utf-8'
                      )
                      start_response("200 OK", [
                          ("Content-Type", "text/html")
                      ])
                      return [bytes('updated: '+result.stdout, 'utf-8')]
                  EOF
            build:
              commands:
                - zip -j $SOURCE_BUNDLE_NAME1 -r app1/*
                - zip -j $SOURCE_BUNDLE_NAME2 -r app2/*
            post_build:
              commands:
                - aws s3 cp $SOURCE_BUNDLE_NAME1 s3://$BUCKET_NAME/
                - aws s3 cp $SOURCE_BUNDLE_NAME2 s3://$BUCKET_NAME/
      Visibility: PRIVATE
Code language: YAML (yaml)

CodeBuildを使用して、Elastic Beanstalk上で実行させるアプリケーションをビルドし、S3バケットに配置します。

今回はデプロイポリシーの検証ということで、アプリケーション用のソースバンドルを2つ用意します。
1つ目はElastic Beanstalk初期構築時のアプリケーション用です。
2つ目はデプロイポリシーを検証するためのアップデート用です。

両アプリケーションで実行する内容ですが、アプリケーションを実行するEC2インスタンスのIDを返すものです。
アップデート用の方は、インスタンスIDの前に「updated: 」という文字列を追加します。

(参考) CodeBuildプロジェクトでソースバンドルが作成された後にElastic Beanstalkリソースを作成する

Resources:
  WaitConditionHandle:
    Type: AWS::CloudFormation::WaitConditionHandle

  WaitCondition:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !Ref WaitConditionHandle
      Timeout: !Ref WaitConditionTimeout

  EventsRule:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: !Ref EventBusName
      EventPattern:
        source:
          - aws.codebuild
        detail-type:
          - CodeBuild Build State Change
        detail:
          build-status:
            - SUCCEEDED
          project-name:
            - !Ref CodeBuildProject
      Name: !Sub "${Prefix}-EventsRule"
      State: ENABLED
      Targets:
        - Arn: !GetAtt Function2.Arn
          Id: !Ref Function2

  EventsRulePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Function2
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventsRule.Arn

  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          SIGNAL_URL: !Ref WaitConditionHandle
      Code:
        ZipFile: |
          import json
          import os
          import urllib3
          import uuid

          def lambda_handler(event, context):
            body = json.dumps({
                "Status": "SUCCESS",
                "Reason": "CodeBuild Project Finished Successfully",
                "UniqueId": str(uuid.uuid4()),
                "Data": "CodeBuild Project Finished Successfully"
            })
            http = urllib3.PoolManager()
            http.request('PUT', os.environ['SIGNAL_URL'], body=body)
      FunctionName: !Sub "${Prefix}-function-02"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn

  FunctionRole2:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Code language: YAML (yaml)

Elastic Beanstalkリソースの構築が開始されるタイミングを、CloudFormationのWaitConditionで制御します。
具体的には、CodeBuildプロジェクトによってS3バケットにソースバンドルの配置が完了した後とします。

考え方は以下のページで説明しているものと同様です。

あわせて読みたい
CloudFormationを使用してElastic Beanstalk入門 【CloudFormationを使用してElastic Beanstalk入門】 Elastic Beanstalkを取り上げます。 Elastic Beanstalk を使用すると、アプリケーションを実行しているインフラス...

上記のページと異なる点は、EventBridgeルールです。
上記のページですと、WaitConditionに成功シグナルを通知するLambda関数を実行する条件は、S3バケットにソースバンドルが配置されることでした。
今回の構成では、CodeBuildプロジェクトが正常に終了することを条件としました。
これは今回の構成では、ソースバンドルを2つ作成するためです。
つまり両方のバンドルのビルドが完了しない内に、Elastic Beanstalkリソース作成が開始されることを防止するためです。
なおCodeBuildに関するEventBridgeルールの作成においては、以下のページを参考としました。

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-build-notifications.html

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • Elastic Beanstalkアプリケーション:fa-143-application
  • Elastic Beanstalk環境:fa-143-env
  • Elastic Beanstalkバージョン1:dva-01-003-elasticbeanstalkstack-gskerhhpiq7r-applicationversion1-0dzuyn8onsz7
  • Elastic Beanstalkバージョン2:dva-01-003-elasticbeanstalkstack-gskerhhpiq7r-applicationversion2-wgik48ddoasm
  • Elastic Beanstalkドメイン:http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/

AWS Management Consoleから各リソースの作成状況を確認します。

Elastic Beanstalkアプリケーションを確認します。

Detail of Elastic Beanstalk 1.

正常にアプリケーションが作成されています。

アプリケーションバージョンを確認します。

Detail of Elastic Beanstalk 2.

2つのアプリケーションバージョンが作成されています。
既存の環境には、1つ目のバージョンが適用されていることがわかります。
またSourceの列を見ると、CodeBuildプロジェクトでビルドされたソースバンドルだということがわかります。
つまり同プロジェクトでビルドされ、S3バケットに配置されたソースバンドルを使用して、両バージョンは作成されているということです。

アプリケーション環境を確認します。

Detail of Elastic Beanstalk 3.

こちらも正常に作成されています。

Elastic Beanstalkによっていくつかのリソースが自動的に作成されています。
今回はその内のALBに紐づくAuto Scalingグループを確認します。

Detail of ALB 1.
Detail of ALB 2.

最大数/最小数/希望数がいずれも2のグループ内に、2つのEC2インスタンスが動作していることがわかります。

動作確認

デプロイ前

準備ができました。
まずはデプロイ前の挙動を確認します。
curlコマンドを使用して、Elastic Beanstalkドメインにアクセスします。

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
instance-id: i-096c93704b418aafb

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
instance-id: i-0d572f8438188e42e
Code language: Bash (bash)

確かにALB配下の2つのEC2インスタンスにアクセスできています。

デプロイ

いよいよ新しいバージョンをデプロイします。

先ほど確認したもう1つのバージョンを環境に適用します。

Detail of Elastic Beanstalk 4.
Detail of Elastic Beanstalk 5.

すぐにデプロイが開始されます。

以下がElastic Beanstalk環境に表示されるログです。

Detail of Elastic Beanstalk 6.

デプロイポリシーRolling with an additional batchでデプロイが始まりました。

この時点でのAuto Scalingグループを確認します。

Detail of ALB 3.
Detail of ALB 4.

最大数/最小数/希望数がいずれも3に変更されています。
そして既存の2台のEC2インスタンスに加えて、新しく1台インスタンスが起動中です。

以下はこの直後にElastic Beanstalkドメインにアクセスした結果です。

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
instance-id: i-0d572f8438188e42e

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
instance-id: i-096c93704b418aafb

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
updated: instance-id: i-00db22aadbc8b7897
Code language: Bash (bash)

3台のEC2インスタンスから応答がありました。
2台は古いアプリケーションバージョンの内容です。
残りの1台は新しいアプリケーションバージョンの内容です。

このようにデプロイポリシーRolling with an additional batchを選択した場合、インスタンス数は維持されますが、新旧のアプリケーションが混在する形となります。

引き続きデプロイ状況を確認します。

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
instance-id: i-0d572f8438188e42e

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
updated: instance-id: i-00db22aadbc8b7897

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
updated: instance-id: i-096c93704b418aafb
Code language: Bash (bash)

3台中、2台のインスタンスに対して、新しいアプリケーションバージョンが適用されました。

デプロイ完了後

しばらく待つと、デプロイが完了します。

Detail of Elastic Beanstalk 7.

ログには、デプロイの完了に伴い、一時的に追加していたインスタンスが削除された旨が記載されています。

改めてAuto Scalingグループを確認します。

Detail of ALB 5.
Detail of ALB 6.

最大数/最小数/希望数がいずれも2に戻り、グループ内には2台のインスタンスが動作しています。

最後にElastic Beanstalkドメインにアクセスします。

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
updated: instance-id: i-096c93704b418aafb

$ curl http://dva-01-003.ap-northeast-1.elasticbeanstalk.com/
updated: instance-id: i-00db22aadbc8b7897
Code language: Bash (bash)

確かに2台のインスタンスに新しいアプリケーションバージョンが適用されています。

まとめ

Elastic BeanstalkのRolling with an additional batch (追加バッチによるローリング)の挙動を検証しました。