Route53でフェイルオーバーしてエラーページを表示する

Route 53でフェイルオーバーしてエラーページを表示する構成

Route53のフェイルオーバールーティングポリシーでエラーページを表示する

AWS SAAの出題範囲の1つでもある、高弾力性に関連する内容です。Route 53のフェイルオーバールーティングポリシーを選択して、障害発生時にエラーページを表示させることによって、システムの弾力性を高めることができます。

Route53では様々なルーティングポリシーを選択することが可能ですが、その1つがフェイルオーバールーティングポリシーです。

フェイルオーバーのルーティングにより、リソースが正常な場合にリソースにトラフィックをルーティングできます。また、最初のリソースが正常でない場合は別のリソースにルーティングできます。

フェイルオーバールーティング

今回はALB配下のEC2インスタンスでシステムを構築してプライマリWebサイトを構築します。プライマリ側に異常が発生した場合は、S3バケット内に配置したエラーページを表示するように設定します。

ちなみにRoute53には多くのルーティングポリシーが用意されています。

レイテンシーに基づくルーティングに関しては、以下のページをご確認ください。

あわせて読みたい
Route53のレイテンシーに基づくルーティング(ALB向け) 【Route53のレイテンシーに基づくルーティング(ALB向け)】 Route 53が提供するルーティングポリシーの1つに、レイテンシーに基づくルーティングがあります。 複数の AWS...

位置情報に基づくルーティングに関しては、以下のページをご確認ください。

あわせて読みたい
CloudFormationを使用して、Route53の位置情報ルーティングポリシー環境を構築する 【CloudFormationを使用して、Route53の位置情報ルーティングポリシー環境を構築する】 Route 53が提供するルーティングポリシーの1つに、位置情報ルーティングがありま...

構築する環境

Diagram of failover with Route53 and display error page.

プライマリーWebサイト側の構成

2つのプライベートサブネットにEC2インスタンスを1つずつ配置します。インスタンスは最新のAmazon Linux 2023とします。

インスタンスの初期設定として、Apacheのインストール・起動の設定を行い、Webサーバとして動作させます。

EC2インスタンスの前面にELBを配置します。ELBはALBタイプとします。

エラーページ側の構成

次にエラーページですが、S3バケットで用意します。

バケットの静的Webサイトホストティング機能を有効化し、エラーページ用のHTMLファイルをバケットに配置します。

CloudFormationカスタムリソースを使用してLambda関数を呼び出し、HTMLファイルを自動的に作成作成します。

Route53の構成

最後にRoute53ですが、先述の通り、アクティブ・パッシブタイプのフェイルオーバールーティングポリシーを設定します。プライマリリソースにALBを指定し、セカンダリリソースにS3バケットを指定します。

フェイルオーバーの挙動確認ですが、以下の手順で実施します。

  1. プライマリリソースが正常時の動作を確認する。
  2. 2台のEC2インスタンスを停止させ、プライマリリソースに障害が発生した状況を再現する。
  3. セカンダリリソースにフェイルオーバーして、エラーページが表示されることを確認する。
  4. 2台のEC2インスタンスを起動させ、プライマリリソースが障害から復旧した状況を再現する。
  5. 再度プライマリリソースにトラフィックがルーティングされることを確認する。

環境構築用のCloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。

以下のURLにCloudFormationテンプレートを配置します。

https://github.com/awstut-an-r/awstut-saa/tree/main/01/001

なお今回の構成で使用するドメインは、Route53で取得したものを使用することを前提としています。そのためドメインのホストゾーン(HostedZone)に関する設定は行いません。AWS以外で取得したドメインを使用する場合は、上記のテンプレートにHostedZoneリソースの定義を追加で実施してください。

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

今回の環境を構成するための、各テンプレートファイルのポイントを取り上げます。

プライベートサブネット内のインスタンスをALBにアタッチする

まずプライマリリソースを、ALBと2台のEC2インスタンスで構築します。

ポイントは2点あります。

プライベートサブネット内でyum/dnfを実行する

1点目はインスタンスが設置されているサブネットです。

Resources:
  Instance1:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref PrivateSubnet1
          GroupSet:
            - !Ref InstanceSecurityGroup
      UserData: !Base64 |
        #!/bin/bash -xe
        dnf update -y
        dnf install -y httpd
        systemctl start httpd
        systemctl enable httpd
        ec2-metadata -i > /var/www/html/index.html
Code language: YAML (yaml)

今回の構成では2台のインスタンスを作成しますが、代表して1台を記載しました。

今回の構成では、プライベートサブネットにインスタンスを配置しています。

通常、yum/dnfを実行する場合、インターネットに抜ける経路が必要となります。ただしAmazon Linuxの場合、S3バケット上にホスティングされるyum/dnfリポジトリにアクセスすることで、yum/dnfを実行することが可能です。インターネットにアクセスするルートがないプライベートサブネットにおいても、S3用エンドポイントを設置することで、セキュアにS3バケットにアクセスすることが可能です。

今回は以下の通り、S3用のVPCエンドポイントを作成します。

Resources:
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !Ref PrivateRouteTable
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcId: !Ref VPC
Code language: YAML (yaml)

詳細につきましては以下のページをご確認ください。

あわせて読みたい
プライベートサブネットのインスタンスでyum/dnfを実行する 【プライベートサブネット内のインスタンスでyum/dnfを実行する構成】 プライベートサブネット内のインスタンスで、yum/dnfを実行する方法を確認します。 今回は以下の2...

今回はユーザーデータを使用して、dnfによるApacheのインストールおよび起動設定、そしてindex.htmlにインスタンスIDを書き込んで作成します。

ALBにパブリックサブネットを紐付ける

2点目はALBにアタッチするサブネットです。

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub "${Prefix}-ALB"
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      Type: application

  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VPC
      Name: !Sub "${Prefix}-ALBTargetGroup"
      Protocol: HTTP
      Port: !Ref HTTPPort
      HealthCheckProtocol: HTTP
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthyThresholdCount: !Ref HealthyThresholdCount
      UnhealthyThresholdCount: !Ref UnhealthyThresholdCount
      HealthCheckTimeoutSeconds: !Ref HealthCheckTimeoutSeconds
      HealthCheckIntervalSeconds: !Ref HealthCheckIntervalSeconds
      Matcher:
        HttpCode: !Ref HttpCode
      Targets:
        - Id: !Ref Instance1
        - Id: !Ref Instance2

  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref ALBTargetGroup
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: !Ref HTTPPort
      Protocol: HTTP
Code language: YAML (yaml)

プライベートサブネット内のインスタンスをALBにアタッチするためには、ALBにはパブリックサブネットを紐付けます。パブリックサブネットはインスタンスが配置されているAZごとに必要になります。今回は2つのAZに1つずつインスタンスを配置していますから、同様に2つのAZに1つずつパブリックサブネットを用意します。

詳細は以下のページをご確認ください。

あわせて読みたい
プライベートサブネット内のインスタンスをALBにアタッチする 【プライベートサブネット内のインスタンスをALBにアタッチする構成】 プライベートサブネット内に設置されたインスタンスを、ALBにアタッチする方法を確認します。 AWS...

エラーページを設置するS3バケット名はドメイン名と一致させる

S3バケットにエラーページを設置し、公開するための設定を行います。

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref DomainName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false
      WebsiteConfiguration:
        IndexDocument: !Ref IndexDocument

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn:
      - S3Bucket
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          Action:
            - s3:GetObject
          Effect: Allow
          Resource: !Sub "arn:aws:s3:::${DomainName}/*"
          Principal: "*"
Code language: YAML (yaml)

S3の静的Webサイトホスティング機能を使用して、エラーページを表示させます。本機能の詳細については、以下のページをご確認ください。

あわせて読みたい
S3静的ウェブサイトホスティング機能でサイトを公開する 【S3静的ウェブサイトホスティング機能でサイトを公開する構成】 S3の静的ウェブサイトホスティング機能を使って、ウェブサイトを公開する方法を確認します。 ウェブサ...

静的Webホスティング機能を使用してエラーページを表示させる上でポイントとなる点は、バケットの名前です。

バケットは、ドメインまたはサブドメインと同じ名前にする必要があります。例えば、サブドメイン acme.example.com を使用している場合、バケットの名前は acme.example.com にする必要があります。

Amazon S3 バケットでホストされているウェブサイトへのトラフィックのルーティング

バケット名はBucketNameプロパティで指定しますが、本プロパティにプライマリWebサイトで使用するドメイン名と同一の文字列を指定する必要があります。今回は組み込み関数Fn::Refを使用して、ドメイン名を参照します。

CloudFormationカスタムリソースを使用して、エラーページを自動的に作成する

今回はエラーページ用のHTMLファイルを自動的に用意します。

Resources:
  CustomResource:
    Type: Custom::CustomResource
    DependsOn:
      - S3Bucket
    Properties:
      ServiceToken: !GetAtt Function.Arn

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          bucket_name = os.environ['BUCKET_NAME']
          index_document = os.environ['INDEX_DOCUMENT']
          prefix = os.environ['PREFIX']

          object_body = """<html>
            <head>{prefix}</head>
            <body>
              <h1>index.html</h1>
              <p>{bucket_name}</p>
            </body>
          </html>""".format(bucket_name=bucket_name, prefix=prefix)
          content_type = 'text/html'
          char_code= 'utf-8'

          s3_client = boto3.client('s3')

          CREATE = 'Create'
          DELETE = 'Delete'
          response_data = {}

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                put_response = s3_client.put_object(
                  Bucket=bucket_name,
                  Key=index_document,
                  Body=object_body.encode(char_code),
                  ContentEncoding=char_code,
                  ContentType=content_type)
                print(put_response)

              elif event['RequestType'] == DELETE:
                list_response = s3_client.list_objects_v2(
                  Bucket=bucket_name)
                for obj in list_response['Contents']:
                  delete_response = s3_client.delete_object(
                    Bucket=bucket_name,
                    Key=obj['Key'])
                  print(delete_response)

              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      Environment:
        Variables:
          BUCKET_NAME: !Ref DomainName
          INDEX_DOCUMENT: !Ref IndexDocument
          PREFIX: !Ref Prefix
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn

  FunctionRole:
    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
      Policies:
        - PolicyName: FunctionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                Resource:
                  - !Sub "arn:aws:s3:::${DomainName}"
                  - !Sub "arn:aws:s3:::${DomainName}/*"Code language: YAML (yaml)

CloudFormationカスタムリソースを使用して、CloudFormationスタック作成/削除時に、Lambda関数を自動的に実行します。スタックのアクションによって、Lambda関数は以下の通りに動作します。

  • スタック作成時:S3バケットにHTMLファイルを配置する。
  • スタック削除時:S3バケット内の全オブジェクトを削除する。

詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースでS3オブジェクトを作成・削除する 【CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法】 CloudFormationカスタムリソースはスタック操作(作成、更新、...

ヘルスチェックでプライマリリソースの障害の有無を確認する

まずRoute53で、プライマリリソースの障害の発生状況を確認できるように設定します。

Resources:
  DnsHealthCheck:
    Type: AWS::Route53::HealthCheck
    Properties:
      HealthCheckConfig:
        Port: !Ref HTTPPort
        Type: HTTP
        ResourcePath: /
        FullyQualifiedDomainName: !Ref ALBDnsName
        RequestInterval: !Ref RequestInterval
        FailureThreshold: !Ref FailureThreshold
Code language: YAML (yaml)

HealthCheckConfigプロパティで、ヘルスチェックする対象や方法、障害発生とみなす条件を指定します。

  • FullyQualifiedDomainNameプロパティ:ヘルスチェックの対象をALBのルートページに指定する。
  • Portプロパティ、Typeプロパティ:ヘルスチェックはHTTP(80/tcp)で行う。
  • RequestIntervalプロパティ:ヘルスチェックは30秒に1回実施する。
  • FailureThresholdプロパティ:ヘルスチェックの結果、HTTPステータスコードが3回連続で「200」で無かった場合、障害が発生したと見なす。

Route53のアクティブ・パッシブタイプのフェイルオーバー設定

最後にRoute53のレコード情報を定義します。

Resources:
  AwstutNetDnsRecordGroup:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: !Sub "${DomainName}."
      RecordSets:
        # ALB
        - Name: !Ref DomainName
          Failover: PRIMARY
          HealthCheckId: !Ref DnsHealthCheck
          SetIdentifier: primary
          Type: A
          AliasTarget:
            DNSName: !Ref ALBDnsName
            EvaluateTargetHealth: true
            HostedZoneId: !Ref ALBHostedZoneId
        # S3
        - Name: !Ref DomainName
          Failover: SECONDARY
          SetIdentifier: secondary
          Type: A
          AliasTarget:
            DNSName: !Ref S3DnsName
            HostedZoneId: !Ref S3HostedZoneId
Code language: YAML (yaml)

HostedZoneNameプロパティで、Route53に登録するドメイン名を指定します。先述のS3バケット名と同一のドメイン名を指定します。

RecordSetsプロパティで、HostedZoneNameプロパティで指定したドメイン名に紐付けるリソースを指定します。1つ目の要素がALBで、2つ目がS3バケットです。フェイルオーバーを設定する上で、特にポイントとなる設定は、以下の3点です。

  • Failoverプロパティは、ALBはプライマリリソースなので「PRIMARY」、S3はセカンダリリソースなので「SECONDARY」を指定する。
  • ALB側のHealthCheckIdプロパティに、先述のヘルスチェックリソースを指定する。
  • ALB側のAliasTargetプロパティ内のEvaluateTargetHealthプロパティに「true」を指定する。

上記の設定を行うことで、ALBに障害が発生した場合に、自動的にフェイルオーバーし、S3側にトラフィックがルーティングされるようになります。

なおRecordSetGroup内のHostedZoneIdプロパティですが、設定するべき値が定められています。

まずALB側の値ですが、Elastic Load Balancing エンドポイントとクォータを参照すると、ap-northeast-1リージョンでは「Z14GRHDCWA56QT」を指定する必要があることがわかります。

次にS3バケット側の値ですが、Amazon Simple Storage Service エンドポイントとクォータによりますと、ap-northeast-1リージョンでは「Z2M4EHUR26P7ZW」を指定する必要があることがわかります。

環境構築

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

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

CloudFormationスタックを作成します。

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

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

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

  • ドメイン名:awstut.net
  • EC2インスタンスのインスタンスID:i-032febbe1b5fd6a9e, i-00520c950f3fbcd97
  • ALB名:saa-01-001-ALB
  • S3バケット名:awstut.net
  • Route53ヘルスチェックID:d0d474a5-aaba-4cf3-b5c4-dc3075f56b49

Route53の設定状況を確認します。

Route53 failover configuration.

テンプレートファイルで指定した通りに、プライマリにALB、セカンダリにS3バケットが登録されていることがわかります。

S3バケットを確認します。

Place the HTML file in the S3 bucket of the failover destination.

確かにエラーページ用のHTMLファイルが配置されていることがわかります。CloudFormationスタック作成時にCloudFormationカスタムリソースが動作し、自動的にHTMLファイルが作成されたということです。

ALBに障害が発生した際は、フェイルオーバーして、このHTMLファイルが表示されることになります。

挙動確認:正常時

準備が整いましたので、構築したWebアプリにアクセスして挙動を確認します。

$ curl http://awstut.net
instance-id: i-00520c950f3fbcd97

$ curl http://awstut.net
instance-id: i-032febbe1b5fd6a9e
Code language: Bash (bash)

2台のEC2インスタンスのIDが交互に返ってきました。これはALBによって、2台のインスタンスにラウンドロビンでトラフィックがルーティングされているためです。

このことより、現状では、ALBおよび配下のEC2インスタンスは、ヘルスチェックに合格し、プライマリリソースとして正常に動作していることがわかります。

Route53ヘルスチェック状況確認:正常時

一応、正常時のヘルスチェックの状態も確認します。

Route53 health check succeeded.

Statusが「Healthy」とあります。

このことから、ヘルスチェックに成功していることがわかります。

EC2インスタンスを停止させて障害発生時を再現する

正常にプライマリリソースが動作していることがわかりました。

今度は2台のインスタンスを停止させて、意図的に障害が発生した状況を作ります。

$ aws ec2 stop-instances \
--instance-ids i-032febbe1b5fd6a9e i-00520c950f3fbcd97
Code language: Bash (bash)
Stop the instance to reproduce the failure.

両インスタンスのInstance stateの値が「Stopped」とありますので、停止しました。

Route53ヘルスチェック状況確認:障害発生時

インスタンスが停止した後、改めてヘルスチェックの状況を確認します。

Route53 health check failed due to failure

Statusの値が「Unthealthy」となりました。ヘルスチェックに失敗しているということです。

これはインスタンスを停止したことによって、ALBに登録されている正常なターゲットがなくなったためです。

挙動確認:障害発生時

ヘルスチェックに失敗していることが確認できましたので、改めてWebアプリにアクセスします。

$ curl http://awstut.net
<html>
  <head>saa-01-001</head>
  <body>
    <h1>index.html</h1>
    <p>awstut.net</p>
  </body>
</html>
Code language: Bash (bash)

S3バケットに設置したHTMLファイルが返ってきました。

このことから、ALBへのヘルスチェックに失敗したことによって、Route53がトラフィックのルーティング先を、プライマリリソースであるALBから、セカンダリリソースであるS3にフェイルオーバーしたことがわかります。

インスタンスを起動させて障害復旧を再現する

障害発生時に自動的にフェイルオーバーすることはわかりました。続いて障害復旧時の挙動を確認します。

障害復旧を再現するために、先ほど停止させたインスタンスを再度起動させます。

$ aws ec2 start-instances \
--instance-ids i-032febbe1b5fd6a9e i-00520c950f3fbcd97
Code language: Bash (bash)
Start the instance and reproduce the failure recovery.

Instance Statusの値が「Running」となりました。これでインスタンスは現在起動中ということを意味しています。

挙動確認:障害復旧時

インスタンスが起動したところで、改めてWebアプリにアクセスし、挙動を確認します。

$ curl http://awstut.net
instance-id: i-032febbe1b5fd6a9e

$ curl http://awstut.net
instance-id: i-00520c950f3fbcd97
Code language: Bash (bash)

再びALB側にトラフィックがルーティングされていることがわかります。これはインスタンスが起動することによって、インスタンスがALBのターゲットとして再認識され、Route53からALBに対するヘルスチェックに成功したためです。

Route53 health check passed again due to disaster recovery.

このようにRoute53のアクティブ・パッシブタイプのフェイルオーバールーティングポリシーにおいては、プライマリリソースが障害から復旧した場合、自動的にフェイルオーバー状態は解消され、再度プライマリ側にトラフィックがルーティングされるようになります。

まとめ

今回はRoute53のアクティブ・パッシブタイプのフェイルオーバーのハンズオンを通じて、障害発生時および障害復旧時のルーティングの挙動を確認しました。

Route 53のフェイルオーバールーティングポリシーを使用することで、障害発生時にエラーページを表示させることができ、システムの弾力性を高めることができます。