EKSとRoute 53をExternalDNSで紐付ける

# はじめに
個人で作成しているサービスにて、ExternalDNSを使って、Route 53からEKSへアクセスできるようになりましたので、ここまでの知見を記事に残します。

このアクセスができる前までは、TerraformでAWSのサービスを構築し、マニフェストファイルにてPod、LoadBalancerの作成までできていました。
このときの構成図は以下のとおりです。
![WeatherReport_BeforeExternalDNS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/f0f58c6e-627e-3b4e-b9c7-d191cb02241a.jpeg)

この構成では、EKSで作成したALBが公開したURLを直接指定することで、サービスにアクセスできます。ただしそのURLは、LoadBalancerで作成されるためランダムになってしまいます。
そのためRoute 53経由でEKSへアクセスできるようにしようと考えました。

# 前提
以下環境で開発、構築を行っています。

- Kubernetes: v1.16
- Terraform: v12.28
- Route 53でドメイン取得済み

Route 53でドメイン取得時、以下のようにNSタイプ、SQAタイプのみ存在していました。
![Route53_NS_SQA_Record.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/a623c2f3-74ba-336c-f453-73d1d439c29b.png)

# ExternalDNSを使用してEKSとRoute 53の紐付け
こちらの記事「[Kubernetesを学ぶ意味とは――AWS EKSでクラスタを構築してみよう (1/4)](https://www.atmarkit.co.jp/ait/articles/1912/06/news011.html)」、「[AWS EKSでDNSレコードの作成／変更を楽にする「ExternalDNS」 (1/4)](https://www.atmarkit.co.jp/ait/articles/2002/17/news006.html)」を参考にして対応しました。
基本的には上記の記事どおりなのですが、この記事ではTerraformを使った場合や、記事公開時と異なる点に対する対応を載せて説明します。

## ExternalDNSについて
[ExternalDNSの公式サイト](https://github.com/kubernetes-sigs/external-dns)によると、ExternalDNSはKubernetesのサービスやIngressをDNSのプロバイダーと同期してくれるツールです。
> ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.

この記事でのDNSのプロバイダーはRoute 53となるため、ExternalDNSにて、前提項目でのKubernetesのLoadBalancerとRoute 53を紐付けることを可能にしてくれます。

## IAMの設定
参考記事では、以下ロールやポリシーの作成やアタッチを、直接AWSマネジメントコンソール上で行っています。

1. Route 53に対するポリシーを作成する。
1. ExternalDNS用IAMロールの信頼関係にセットする信頼ポリシーを作成する。
この信頼ポリシーにはEKSのワーカーノードのARNをセットする。
1. ExternalDNS用のIAMロールを作成し、1のポリシー、2の信頼ポリシーをセットする。

ここでは上記1~3をTerraformで行った場合を載せます。なお、3でEKSのワーカーノードのARNが必要になるため、こちらもTerraformでワーカーノード用のIAMロールを作成しておきます。
ロールの作成やポリシーのアタッチについては、こちらの記事「[【連載】terraform によるAWS環境構築入門 第２回 ~ 権限管理とモジュール化 ~](https://semigeek-dev.hatenablog.com/entry/2019/05/04/225746)」を参考にしました。

```iam.tf
# 1の対応
resource "aws_iam_policy" "route53-external-policy" {
  name = "route53-external-policy"
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets"
      ],
      "Resource": [
        "arn:aws:route53:::hostedzone/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
EOF
}

# EKSのワーカーノードのIAMロール作成
resource "aws_iam_role" "eks_node_group_iam_role" {
  name = "eks-node-group-iam-role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

# 2の対応
data "aws_iam_policy_document" "route53-externaldns-policy" {
  statement {
    effect = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }

  statement {
    effect = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type = "AWS"
      #identifiersにEKSワーカーノードのarnをセットする。
      identifiers = ["${aws_iam_role.eks_node_group_iam_role.arn}"]
    }
  }
}

# 3の対応
resource "aws_iam_role" "route53-externaldns-controller" {
  name = "route53-externaldns-controller" 
  assume_role_policy = data.aws_iam_policy_document.route53-externaldns-policy.json
}


# 3の対応
resource "aws_iam_role_policy_attachment" "route53-externaldns-attachment"{
  role = aws_iam_role.route53-externaldns-controller.name
  policy_arn = aws_iam_policy.route53-external-policy.arn
}
```

## マニフェストファイルの作成
### kube2iamとは
[kube2iam](https://github.com/jtblin/kube2iam)の公式ページでは、「アノテーションに基づいて、IAMクレデンシャルをKubernetesのクラスター内で実行しているコンテナに与える」とあります。
> Provide IAM credentials to containers running inside a kubernetes cluster based on annotations.

また参考にしている記事では、以下に記載してあるように、アプリケーションに対して必要なアクセス権を与えられるとあります。

> アクセス権限が必要ないアプリケーションに対してアクセス権限が付与されることを防ぐために、クラスタ内で起動しているアプリケーションそれぞれに対して最低限必要なアクセス権限を与えられる「kube2iam」

### kube2iamの作成
さっそくkube2iamのマニフェストファイルを作成します。このファイルは[参考記事](https://www.atmarkit.co.jp/ait/articles/1912/06/news011_4.html)や[公式ページ](https://github.com/jtblin/kube2iam/blob/0.10.6/examples/eks-example.yml)で紹介されているので、そのファイルを参考に作成します。

```kube2iam.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kube2iam
  namespace: kube-system
---
apiVersion: v1
items:
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: kube2iam
    rules:
      - apiGroups: [""]
        resources: ["namespaces","pods"]
        verbs: ["get","watch","list"]
  - apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: kube2iam
    subjects:
    - kind: ServiceAccount
      name: kube2iam
      namespace: kube-system
    roleRef:
      kind: ClusterRole
      name: kube2iam
      apiGroup: rbac.authorization.k8s.io
kind: List
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube2iam
  namespace: kube-system
  labels:
    app: kube2iam
spec:
  selector:
    matchLabels:
      app: kube2iam
  template:
    metadata:
      labels:
        app: kube2iam
    spec:
      serviceAccountName: kube2iam
      hostNetwork: true
      containers:
        - name: kube2iam
          image: jtblin/kube2iam:latest
          imagePullPolicy: Always
          args:
            - "--auto-discover-base-arn"
            - "--iptables=true"
            - "--host-ip=$(HOST_IP)"
            - "--host-interface=eni+"
            - "--verbose"
          env:
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          ports:
            - containerPort: 8181
              hostPort: 8181
              name: http
          securityContext:
            privileged: true
```

### ExternalDNSの作成
記事を参考にしたマニフェストファイルを作成した際、以下2点の問題が発生していました。

1. DeploymentのapiVersionが```apiVersion: extensions/v1beta1```だが、2020/08/10時点で```extensions/v1beta1```は使用不可となっている。
1. ClusterRoleにapiGroupが不足しているため、ExternalDNSのマニフェストファイルapply時に```failed to sync cache: timed out waiting for the condition```が発生する。

1については、apiVersionをapps/v1に変更し、不足しているselectorを追加します。
2については、現象発生時の実際のログは以下のとおりです。

```
time="2020-08-10T12:39:22Z" level=info msg="Instantiating new Kubernetes client"
time="2020-08-10T12:39:22Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2020-08-10T12:39:22Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2020-08-10T12:40:22Z" level=fatal msg="failed to sync cache: timed out waiting for the condition"
```
この問題についてExternalDNSに[issue](https://github.com/kubernetes-sigs/external-dns/issues/961)があがっていました。このissueによると、endpointのリソースを持つapiGroupsを加えることで解決できるようです。

> After I recreated some nodes, external-dns failed to startup again. It failed after printing the error message "failed to sync cache: timed out waiting for the condition". It seems that endpoints were added and external-dns now requires extra permissions.

>Make sure you have added

> ```
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get","watch","list"]
```
to your external-dns ClusterRole. Adding this solved the problem for me.

1、2を含めたマニフェストファイルは以下のとおりです。
また、Deploymentの```iam.amazonaws.com/role```にて、ExternalDNSのIAMロール(route53-externaldns-controller)のARNが必要になります。そのためARNを取得し```iam.amazonaws.com/role```にセットしておきます。

```external-dns-manifest.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
  resources: ["ingresses"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list","watch"]
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: external-dns
  name: external-dns
spec:
  selector:
    matchLabels:
      app: external-dns
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: external-dns
      annotations:
        iam.amazonaws.com/role: $ROUTE53_EXDNS_CONTROLLER_ARN
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:latest
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=the-weather-report.com
        - --provider=aws
        - --policy=sync
        - --aws-zone-type=public
        - --registry=txt
        - --txt-owner-id=example-identifier
```

## マニフェストファイルの適用
上記項目で作成したマニフェストファイルをapplyします。
なお、deployment.yamlはVue、Ruby on Railsを1つのpodとして作成したマニフェストファイルです。

```
kubectl apply -f ./kube2iam/kube2iam.yaml
kubectl apply -f ./deployment.yaml
kubectl apply -f ./external-dns-manifest.yaml
```

PodがRunningになっていること、ServiceのLoadBalancerにEXTERNAL-IPが割り当てられていることを確認します。

```
$ kubectl get pods
NAME                                         READY   STATUS      RESTARTS   AGE
external-dns-xxxxx                           1/1     Running     0          39s
・・・略・・・

$ kubectl get services
NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP                                                                  PORT(S)        AGE
kubernetes              ClusterIP      xxx.xxx.xxx.xxx  <none>                                                                       443/TCP        13m
weatherreport-service   LoadBalancer   xxx.xxx.xxx.xxx ![Route53_Mosaic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/ab4afb9a-a8aa-b3b7-4bd1-af51bf9f0632.jpeg)
 xxxxx.ap-northeast-1.elb.amazonaws.com   80:31619/TCP   61s
```

ExternalDNSのpodのログを確認し、```All records are already up to date```が表示されていれば成功です。

```
$ kubectl logs external-dns-xxxxx
time="2020-08-11T15:59:20Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2020-08-11T15:59:29Z" level=info msg="Desired change: UPSERT the-weather-report.com A [Id: /hostedzone/Z11CZOCGRGWAHV]"
time="2020-08-11T15:59:29Z" level=info msg="Desired change: UPSERT the-weather-report.com TXT [Id: /hostedzone/Z11CZOCGRGWAHV]"
time="2020-08-11T15:59:30Z" level=info msg="2 record(s) in zone the-weather-report.com. [Id: /hostedzone/Z11CZOCGRGWAHV] were successfully updated"
time="2020-08-11T16:00:29Z" level=info msg="All records are already up to date"
```

マネジメントコンソールでRoute 53を見てみると、取得したドメインにAレコード、TXTレコードが作成されていることが分かります。
このAレコードのドメインにアクセスするとアプリケーションが表示されます。
![Route53_Mosaic.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/2eb67e7d-7b23-ce7c-c421-aad975367a40.jpeg)

ExternalDNSを使ってRoute 53からアクセスできる構成図は以下のとおりです。
![WeatherReport_AfterExternalDNS.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406773/5373c73c-e651-f75e-e15c-cc901ab42b69.jpeg)


# おわりに
ExternalDNSを使用し、EKSとRoute 53を紐付ける方法を説明しました。
ExternalDNSを最初見たときには何をやっているのだろうかと疑問でしたが、実際に動かし説明を読むことで、理解が深まりました。
この記事が誰かのお役に立てれば幸いです。