🌊

External-DNSにより、EKS上のアプリケーションにドメイン名を設定する

2024/07/23に公開

エンジニアのTです。

7/27(火)、Sprocketはデータ連携の新機能を発表しました。
Sprocket、あらゆる顧客データを取り込める「データ連携(バッチ取り込み)」機能を提供開始

当機能はかねてより一部のクライアントに提供していたものですが、GAリリースに伴い、アーキテクチャの刷新を実施しました。
その一環として、データ連携の設定項目を更新できるAPIを追加しました。従来はクライアントが少なく、更新の頻度が低かったため、エンジニアが直接データベースへアクセスして作業をしていました。GAにあたっては、他のメンバーが設定項目をメンテナンスできるように機能を追加する必要があります。

まず、以下の構成を決定しました。

  • 設定項目のデータベースを操作するgRPCサーバーをEKS上に構築します。
  • 認証機能を持つBackends For Frontendsサーバー(以下、BFFサーバー)へgRPCサーバと対応するエンドポイントを追加します。
  • BFFサーバーとEKS上のアプリケーションが通信することにより、データベースへの問い合わせを実現します。

BFFサーバーはEKSの外部で稼働しているため、gRPCサーバーを適切な範囲で公開する手順を調査し、実施しました。
当記事では、アプリケーションの公開にあたって実施した内容を手順を交えながら解説いたします。

EKSのサービスをVPC内部に公開する手順

マネージドサービスにおけるKubernetes上のアプリケーションを公開する場合、LoadBalancerタイプのKubernetes Serviceを作成します。また、クラウドサービスプロバイダ毎に決められたアノテーションを記述すると、ロードバランサを作成してKubernetes Serviceへの紐付けまでを行ってくれます。

以下の手順により、検証用のnginxサーバーをVPC内の範囲に公開できることを確認します。なお、EKSクラスターは既に構築されているものとします。

下記のマニフェスト例では、nginxサーバーのKubernetes Deploymentと、LoadBalancerタイプのKubernetes Serviceを定義しています。
また、Kubernetes Serviceのアノテーションにより、VPC内の範囲で公開するように指定しています。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: external
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
  type: LoadBalancer
  ports:
    - port: 80
      name: http
      targetPort: 80
  selector:
    app: nginx

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
              name: http

このマニフェストをapplyすると、Kubernetes ServiceへEXTERNAL-IPが設定されます。

kubectl get svc nginx
NAME    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                       PORT(S)        AGE
nginx   LoadBalancer   10.100.102.195   k8s-external-nginx-24a9176dc6-10e691950c49324a.elb.ap-northeast-1.amazonaws.com   80:32751/TCP   28m

AWSのリソースを参照すると、NLBが新規に作成されており、そのDNS名がKubernetes Serviceに割り当てられています。
作成されたNLBのScheme: "internal"は、DNS解決をVPC内で行うことを示しています。

aws elbv2 describe-load-balancers | jq ".LoadBalancers[] | { LoadBalancerName:.LoadBalancerName, DNSName:.DNSName, Scheme:.Scheme, Type:.Type}"
{
  "LoadBalancerName": "k8s-external-nginx-b93fcd853e",
  "DNSName": "k8s-external-nginx-b93fcd853e-56fefd8e343f5908.elb.ap-northeast-1.amazonaws.com",
  "Scheme": "internal",
  "Type": "network"
}

VPC内で稼働しているEC2にログインして、NLBのDNS名に対してHTTPリクエストを送信すると、nginxからStatus 200のレスポンスが返ってくることを確認できました。

curl -I k8s-external-nginx-b93fcd853e-56fefd8e343f5908.elb.ap-northeast-1.amazonaws.com
HTTP/1.1 200 OK
Server: nginx/1.21.1
Date: Wed, 28 Jul 2021 01:10:03 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 06 Jul 2021 14:59:17 GMT
Connection: keep-alive
ETag: "60e46fc5-264"
Accept-Ranges: bytes

また、このNLBはinternalタイプであるため、VPC外部からのHTTPリクエストは無効です。

ドメイン名を割り当てる必要性

前節では、Kubernetes Serviceのアノテーションにより、VPC内の範囲でアプリケーションを公開できることを確認しました。
しかしながら、自動生成されるNLBをそのまま運用する場合、以下のような問題があります。

まず、生成されるNLBのドメイン名はアプリケーションの内容に即していません。外部公開している通信ではないにせよ、EKSの運用を拡大していくことを見据えると、アプリケーション毎に払い出されるエンドポイントの増大が運用上の課題になることが想像できました。
また、Kubernetes Serviceの設定変更を行う度に、BFFサーバへ登録しているgRPCサーバの情報を変更する必要があります。EKSのロードバランシングに関するドキュメントにもある通り、アプリケーション公開の設定を変更する場合にはKubernetes ServiceおよびNLBを再作成する必要があります。
SprocketではBFFサーバーとgRPCサーバーをメンテナンスしているチームが異なっています。リリースの順序やタイミングについてチーム間の調整が必要になることにより、開発速度の低下が懸念されました。

上記を踏まえて、EKS上のアプリケーションに任意かつ恒常的なドメイン名を割り当てる方法を調査しました。

External-DNSとは

NLBにドメイン名を割り当てる場合、Route53レコードを使用するのが一般的です。Route53レコードを作成してKubernetes ServiceやNLBに対応付けることも可能ではありますが、AWSの公式ブログでも紹介されているExternal-DNSに着目しました。(GitHubリンク)
External-DNSはkubernetes-sigsが管理するプロジェクトです。AWSのみならず、各クラウドサービスプロバイダのDNSサービスに対応しており、任意のドメイン名をKubernetes Serviceに割り当てることができます。AWSの場合は、Route53やCloud Mapと連携して動作します。

External-DNSがEKS上のKubernetes Serviceへドメイン名を割り当てる処理の概略は、下記の通りです。

  • External-DNSをEKSにapplyすると、以降に追加されるKubernetes Serviceを監視する。
  • External-DNS用のアノテーションを付与したKubernetes Serviceがapplyされると、Route53ゾーンを参照し、新規レコードを追加する。
  • 追加したRoute53レコードと、Kubernetes Serviceが作成したNLBを対応付ける。

External-DNSの導入手順

GitHubリポジトリ内のチュートリアルに沿って設定を進めますが、インフラ定義はTerraformで記述します。
まずは、Route53ゾーンを作成します。今回のケースではVPC内のみ有効なドメイン名を設定したいため、プライベートホストゾーンとします。

resource "aws_route53_zone" "example" {
  name = "example.com"
  vpc {
    vpc_id = {EKSが所属するVPCのID}
  }
}

次に、予めExternal-DNSに割り当てる権限を作成します。

  • ClusterRoleとして、Kubernetesの各種リソースを参照する権限を付与します。
  • RBACにより、Route53のレコードを参照および操作する権限を付与します。
resource "kubernetes_service_account" "external_dns" {
  metadata {
    name      = "external-dns"
    namespace = "kube-system"
    annotations = {
      "eks.amazonaws.com/role-arn" : aws_iam_role.external_dns_role.arn
    }
  }
}

resource "kubernetes_cluster_role" "external_dns" {
  metadata {
    name = "external-dns"
  }

  rule {
    api_groups = [""]
    resources  = ["services", "endpoints", "pods"]
    verbs      = ["get", "watch", "list"]
  }
  rule {
    api_groups = ["extensions", "networking.k8s.io"]
    resources  = ["ingresses"]
    verbs      = ["get", "watch", "list"]
  }
  rule {
    api_groups = [""]
    resources  = ["nodes"]
    verbs      = ["list", "watch"]
  }
}

resource "kubernetes_cluster_role_binding" "external_dns_viewer" {
  metadata {
    name = "external-dns-viewer"
  }
  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind      = "ClusterRole"
    name      = kubernetes_cluster_role.external_dns.metadata[0].name
  }
  subject {
    kind      = "ServiceAccount"
    name      = kubernetes_service_account.external_dns.metadata[0].name
    namespace = "kube-system"
  }
}

resource "aws_iam_role" "external_dns_role" {
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy_for_kubernetes_service_account.json
}

resource "aws_iam_role_policy_attachment" "attach_external_dns_policy" {
  role       = aws_iam_role.external_dns_role.id
  policy_arn = aws_iam_policy.external_dns.arn
}

resource "aws_iam_policy" "external_dns" {
  policy = data.aws_iam_policy_document.external_dns.json
}

data "aws_iam_policy_document" "external_dns" {
  statement {
    actions   = ["route53:ChangeResourceRecordSets"]
    resources = ["arn:aws:route53:::hostedzone/*"]
  }
  statement {
    actions = [
      "route53:ListHostedZones",
      "route53:ListResourceRecordSets"
    ]
    resources = ["*"]
  }
}

権限を設定するリソースを作成後、External-DNSのDeploymentをapplyします。
コンテナ引数に着目すると、Kubernetes Serviceが制御対象であり、Route53プライベートホストゾーンへのレコードを操作することを示しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: external-dns
  strategy:
    type: Recreate
  replicas: 1
  template:
    metadata:
      labels:
        app.kubernetes.io/name: external-dns
    spec:
      containers:
      - name: external-dns
        image: k8s.gcr.io/external-dns/external-dns:v0.8.0
        args:
        - --provider=aws
        - --source=service
        - --aws-zone-type=private
        - --registry=txt
        - --txt-owner-id={EKSクラスター名}
      securityContext:
        fsGroup: 65534
      serviceAccountName: external-dns

External-DNSが稼働を開始すると、下記のようなログが出力されます。およそ1分間隔でRoute53レコードを参照していることが読み取れます。

time="2021-07-28T00:18:37Z" level=info msg="Instantiating new Kubernetes client"
time="2021-07-28T00:18:37Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2021-07-28T00:18:37Z" level=info msg="Created Kubernetes client https://10.100.0.1:443"
time="2021-07-28T00:18:46Z" level=info msg="All records are already up to date"
time="2021-07-28T00:19:44Z" level=info msg="All records are already up to date"
time="2021-07-28T00:20:45Z" level=info msg="All records are already up to date"
(省略)

External-DNSの設定は以上ですが、ドメイン名の挙動を検証するためにnginxサーバーを設置します。なお、Deploymentは前回の検証時と同一であるため省略します。
アノテーションとしてexternal-dns.alpha.kubernetes.io/hostnameを追加しました。作成したRoute53ホストゾーンに所属するドメイン名として、nginx.example.comを指定しています。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    external-dns.alpha.kubernetes.io/hostname: nginx.example.com
    service.beta.kubernetes.io/aws-load-balancer-type: external
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
  type: LoadBalancer
  ports:
    - port: 80
      name: http
      targetPort: 80
  selector:
    app: nginx

作成されたKubernetes Serviceを参照すると、先程と同様にEXTERNAL-IPへNLBのDNS名が登録されています。

kubectl get svc nginx
NAME    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                       PORT(S)        AGE
nginx   LoadBalancer   10.100.179.105   k8s-external-nginx-5ec9fb0c4f-7ef48fa6f4659076.elb.ap-northeast-1.amazonaws.com   80:32239/TCP   12m

予め作成したRoute53ゾーン(example.com)に紐づくレコードを参照します。アノテーションで指定したnginx.example.comのレコードを作成して、NLBのDNSに対応付けていることが確認できます。

# Route53ゾーンのIDを取得する。
aws route53 list-hosted-zones-by-name --dns-name example.com | jq '.HostedZones[] .Id' -r
/hostedzone/Z017427130ZUXKR22B393

# Route53ゾーンに紐づくレコードのうち、nginx.example.com に関連するものを抽出する。
aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/Z017427130ZUXKR22B393 | jq '.ResourceRecordSets[] | select(.Name == "nginx.example.com.")'
{
  "Name": "nginx.example.com.",
  "Type": "A",
  "AliasTarget": {
    "HostedZoneId": "Z31USIVHYNEOWT",
    "DNSName": "k8s-external-nginx-5ec9fb0c4f-7ef48fa6f4659076.elb.ap-northeast-1.amazonaws.com.",
    "EvaluateTargetHealth": true
  }
}
{
  "Name": "nginx.example.com.",
  "Type": "TXT",
  "TTL": 300,
  "ResourceRecords": [
    {
      "Value": "\"heritage=external-dns,external-dns/owner={EKSクラスター名},external-dns/resource=service/external-dns-test/nginx\""
    }
  ]
}

通信を確認するため、VPC内に設置しているEC2にログインして、nginx.example.comに対してHTTPリクエストを送信します。

curl -I nginx.example.com
HTTP/1.1 200 OK
Server: nginx/1.21.1
Date: Wed, 28 Jul 2021 04:45:14 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 06 Jul 2021 14:59:17 GMT
Connection: keep-alive
ETag: "60e46fc5-264"
Accept-Ranges: bytes

Kubernetes Serviceで指定したドメイン名により、VPC内かつEKSの外部からアプリケーションへ通信できることを確認しました。

まとめ

Kubernetes Serviceにアノテーションを付与することにより、EKS上のアプリケーションを公開できます。しかし、アノテーションにより作成されたNLBをそのまま運用する場合、異なるチームがメンテナンスするサービス開発を進める際に開発速度のボトルネックとなるおそれがあります。そこで、External-DNSを導入することにより、チーム間で予め定めたドメイン名を使用してサービス間で通信を行えました。

Sprocketで働きませんか?

弊社ではカジュアル面談を実施しております。
ご興味を持たれましたら、こちらからご応募お待ちしております。

Sprocketテックブログ

Discussion