Traefik 2とExternalDNSでDNSレコードを自動作成する方法

こんにちは、(株)日立製作所 研究開発グループ サービスコンピューティング研究部の中村です。

kubernetesでWebサービスを公開する場合、L7 Load Balancerとして動作するIngressを利用している方も多いと思います。そして、Webサービスを外部に公開するためにDNSレコードの自動作成をExternalDNSで管理している方も多いと思います。今回は、IngressコントローラとしてTraefik 2とExternalDNSを連携させて、DNSレコードを自動作成する方法について紹介したいと思います。

そもそも、Ingressコントローラは、kubernetesプロジェクトとして標準サポートしているnginx以外にも、以下のようなものがIngressコントローラがあります。

  * [Traefik](https://docs.traefik.io/)
  * [Ambassador](https://www.getambassador.io/)
  * [Voyager](https://appscode.com/products/voyager/v11.0.1/welcome/)
  * [AWS ALB Ingress Controller](https://github.com/kubernetes-sigs/aws-alb-ingress-controller)
  * [Contour](https://projectcontour.io/)
  * など

Traefikは、それ単体でLet's encrypt(ACME)をサポートし証明書更新が自動化される点と、ユーザ認証によるアクセス制限されていないWebアプリをOAuth2.0認証できるようするForward Auth機能を備えている点が他よりも使いやすいと感じており、個人的にはよく利用しています。

また、Traefikは、2019年9月にv2.0が正式にリリースされ、v1系と比較すると、かなり機能が変わりました。
その中でも大きな改善項目は、v1系ではL7 Load Balancerとしてしか動作しなかったのが、v2.0からL4 Load Balancerとして動作するようになった点です。これにより、HTTP(80)/HTTPS(443)のポートだけではなく、それ以外のポートであるSSHやDatabaseをTraefik経由で公開できたり、HTTPS Terminateさせずにbackendにforwardさせるようにできるようになっています。

## 動作環境

以下の環境を前提に説明を進めます。

| 種別    | コンポーネント    | Version           |
|:------:|:----------------|:------------------|
| OS     | MacOS           | Catalina(10.15.1) |
| OSS    | Kubernetes      | 1.16.3            |
| OSS    | kubectl         | 1.16.3            |
| OSS    | ExternalDNS     | 0.8.3             |
| OSS    | Traefik         | 2.1.1             |

## KubernetesクラスタとExternalDNSのセットアップ

こちら([[Mac] ExternalDNS+ローカルCoreDNS/etcdでkind上のServiceを名前解決してみる - Qiita](https://qiita.com/h-nakamura/items/23b0cd13d2611ce046e7))を参考にKubernetessクラスタとExternalDNSをセットアップしてください。

## Traefikのセットアップ

Namespace "kube-system"にTraefikをIngress Controllerとしてデプロイします。以下のようなmanifestファイルを作成し、kubectlでデプロイします。

```yaml:traefik.yaml
    ---
    kind: ServiceAccount
    apiVersion: v1
    metadata:
      name: traefik-ingress-controller
      namespace: kube-system
    ---
    kind: ClusterRole
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: traefik
    rules:
      - apiGroups: [""]
        resources: ["pods", "services", "endpoints", "secrets"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["extensions"]
        resources: ["ingresses"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["extensions"]
        resources: ["ingresses/status"]
        verbs: ["update"]
      - apiGroups: ["traefik.containo.us"]
        resources:
          - ingressroutes
          - ingressroutetcps
          - middlewares
          - tlsoptions
          - traefikservices
        verbs: ["get", "list", "watch"]
    ---
    kind: ClusterRoleBinding
    apiVersion: rbac.authorization.k8s.io/v1
    metadata:
      name: traefik
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: traefik
    subjects:
      - kind: ServiceAccount
        name: traefik-ingress-controller
        namespace: kube-system
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: traefik-ingress-controller
      namespace: kube-system
      labels:
        app: traefik
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: traefik
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 1
          maxSurge: 1
      template:
        metadata:
          labels:
            app: traefik
        spec:
          serviceAccountName: traefik-ingress-controller
          terminationGracePeriodSeconds: 60
          containers:
            - image: traefik:2.1.1
              name: traefik-ingress-controller
              ports:
                - name: traefik
                  containerPort: 8000
                  protocol: TCP
                - name: web
                  containerPort: 80
                  protocol: TCP
                - name: websecure
                  containerPort: 443
                  protocol: TCP
              args:
                - --entryPoints.traefik.address=:8000
                - --entryPoints.web.address=:80
                - --entryPoints.websecure.address=:443
                - --api.dashboard=true
                - --ping=true
                - --providers.kubernetescrd
                - --providers.kubernetesingress
                - --providers.kubernetesingress.ingressendpoint.publishedservice=kube-system/traefik-ingeress-svc
                - --log.level=debug
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: traefik-ingeress-svc
      namespace: kube-system
      labels:
        app: traefik
    spec:
      type: LoadBalancer
      selector:
        app: traefik
      ports:
        - name: traefik
          port: 8000
          targetPort: traefik
        - name: web
          port: 80
          targetPort: web
        - name: websecure
          port: 443
          targetPort: websecure
```

TraefikのPodが起動したか確認してみましょう。

    $ kubectl -n kube-system get pods -l app=traefik
    NAME                                          READY   STATUS    RESTARTS   AGE
    traefik-ingress-controller-64cc9449cb-kgwjl   1/1     Running   0          2m23s

もし、STATUSがRunningとなっていなければ、Runningになるまで待ってください。
また、TraefikのServiceがLoadBalancerとしてデプロイされ、External IPが割り当てられているか確認してみましょう。

    $ kubectl -n kube-system get services
    NAME                   TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                                     AGE
    kube-dns               ClusterIP      10.96.0.10       <none>         53/UDP,53/TCP,9153/TCP                      2d5h
    traefik-ingeress-svc   LoadBalancer   10.107.102.194   172.17.255.1   8000:30403/TCP,80:32367/TCP,443:30214/TCP   8s

次に、Custom Resource Definition(CRD)を利用するので、下記のようなCRDを定義したmanifestファイルを作成して、こちらも忘れずにデプロイします。

```yaml:traefik-crd.yaml
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: ingressroutes.traefik.containo.us
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: IngressRoute
        plural: ingressroutes
        singular: ingressroute
      scope: Namespaced
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: ingressroutetcps.traefik.containo.us
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: IngressRouteTCP
        plural: ingressroutetcps
        singular: ingressroutetcp
      scope: Namespaced
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: middlewares.traefik.containo.us
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: Middleware
        plural: middlewares
        singular: middleware
      scope: Namespaced
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: tlsoptions.traefik.containo.us
    spec:
      group: traefik.containo.us
      version: v1alpha1
      names:
        kind: TLSOption
        plural: tlsoptions
        singular: tlsoption
      scope: Namespaced
    ---
    apiVersion: apiextensions.k8s.io/v1beta1
    kind: CustomResourceDefinition
    metadata:
      name: traefikservices.traefik.containo.us
    spec:
      group: traefik.containo.us
    version: v1alpha1
      names:
        kind: TraefikService
        plural: traefikservices
        singular: traefikservice
      scope: Namespaced
```

## TraefikでのEndpoint公開方法

サービスを外部に公開する際、Traefik 2では以下２通りの方法を提供しています。

  1. Kubernetes標準のIngressリソースで公開する
  2. Traefik独自CRD(IngressRoute/IngressTCPRoute)リソースで公開する

以下、具体例を元に詳細について紹介したいと思います。

## 1. Ingressリソースで公開する

単純なWebサービスをDeploymentとしてデプロイして、Ingressリソースで外部からアクセスできるようにしてみたいと思います。
今回は、Webサービスとして、[paulbouwer/hello-kubernetes](https://github.com/paulbouwer/hello-kubernetes)というDockerイメージを利用しました。
以下のようなmanifestファイルを作成し、kubectlでデプロイします。

```yaml:helloworld-ingress.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: helloworld-ingress
    spec:
      selector:
        matchLabels:
          app: helloworld-ingress
      template:
        metadata:
          labels:
            app: helloworld-ingress
        spec:
          containers:
            - name: helloworld-ingress
              image: paulbouwer/hello-kubernetes:1.5
              ports:
                - name: http
                  containerPort: 8080
              env:
                - name: MESSAGE
                  value: This is defined by an Ingress resource!
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: helloworld-ingress-svc
    spec:
      ports:
        - name: http
          port: 80
          protocol: TCP
          targetPort: http
      selector:
        app: helloworld-ingress
    ---
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      name: helloworld-ingress
      annotations:
        kubernetes.io/ingress.class: "traefik"
    spec:
      rules:
        - host: helloworld-ingress.example.com
          http:
            paths:
              - path: /
                backend:
                  serviceName: helloworld-ingress-svc
                  servicePort: 80
```

TraefikをIngress Controllerとして、Kubernetes標準のIngressリソースを定義するには、annotationに`kubernetes.io/ingress.class: "traefik"`を忘れずに追加してください。

### 動作確認

Ingressリソースが作成で来ているか確認してみます。

    $ kubectl get ingress helloworld-ingress
    NAME                 HOSTS                            ADDRESS        PORTS   AGE
    helloworld-ingress   helloworld-ingress.example.com   172.17.255.1   80      28s

Ingressリソースが作成され、TraefikのServiceにアサインされたExternal IPと同じアドレスがADDRESSに割り当てられています。
次に、ExternalDNSのログを確認してみます。

    $ kubectl -n kube-system logs external-dns-56c79c6b69-ptzsr
    〜〜〜
    time="2019-12-20T00:17:20Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T00:17:20Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T00:17:20Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T00:17:20Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T00:17:20Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T00:17:20Z" level=info msg="Add/set key /skydns/com/example/helloworld-ingress/30e3a08a to Host=172.17.255.1, Text=\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/helloworld-ingress\", TTL=0"
    〜〜〜

Ingressリソースを検出し、DNSレコードが登録されたログが確認できました。
それでは、実際にアクセスしてみます。ブラウザを開き、アドレスバーに"http\://helloworld-ingress.example.com"と入力してみます。

<img width="1681" alt="helloworld-ingress.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/b18608e3-e3c2-6e6e-e8cb-3247f00b1c07.png">

無事にアクセスできました。

## 2. IngressRouteで公開する

次に、Traefik 2.0より追加されたCustom Resource Definition(CRD)であるIngressRouteを使って公開してみたいと思います。
先程のIngressで公開した時と同じWebサービスをDeploymentでデプロイし、TraefikのCRDであるIngressRouteを使って定義してみます。
具体的には、以下のようなmanifestファイルを作成し、デプロイします。

```yaml:helloworld-ingressroute.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: helloworld-ingressroute-crd
    spec:
    selector:
      matchLabels:
        app: helloworld-ingressroute-crd
    template:
      metadata:
        labels:
            app: helloworld-ingressroute-crd
      spec:
        containers:
          - name: helloworld-ingressroute-crd
            image: paulbouwer/hello-kubernetes:1.5
            ports:
              - name: http
                containerPort: 8080
            env:
              - name: MESSAGE
                value: This is defined by an IngressRoute resource!
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: helloworld-ingressroute-crd-svc
    spec:
      ports:
        - name: http
          port: 80
          protocol: TCP
          targetPort: http
      selector:
        app: helloworld-ingressroute-crd
    ---
    apiVersion: traefik.containo.us/v1alpha1
    kind: IngressRoute
    metadata:
      name: helloworld-ingressroute-crd
    spec:
      entryPoints:
        - web
      routes:
        - match: Host(`helloworld-ingressroute.example.com`)
          kind: Rule
          services:
            - name: helloworld-ingressroute-crd-svc
              port: 80
```

実際にIngressRouteリソースが作成されたか確認してみます。

    $ kubectl describe ingressroute helloworld-ingressroute-crd
    Name:         helloworld-ingressroute-crd
    Namespace:    default
    Labels:       <none>
    Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                    {"apiVersion":"traefik.containo.us/v1alpha1","kind":"IngressRoute","metadata":{"annotations":{},"name":"helloworld-ingressroute-crd","name...
    API Version:  traefik.containo.us/v1alpha1
    Kind:         IngressRoute
    Metadata:
    Creation Timestamp:  2019-12-20T00:43:22Z
    Generation:          1
    Resource Version:    108873
    Self Link:           /apis/traefik.containo.us/v1alpha1/namespaces/default/ingressroutes/helloworld-ingressroute-crd
    UID:                 ad90f795-0dc5-453d-af6c-27340737cbb1
    Spec:
    Entry Points:
        web
    Routes:
        Kind:   Rule
        Match:  Host(`helloworld-ingressroute.example.com`)
        Services:
        Name:  helloworld-ingressroute-crd-svc
        Port:  80
    Events:      <none>

正常に作成できているようです。
それでは、ExternalDNSのログをみてみましょう。

    $ kubectl -n kube-system logs external-dns-56c79c6b69-ptzsr
    〜〜〜
    time="2019-12-20T00:39:19Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T00:39:19Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T00:39:19Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T00:39:19Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T00:39:19Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T00:39:32Z" level=debug msg="pod added"
    time="2019-12-20T00:39:32Z" level=debug msg="service added"
    time="2019-12-20T00:40:19Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T00:40:19Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T00:40:19Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T00:40:19Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T00:40:19Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingressroute-crd-svc"
    time="2019-12-20T00:40:19Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    〜〜〜

Serviceリソースが追加されていることは検知できているようですが、IngressRouteリソースの追加が検知できていないので、DNSレコードの作成がされていないようです。
実際に、curlでアクセスして確認してみましょう。

    $ curl http://helloworld-ingressroute.example.com
    curl: (6) Could not resolve host: helloworld-ingressroute.example.com

DNSで名前解決できていないようですね。

### ExternalDNSがサポートしているリソース

なぜExternalDNSで自動でDNSレコードができなかったのか考えてみたいと思います。
ExternalDNSの[Sourceに関するドキュメント](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/contributing/sources-and-providers.md#sources)を確認してみましょう。ExternalDNSでサポートしているのは、以下の通りです。

  * ServiceSource
  * IngressSource
  * IstioGatewaySource
  * ContourIngressRouteSource
  * FakeSource
  * ConnectorSource
  * CRDSource
  * EmptySource

これを見ると、CRDもサポートしているように見えます。どのようなものか詳細を[こちら](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/contributing/crd-source.md)で確認してみましょう。

どうも、CRDとしてサポートしているデータ構造は、特定のもののようです。また、ExternalDNSのIssueで調べてみても、[こちら](https://github.com/kubernetes-sigs/external-dns/issues/1257)のように、TraefikのIngressRouteはサポートされていないようです。これはTraefikに限らず、その他のCRDを使ってサービス公開を行うIngress Controller(例えば、AmbassadorやVoyager)も同様の問題にぶつかってしまうと思われます。

### 解決策

現状、どのような解決策があるか整理してみたいと思います。

  1. ExternalDNSのCRDSourceを使ってDNSを定義する
  2. Serviceリソース(ExternalName)を使ってDNSを定義する

１つ目は、ExternalDNSのIssueでも推奨されていましたが、ExternalDNSのDNS定義専用のCRD("DNSEndpoint")を用いる方法です。
２つ目は、ExternalDNSがサポートしているServiceリソースのtypeを[ExternalName](https://kubernetes.io/docs/concepts/services-networking/service/#externalname)を用いて、DNSを定義する方法です。
以下、実際に、その手順を紹介したいと思います。

#### (解決策1) ExternalDNSのCRDSourceを使ってDNSを定義する

[ExternalDNSのドキュメント](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/contributing/crd-source.md#example)を参考に、進めていきたいと思います。
まずは、CRDを定義した以下の様なmanifestファイルを作成します。

```yaml:dnsendpoint-crd.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  creationTimestamp: null
  labels:
    api: externaldns
    kubebuilder.k8s.io: 1.0.0
  name: dnsendpoints.externaldns.k8s.io
spec:
  group: externaldns.k8s.io
  names:
    kind: DNSEndpoint
    plural: dnsendpoints
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      properties:
        apiVersion:
          type: string
        kind:
          type: string
        metadata:
          type: object
        spec:
          properties:
            endpoints:
              items:
                properties:
                  dnsName:
                    type: string
                  labels:
                    type: object
                  providerSpecific:
                    items:
                      properties:
                        name: 
                          type: string
                        value:
                          type: string
                      type: object
                    type: array
                  recordTTL:
                    format: int64
                    type: integer
                  recordType:
                    type: string
                  targets:
                    items:
                      type: string
                    type: array
                type: object
              type: array
          type: object
        status:
          properties:
            observedGeneration:
              format: int64
              type: integer
          type: object
  version: v1alpha1
status:
  acceptedNames:
    kind: ""
    plural: ""
  conditions: null
```

これをkubectlでデプロイします。デプロイする際には、validateが無効になるオプションをつけて実行します。

    $ kubectl apply --validate=false -f dnsendpoint-crd.yaml
    customresourcedefinition.apiextensions.k8s.io/dnsendpoints.externaldns.k8s.io created

次に、ExternalDNSにRBACを設定している場合は、先程追加したCRDに対するアクセス権を付与する以下のmanifestファイルを作成し、適用します。

```yaml:update-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns-crd
rules:
- apiGroups: ["externaldns.k8s.io"]
  resources: ["dnsendpoints"]
  verbs: ["get","watch","list","update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: external-dns-crd-manager
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns-crd
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: kube-system
```

そして、追加したCRDをExternalDNSが検出できるように、ExternalDNSの起動時のオプションを追加します。以下のようなmanifestファイルを作成し、ExternalDNSのDeploymentをUpdateします。

```yaml:update-extenaldns.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: kube-system
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.opensource.zalan.do/teapot/external-dns:latest
        args:
        - --source=service
        - --source=ingress
        - --provider=coredns
        - --log-level=debug
        - --policy=sync
        - --domain-filter=example.com
        - --source=crd                                        # <--- 追加
        - --crd-source-apiversion=externaldns.k8s.io/v1alpha1 # <--- 追加
        - --crd-source-kind=DNSEndpoint                       # <--- 追加
        env:
        - name: ETCD_URLS
          value: http://host.docker.internal:2379
```

最後に、追加したCRD"DNSEndpoint"を使って、DNSを以下のように定義して、kubectlでデプロイします。なお、最後のtargetsには、Ingress ControllerであるTraefikのServiceに割り当てられたExternal IPを指定します。

```yaml:helloworld-dns.yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
  name: helloworld-ingressroute-dnsrecord
spec:
  endpoints:
  - dnsName: helloworld-ingressroute.example.com
    recordTTL: 180
    recordType: A
    targets:
    - 172.17.255.1
```

それでは、確認してみましょう。まずは、DNSEndpointリソースが作成されているか確認します。

    $ kubectl get dnsendpoint
    NAME                                AGE
    helloworld-ingressroute-dnsrecord   21s

次に、ExternalDNSのログを確認してみます。

    $ kubectl -n kube-system logs external-dns-6f59d484c-nh6zv
    〜〜〜
    time="2019-12-20T01:54:14Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T01:54:14Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T01:54:14Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T01:54:14Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T01:54:14Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingressroute-crd-svc"
    time="2019-12-20T01:54:14Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T01:54:14Z" level=warning msg="Could not update ObservedGeneration of the CRD: dnsendpoints.externaldns.k8s.io \"helloworld-ingressroute-dnsrecord\" is forbidden: User \"system:serviceaccount:kube-system:external-dns\" cannot update resource \"dnsendpoints/status\" in API group \"externaldns.k8s.io\" in the namespace \"default\""
    time="2019-12-20T01:54:14Z" level=info msg="Add/set key /skydns/com/example/helloworld-ingressroute/089779ea to Host=172.17.255.1, Text=\"heritage=external-dns,external-dns/owner=default,external-dns/resource=crd/default/helloworld-ingressroute-dnsrecord\", TTL=180"
    〜〜〜

CRDの情報を元に、DNSレコードが追加されたようです。
早速、ブラウザからアクセスできるか確認してみましょう。

<img width="1681" alt="helloworld-ingressroute-crd.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/3e548631-208c-1f86-91fa-13701284d844.png">


無事にアクセスできました。
別の解決策を試すために、一旦、追加したDNSEndpointリソースを削除します。

    $ kubectl delete dnsendpoint helloworld-ingressroute-dnsrecord
    dnsendpoint.externaldns.k8s.io "helloworld-ingressroute-dnsrecord" deleted

１分程度待ってからcurlでアクセスすると、以下のようにDNSの名前解決に失敗すると思います。

    $ curl http://helloworld-ingressroute.example.com
    curl: (6) Could not resolve host: helloworld-ingressroute.example.com

#### (解決策2) Serviceリソース(ExternalName)を使ってDNSを定義する

今度は、Kubernetes標準のServiceリソースだけでDNSを定義したいと思います。
まずは、以下のようなServiceリソースを定義します。これは、サービスを公開するときに作成しているServiceとは別に、DNSレコード登録用のServiceリソースになるので、名前が重複しないように気をつけて作成してください。

```yaml:helloworld-external-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: hellowrold-ingressroute-svc-external
  annotations:
    external-dns.alpha.kubernetes.io/hostname: helloworld-ingressroute.example.com  <-- 3
spec:
  type: ExternalName                     <-- 1
  externalName: 172.17.255.1             <-- 2
```

ポイントは以下の３点です。

  1. typeをExternalNameと指定
  2. externalNameに、Ingress ControllerであるTraefikのServiceに割り当てられたExternal IPを指定
  3. annotationにDNSとして登録したいhostnameを記載

この作成したmanifestファイルをデプロイすればOKです。

早速、確認してみましょう。
まずは、Serviceリソースが作成されているかを確認します。

    $ kubectl get service
    NAME                                   TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)   AGE
    helloworld-ingress-svc                 ClusterIP      10.98.240.223   <none>         80/TCP    124m
    helloworld-ingressroute-crd-svc        ClusterIP      10.103.54.127   <none>         80/TCP    101m
    hellowrold-ingressroute-svc-external   ExternalName   <none>          172.17.255.1   <none>    7s
    kubernetes                             ClusterIP      10.96.0.1       <none>         443/TCP   2d7h

typeがExternalNameのServiceリソースが作成されているようです。
次に、ExternalDNSのログを確認します。

    $ kubectl -n kube-system logs external-dns-6f59d484c-nh6zv
    〜〜〜
    time="2019-12-20T02:20:13Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T02:20:13Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T02:20:13Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T02:20:13Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T02:20:13Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingressroute-crd-svc"
    time="2019-12-20T02:20:13Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T02:20:41Z" level=debug msg="service added"
    time="2019-12-20T02:21:12Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingressroute-crd-svc"
    time="2019-12-20T02:21:12Z" level=debug msg="Endpoints generated from service: default/hellowrold-ingressroute-svc-external: [helloworld-ingressroute.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T02:21:12Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-20T02:21:12Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-20T02:21:12Z" level=debug msg="No endpoints could be generated from service kube-system/traefik-ingeress-svc"
    time="2019-12-20T02:21:12Z" level=debug msg="No endpoints could be generated from service default/helloworld-ingress-svc"
    time="2019-12-20T02:21:12Z" level=debug msg="Endpoints generated from ingress: default/helloworld-ingress: [helloworld-ingress.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-20T02:21:12Z" level=info msg="Add/set key /skydns/com/example/helloworld-ingressroute/671dbad1 to Host=172.17.255.1, Text=\"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/hellowrold-ingressroute-svc-external\", TTL=0"
    〜〜〜

Serviceの追加が検出され、DNSレコードが登録されたようです。
最後に、ブラウザからアクセスできるか確認してみます。

<img width="1681" alt="helloworld-ingressroute-crd2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/3737812f-d827-3bbc-0bdb-e574b54e810b.png">


先程と全く同じ画面が表示されることが確認できました。

## まとめ

IngressコントローラとしてTraefik 2とExternalDNSを連携させて、DNSレコードを自動作成する方法について紹介しました。Kubernetes標準のIngressリソースの場合は特に問題はなかったのですが、Traefik独自のCRDを用いた場合は、そのままではExternalDNSが追加されたCRDを検出できずDNSレコードの作成がされないという問題があり、その解決方法を２つ紹介しました。

ご紹介した２つの解決方法はどちらも、Ingress ControllerのServiceにアサインされているExternal IPを手作業で埋める必要があるので、運用は煩雑になってしまいます。
この問題は、Traefikのコミュニティでも認識されており、[これに関するIssue](https://github.com/containous/traefik/issues/4655)が登録されています。早く改善されることを望みながら、この動向をWatchしていきたいと思います。