Kubernetesで動作するアプリケーションのDNS管理には、external-dnsが広く使われています。external-dnsを使うと、様々なDNSプロバイダーのレコード設定を統一的な方法で管理できるため、運用の手間を大幅に削減できます。
しかし、external-dnsがDNSプロバイダー固有の機能にどこまで対応しているのかは気になるところです。特に、AWS Route53のフェイルオーバー機能への対応については、公式ドキュメントでも触れられているものの、本当にうまく統合できるのか気になったので試してみることにしました。
本記事では、external-dnsを使ってAWS Route53のフェイルオーバーを設定する方法を、実際の検証を通じて詳しく解説します。
TL;DR
- external-dnsはAWS Route53のフェイルオーバーに完全対応しています
- ただし、Health Checkの作成は自分で行う必要があります(external-dnsの責務外)
- ヘルスチェックIDをKubernetesのオブジェクトにセットすることで、external-dnsがフェイルオーバー設定を含むレコードを作成できます
検証環境の構成
今回は以下のような構成で検証を行います:
-
AWS側の構成
- 2つのALB(Primary/Secondary)を配置
- Route53でフェイルオーバー設定
- Primary ALBにヘルスチェックを設定
-
Kubernetes側の構成
- ローカルのKubernetesクラスター上でexternal-dnsを実行
- CRDを使ってDNSレコードを管理
なぜこの構成なのか?
実務では、ALBの後ろにEC2インスタンスやECSタスクなどのアプリケーションを配置するのが一般的です。しかし、今回は検証をシンプルにするために、ALBのFixed Response機能を使って、アプリケーション層を省略しています。
Fixed Responseとは、ALBのリスナーに設定できるアクションの1つで、リクエストに対して固定のレスポンスを返す機能です。この機能を使うことで:
- Primary ALBは通常時に200 HTTPステータスを返す
- 障害時は503 HTTPステータスを返すように変更
という形で、フェイルオーバーのシミュレーションが簡単にできます。
フェイルオーバーの仕組み
この構成では、以下のようにフェイルオーバーが動作します:
-
通常時
- クライアントからのリクエストはPrimary ALBに転送
- Primary ALBは200 OKを返す
-
障害発生時
- Primary ALBが503を返すようになる
- Route53のヘルスチェックがPrimary ALBを「Unhealthy」と判定
- Route53がDNSレコードを自動的に切り替え
- クライアントからのリクエストがSecondary ALBに転送
必要な準備
必要なリソース
- AWSアカウント
- AWSの認証情報(アクセスキー/シークレットキー)
- ローカルのKubernetesクラスター(k3dなど)
必要なツール
- awscli: AWSリソースの操作用
- terraform: インフラ構築用
- httpie: APIテスト用(curlでも代用可)
- dig: DNSレコードの確認用
AWS認証情報の設定
export AWS_ACCESS_KEY_ID="your_access_key"
export AWS_SECRET_ACCESS_KEY="your_secret_key"
export AWS_REGION="ap-northeast-1"
認証情報が正しく設定されているか確認:
aws sts get-caller-identity
検証環境の構築
重要:external-dnsの責務範囲について
external-dnsは「DNSレコードの管理」に特化したツールです。そのため、以下のリソースはexternal-dnsの責務範囲外となり、別途作成する必要があります:
- Route53ゾーン
- Route53 Health Check
- ALBなどのインフラリソース
今回はこれらのリソースをTerraformで作成します。
1. AWSリソースの作成
main.tf
を作成し、必要なリソースを定義します。
# Terraformの初期化
terraform init
# リソースの作成
terraform apply
main.tfを見る
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1" # Tokyo region
}
# Common tags for all resources
locals {
common_tags = {
Environment = "demo"
Project = "route53-failover"
Purpose = "external-dns-and-failover-testing"
Terraform = "true"
}
}
# Use default VPC and its public subnets
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
# Security Group for ALBs
resource "aws_security_group" "alb" {
name = "alb-failover-demo"
description = "Allow HTTP inbound traffic for failover demo"
vpc_id = data.aws_vpc.default.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
# Primary ALB
resource "aws_lb" "primary" {
name = "alb-failover-demo-primary"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = slice(data.aws_subnets.public.ids, 0, 2)
tags = local.common_tags
}
# Secondary ALB
resource "aws_lb" "secondary" {
name = "alb-failover-demo-secondary"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = slice(data.aws_subnets.public.ids, 0, 2)
tags = local.common_tags
}
# Primary ALB Listener with Fixed Response
resource "aws_lb_listener" "primary" {
load_balancer_arn = aws_lb.primary.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "This is PRIMARY ALB"
status_code = "200"
}
}
tags = local.common_tags
}
# Secondary ALB Listener with Fixed Response
resource "aws_lb_listener" "secondary" {
load_balancer_arn = aws_lb.secondary.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "This is SECONDARY ALB"
status_code = "200"
}
}
tags = local.common_tags
}
# Route53 Hosted Zone
resource "aws_route53_zone" "main" {
name = "failover.test"
tags = local.common_tags
}
# Health Check for Primary ALB
resource "aws_route53_health_check" "primary" {
fqdn = aws_lb.primary.dns_name
port = 80
type = "HTTP"
resource_path = "/"
failure_threshold = "3"
request_interval = "10"
tags = local.common_tags
}
# Primary Record (Commented out for now because it is created by external-dns)
# resource "aws_route53_record" "primary" {
# zone_id = aws_route53_zone.main.zone_id
# name = "www"
# type = "A"
# failover_routing_policy {
# type = "PRIMARY"
# }
# set_identifier = "primary"
# health_check_id = aws_route53_health_check.primary.id
# alias {
# name = aws_lb.primary.dns_name
# zone_id = aws_lb.primary.zone_id
# evaluate_target_health = true
# }
# }
# Secondary Record (Commented out for now because it is created by external-dns)
# resource "aws_route53_record" "secondary" {
# zone_id = aws_route53_zone.main.zone_id
# name = "www"
# type = "A"
# failover_routing_policy {
# type = "SECONDARY"
# }
# set_identifier = "secondary"
# alias {
# name = aws_lb.secondary.dns_name
# zone_id = aws_lb.secondary.zone_id
# evaluate_target_health = true
# }
# }
# Outputs
output "primary_alb_dns" {
value = aws_lb.primary.dns_name
}
output "primary_alb_arn" {
value = aws_lb.primary.arn
}
output "primary_listener_arn" {
value = aws_lb_listener.primary.arn
}
output "secondary_alb_dns" {
value = aws_lb.secondary.dns_name
}
output "zone_id" {
value = aws_route53_zone.main.zone_id
}
output "health_check_id" {
value = aws_route53_health_check.primary.id
}
output "nameservers" {
value = aws_route53_zone.main.name_servers
description = "Nameservers for the Route53 zone"
}
2. ALBの動作確認
両方のALBに直接アクセスして、正常に応答することを確認します:
# Primary ALBの確認
http $(terraform output -raw primary_alb_dns)
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 19
This is PRIMARY ALB
http $(terraform output -raw secondary_alb_dns)
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 21
This is SECONDARY ALB
このように、それぞれ別のレスポンスが返ってくることを確認します。
3. external-dnsのインストール
今回はBitnamiが提供するHelmチャートを使用します。このチャートを選んだ理由は:
- より多くの設定オプションが用意されている
- AWS Route53との連携が容易
- コミュニティでの利用実績が豊富
また、今回はCRDを使ってDNSレコードを管理します。これにより:
- IngressやGatewayなどのリソースが不要
- external-dnsの動作を純粋に検証可能
- 設定がシンプルで理解しやすい
helm install external-dns \
--set provider=aws \
--set aws.zoneType=public \
--set aws.credentials.accessKey="$AWS_ACCESS_KEY_ID" \
--set aws.credentials.secretKey="$AWS_SECRET_ACCESS_KEY" \
--set txtOwnerId=external-dns-and-failover-testing \
--set "domainFilters[0]=failover.test" \
--set policy=sync \
--set "sources[0]=crd" \
--set crd.create=true \
--set crd.apiversion=externaldns.k8s.io/v1alpha1 \
--set crd.kind=DNSEndpoint \
oci://registry-1.docker.io/bitnamicharts/external-dns
各オプションの説明
オプション | 説明 | なぜ必要か |
---|---|---|
provider=aws | AWS Route53を使用 | DNSプロバイダーの指定 |
aws.zoneType=public | パブリックゾーンを使用 | ゾーンタイプの指定 |
aws.credentials.* | AWS認証情報 | Route53への認証用 |
txtOwnerId | TXTレコードの所有者ID | レコード管理の識別用(なんでもいい) |
domainFilters | 監視対象ドメイン | 範囲限定のため |
policy=sync | 同期ポリシー | レコード同期方式の指定 |
sources[0]=crd | CRDをソースとして使用 | レコード定義方法の指定 |
crd.* | CRD関連の設定 | CRD作成・設定用 |
4. フェイルオーバーレコードの作成
www.failover.test.yaml
を作成します:
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: www.failover.test
namespace: default
spec:
endpoints:
# Primaryレコード
- dnsName: www.failover.test
recordType: A
recordTTL: 60
providerSpecific:
- name: alias
value: "true"
- name: aws/failover
value: PRIMARY
- name: aws/health-check-id
value: b0d7bb6b-d5aa-44a2-9f33-53f550df7f96 # terraform output -raw primary_health_check_id
- name: aws/evaluate-target-health
value: "true"
setIdentifier: www-primary
targets:
- alb-failover-demo-primary-88520931.ap-northeast-1.elb.amazonaws.com # terraform output -raw primary_alb_dns
# Secondaryレコード
- dnsName: www.failover.test
recordType: A
recordTTL: 60
providerSpecific:
- name: alias
value: "true"
- name: aws/failover
value: SECONDARY
setIdentifier: www-secondary
targets:
- alb-failover-demo-secondary-1897280663.ap-northeast-1.elb.amazonaws.com # terraform output -raw secondary_alb_dns
設定のポイント
-
フェイルオーバー設定
-
aws/failover
: PRIMARY/SECONDARYの指定 -
aws/health-check-id
: ヘルスチェックの関連付け -
aws/evaluate-target-health
: ヘルスチェック評価の有効化
-
-
レコード識別子
-
setIdentifier
: 同じドメインの複数レコードを区別。Route53では「Record ID」と呼ばれるもの。
-
-
エイリアス設定
-
alias: "true"
: ALBへのエイリアスレコードとして設定
-
この設定を適用します:
kubectl apply -f www.failover.test.yaml
external-dnsのログを確認して、レコードが作成されたことを確認します。
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE cname-www.failover.test TXT" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE cname-www.failover.test TXT" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE www.failover.test A" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE www.failover.test A" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE www.failover.test TXT" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="Desired change: CREATE www.failover.test TXT" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
time="2024-11-07T08:33:55Z" level=info msg="6 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z10116603TTQ6VNVFWSZL zoneName=failover.test.
フェイルオーバーのテスト
1. 通常時の動作確認
まず、dig
でwww.failover.test
のIPアドレスを確認します:
dig @ns-1413.awsdns-48.org +noall +answer -t A www.failover.test
# ^^^^^^^^^^^^^^^^^^^^^^ この部分はRoute53のネームサーバーに合わせて変更してください
上で確認したIPアドレスにアクセスして、Primary ALBにアクセスできることを確認します:
http 18.178.70.206 # digで確認したIPアドレスを使用
次のように「This is PRIMARY ALB」というレスポンスが返ってくるはずです:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 19
This is PRIMARY ALB
2. フェイルオーバーのテスト
フェイルオーバーをテストするためにPrimary ALBを意図的に異常状態にします:
# Primary ALBのステータスを503に変更
aws elbv2 modify-listener \
--listener-arn $(terraform output -raw primary_listener_arn) \
--default-actions '[{"Type": "fixed-response","FixedResponseConfig": {"MessageBody": "Service Unavailable","StatusCode": "503","ContentType": "text/plain"}}]'
http $(terraform output -raw primary_alb_dns)
次のように「Service Unavailable」というレスポンスが返ってくるはずです:
HTTP/1.1 503 Service Temporarily Unavailable
Content-Type: text/plain
Content-Length: 19
Service Unavailable
3. フェイルオーバーの確認
AWSのコンソールでRoute 53の「Health checks」を確認して、「Status」が「Unhealthy」になっていることを確認します。Unhealthyになるまで数十秒かかることがあります。
Unhealthyになったら、dig
でwww.failover.test
のIPアドレスを確認します。
dig @ns-1413.awsdns-48.org +noall +answer -t A www.failover.test
# ^^^^^^^^^^^^^^^^^^^^^^ この部分はRoute53のネームサーバーに合わせて変更してください
出力結果の例:
www.failover.test. 60 IN A 54.150.184.236
www.failover.test. 60 IN A 54.64.199.152
さっきとは違うIPアドレスが返ってくることを確認します。キャッシュが効いていて、古いIPアドレスが返ってくることがあるので、時間を置いてから確認してください。
続いて、そのIPアドレスにアクセスして、Secondary ALBにアクセスできることを確認します。
http 54.150.184.236 # digで確認した新しいIPアドレス
次のようにSecondary ALBからのレスポンスが返ってれば、フェイルオーバーが成功しています:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 21
This is SECONDARY ALB
4. 復旧テスト
Primary ALBを復旧させるために、fixed-response
を元に戻します。
# Primary ALBを正常状態に戻す
aws elbv2 modify-listener \
--listener-arn $(terraform output -raw primary_listener_arn) \
--default-actions '[{"Type": "fixed-response","FixedResponseConfig": {"MessageBody": "This is PRIMARY ALB","StatusCode": "200","ContentType": "text/plain"}}]'
AWSのコンソールでRoute 53の「Health checks」を確認して、「Status」が「Healthy」になっていることを確認したら、dig
でwww.failover.test
のIPアドレスを確認します。
dig @ns-1413.awsdns-48.org +noall +answer -t A www.failover.test
# ^^^^^^^^^^^^^^^^^^^^^^ この部分はRoute53のネームサーバーに合わせて変更してください
Primary ALBのIPアドレスに戻っているはずです。
クリーンアップ
検証が終わったら、以下の順序でリソースを削除します:
# 1. DNSレコードの削除
kubectl delete -f www.failover.test.yaml
# 2. external-dnsの削除
helm uninstall external-dns
# 3. AWSリソースの削除
terraform destroy
まとめ
- external-dnsはAWS Route53のフェイルオーバーに完全対応
- 適切な設定で信頼性の高いフェイルオーバーが実現可能
- ヘルスチェックの作成は別途必要だが、運用は容易