4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

external-dnsでAWS Route53フェイルオーバーを実現する方法

Posted at

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がフェイルオーバー設定を含むレコードを作成できます

検証環境の構成

今回は以下のような構成で検証を行います:

  1. AWS側の構成

    • 2つのALB(Primary/Secondary)を配置
    • Route53でフェイルオーバー設定
    • Primary ALBにヘルスチェックを設定
  2. Kubernetes側の構成

    • ローカルのKubernetesクラスター上でexternal-dnsを実行
    • CRDを使ってDNSレコードを管理

なぜこの構成なのか?

実務では、ALBの後ろにEC2インスタンスやECSタスクなどのアプリケーションを配置するのが一般的です。しかし、今回は検証をシンプルにするために、ALBのFixed Response機能を使って、アプリケーション層を省略しています。

Fixed Responseとは、ALBのリスナーに設定できるアクションの1つで、リクエストに対して固定のレスポンスを返す機能です。この機能を使うことで:

  • Primary ALBは通常時に200 HTTPステータスを返す
  • 障害時は503 HTTPステータスを返すように変更

という形で、フェイルオーバーのシミュレーションが簡単にできます。

フェイルオーバーの仕組み

この構成では、以下のようにフェイルオーバーが動作します:

  1. 通常時

    • クライアントからのリクエストはPrimary ALBに転送
    • Primary ALBは200 OKを返す
  2. 障害発生時

    • 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を見る
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

設定のポイント

  1. フェイルオーバー設定

    • aws/failover: PRIMARY/SECONDARYの指定
    • aws/health-check-id: ヘルスチェックの関連付け
    • aws/evaluate-target-health: ヘルスチェック評価の有効化
  2. レコード識別子

    • setIdentifier: 同じドメインの複数レコードを区別。Route53では「Record ID」と呼ばれるもの。
  3. エイリアス設定

    • 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. 通常時の動作確認

まず、digwww.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になったら、digwww.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」になっていることを確認したら、digwww.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のフェイルオーバーに完全対応
  • 適切な設定で信頼性の高いフェイルオーバーが実現可能
  • ヘルスチェックの作成は別途必要だが、運用は容易
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?