Route53のフェイルオーバールーティングポリシーでエラーページを表示する
AWS SAAの出題範囲の1つでもある、高弾力性に関連する内容です。Route 53のフェイルオーバールーティングポリシーを選択して、障害発生時にエラーページを表示させることによって、システムの弾力性を高めることができます。
Route53では様々なルーティングポリシーを選択することが可能ですが、その1つがフェイルオーバールーティングポリシーです。
フェイルオーバーのルーティングにより、リソースが正常な場合にリソースにトラフィックをルーティングできます。また、最初のリソースが正常でない場合は別のリソースにルーティングできます。
フェイルオーバールーティング
今回はALB配下のEC2インスタンスでシステムを構築してプライマリWebサイトを構築します。プライマリ側に異常が発生した場合は、S3バケット内に配置したエラーページを表示するように設定します。
ちなみにRoute53には多くのルーティングポリシーが用意されています。
レイテンシーに基づくルーティングに関しては、以下のページをご確認ください。
位置情報に基づくルーティングに関しては、以下のページをご確認ください。
構築する環境
プライマリーWebサイト側の構成
2つのプライベートサブネットにEC2インスタンスを1つずつ配置します。インスタンスは最新のAmazon Linux 2023とします。
インスタンスの初期設定として、Apacheのインストール・起動の設定を行い、Webサーバとして動作させます。
EC2インスタンスの前面にELBを配置します。ELBはALBタイプとします。
エラーページ側の構成
次にエラーページですが、S3バケットで用意します。
バケットの静的Webサイトホストティング機能を有効化し、エラーページ用のHTMLファイルをバケットに配置します。
CloudFormationカスタムリソースを使用してLambda関数を呼び出し、HTMLファイルを自動的に作成作成します。
Route53の構成
最後にRoute53ですが、先述の通り、アクティブ・パッシブタイプのフェイルオーバールーティングポリシーを設定します。プライマリリソースにALBを指定し、セカンダリリソースにS3バケットを指定します。
フェイルオーバーの挙動確認ですが、以下の手順で実施します。
- プライマリリソースが正常時の動作を確認する。
- 2台のEC2インスタンスを停止させ、プライマリリソースに障害が発生した状況を再現する。
- セカンダリリソースにフェイルオーバーして、エラーページが表示されることを確認する。
- 2台のEC2インスタンスを起動させ、プライマリリソースが障害から復旧した状況を再現する。
- 再度プライマリリソースにトラフィックがルーティングされることを確認する。
環境構築用の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)
詳細につきましては以下のページをご確認ください。
今回はユーザーデータを使用して、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つずつパブリックサブネットを用意します。
詳細は以下のページをご確認ください。
エラーページを設置する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サイトホスティング機能を使用して、エラーページを表示させます。本機能の詳細については、以下のページをご確認ください。
静的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バケット内の全オブジェクトを削除する。
詳細につきましては、以下のページをご確認ください。
ヘルスチェックでプライマリリソースの障害の有無を確認する
まず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スタックを作成します。
スタック作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要なリソースの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の設定状況を確認します。
テンプレートファイルで指定した通りに、プライマリにALB、セカンダリにS3バケットが登録されていることがわかります。
S3バケットを確認します。
確かにエラーページ用の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ヘルスチェック状況確認:正常時
一応、正常時のヘルスチェックの状態も確認します。
Statusが「Healthy」とあります。
このことから、ヘルスチェックに成功していることがわかります。
EC2インスタンスを停止させて障害発生時を再現する
正常にプライマリリソースが動作していることがわかりました。
今度は2台のインスタンスを停止させて、意図的に障害が発生した状況を作ります。
$ aws ec2 stop-instances \
--instance-ids i-032febbe1b5fd6a9e i-00520c950f3fbcd97
Code language: Bash (bash)
両インスタンスのInstance stateの値が「Stopped」とありますので、停止しました。
Route53ヘルスチェック状況確認:障害発生時
インスタンスが停止した後、改めてヘルスチェックの状況を確認します。
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)
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のアクティブ・パッシブタイプのフェイルオーバールーティングポリシーにおいては、プライマリリソースが障害から復旧した場合、自動的にフェイルオーバー状態は解消され、再度プライマリ側にトラフィックがルーティングされるようになります。
まとめ
今回はRoute53のアクティブ・パッシブタイプのフェイルオーバーのハンズオンを通じて、障害発生時および障害復旧時のルーティングの挙動を確認しました。
Route 53のフェイルオーバールーティングポリシーを使用することで、障害発生時にエラーページを表示させることができ、システムの弾力性を高めることができます。