Kubernetes: обновление DNS в Route53 при создании Ingress

Автор: | 14/11/2020

Задача: при создании Ingress ресурса – создавать запись на DNS, которая будет привязана к URL создаваемого Ingress, потому что сейчас это приходится делать руками для каждого нового Application Load Balancer, который создаётся из Ingress через ALB Ingress controller.

Для решения – используем ExternalDNS, который будет ходить в наш AWS Route53, и добавлять записи.

Документация на установке в AWS – тут>>>.

Настройка AWS

IAM Policy

Политику создадим с доступом только к одной зоне, так как работу ExternalDNS ещё не проверял, и давать полный доступ к Route53 пока не хочется.

Переходим в Route53, находим ID зоны:

Переходим в IAM > Policies, создаём новую политику:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets"
      ],
      "Resource": [
        "arn:aws:route53:::hostedzone/Z07***ZM6"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "route53:ListHostedZones",
        "route53:ListResourceRecordSets"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Сохраняем:

Находим, копируем её ARN:

IAM Role

Наш AWS EKS кластер создавался с помощью ekctl, см AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl.

Подключем OIDC:

[simterm]

$ eksctl utils associate-iam-oidc-provider --region=us-east-2 --cluster=bttrm-eks-dev-1 --approve

[/simterm]

Создаём ServiceAccount:

[simterm]

$ eksctl --profile arseniy create iamserviceaccount --name external-dns --cluster bttrm-eks-dev-1 --attach-policy-arn arn:aws:iam::534***385:policy/AllowExternalDNSUpdates --approve --override-existing-serviceaccounts

[/simterm]

Переходим в CloudFormation, находим стек, созданный eksctl, и в нём – роль:

Копируем ARN роли:

ExternalDNS

Проверяем есть ли RBAC в кластере:

[simterm]

$ kubectl api-versions | grep rbac.authorization.k8s.io
rbac.authorization.k8s.io/v1
rbac.authorization.k8s.io/v1beta1

[/simterm]

Создаём деплоймент – см. Manifest (for clusters with RBAC enabled), в аннотиации к ServiceAccount указываем нашу роль, в --domain-filter – наш домен example.com, т.к. мы всё ещё хотим тестировать только на одном домене, а не всех доменах в AWS аккаунте:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  # If you're using Amazon EKS with IAM Roles for Service Accounts, specify the following annotation.
  # Otherwise, you may safely omit it.
  annotations:
    # Substitute your account ID and IAM service role name below.
    eks.amazonaws.com/role-arn: arn:aws:iam::534***385:role/eksctl-bttrm-eks-dev-1-addon-iamserviceaccou-Role1-LOQOWXLJ8SD3
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
- apiGroups: [""]
  resources: ["services","endpoints","pods"]
  verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get","watch","list"]
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["list","watch"]
---
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:
  name: external-dns
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: k8s.gcr.io/external-dns/external-dns:v0.7.3
        args:
        - --source=service
        - --source=ingress
        - --domain-filter=example.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones
        - --provider=aws
        - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization
        - --aws-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both)
        - --registry=txt
        - --txt-owner-id=bttrm-eks-dev-1-external-dns
      securityContext:
        fsGroup: 65534 # For ExternalDNS to be able to read Kubernetes and AWS token files

Нигде в документации толком не сказано, что за опция “txt-owner-id” – она определяет значения TXT-записи, которая создаётся ExternalDNS для записей, которые он создаёт, что бы отменить их как “свои”, т.е. те, которые управляются им.

Если, к примеру, у вас в домене уже есть запись subdomain.example.com, и к ней нет TXT, которую добавляет ExternalDNS, то при создании Ingress с host: subdomain.example.com – ExternalDNS ничего с существующей записью не сделает.

Увидим работу с TXT чуть позже в работе.

Деплоим:

[simterm]

$ kubectl apply -f ~/Work/Temp/EKS/external-dns-deployment.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
serviceaccount/external-dns configured
clusterrole.rbac.authorization.k8s.io/external-dns created
clusterrolebinding.rbac.authorization.k8s.io/external-dns-viewer created
deployment.apps/external-dns created

[/simterm]

Проверяем под:

[simterm]

$ kubectl get pod -l app=external-dns
NAME                           READY   STATUS    RESTARTS   AGE
external-dns-75894b84b-2qnr5   1/1     Running   0          2m2s

[/simterm]

Kubernetes Ingress и AWS Application LoadBalancer

И проверяем работу ExternalDNS.

Создаём Deployment и Service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
  labels:
    app: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      dnsPolicy: None
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc
spec:
  type: NodePort
  selector:
    app: test
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Создаём Ingress, в аннотации external-dns.alpha.kubernetes.io/hostname указываем домен (или в spec.rules.host):

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
    external-dns.alpha.kubernetes.io/hostname: test-dns.example.com
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: test-svc
          servicePort: 80

Деплоим ресурсы:

[simterm]

$ kubectl apply -f ~/Work/devops-kubernetes/tests/test-deployment.yaml 
deployment.apps/test-deployment created
service/test-svc created
ingress.extensions/test-ingress created

[/simterm]

Проверяем Ingress:

[simterm]

$ kubectl get ingress test-ingress
NAME           HOSTS   ADDRESS                                                                  PORTS   AGE
test-ingress   *       e172ad3e-default-testingre-5bb0-1920769979.us-east-2.elb.amazonaws.com   80      28s

[/simterm]

Логи пода:

[simterm]

$ kubectl logs external-dns-75894b84b-2qnr5
...
time="2020-11-13T12:31:13Z" level=info msg="Desired change: CREATE test-dns.example.com A [Id: /hostedzone/Z07***ZM6]"
time="2020-11-13T12:31:13Z" level=info msg="Desired change: CREATE test-dns.example.com TXT [Id: /hostedzone/Z07***ZM6]"
time="2020-11-13T12:31:13Z" level=info msg="2 record(s) in zone example.com. [Id: /hostedzone/Z07***ZM6] were successfully updated"

[/simterm]

Проверяем DNS:

Обратите внимание на TXT со значением “heritage=external-dns,external-dns/owner=bttrm-eks-dev-1-external-dns,external-dns/resource=ingress/default/test-ingress” – вот тут и используется значение параметра txt-owner-id из Deployment самого ExternalDNS.

Проверяем работу домена:

[simterm]

$ dig test-dns.example.com +short
3.133.54.4
13.59.209.195

$ curl -I test-dns.example.com.com
HTTP/1.1 200 OK

[/simterm]

Обновление записей и root-level домен

Для того, что бы ExternalDNS мог обновлять и/или удалять записи – убираем – --policy=upsert-only в его Deployment, передеплоиваем.

И нюанс с root-leve  доменом – для него не должно быть задано TXT, иначе ExternalDNS не сможет его обновить. Более того – не должно быть старой записи example.com вообще. Хотя можно попробовать добавить к ней TXT-запись – тогда, по идее, ExternalDNS сочтёт запись “своей”, и изменит её.

В нашем случае когда-то была создана TXT-запись Google верификации – её пришлось удалить, благо домен тестовый.

Удаляем её, обновляем Ingress, указываем и субдомен, и корневой домен:

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/inbound-cidrs: 0.0.0.0/0
    external-dns.alpha.kubernetes.io/hostname: test-dns.example.com, example.com
spec:
  rules:
  - http:
      paths:
      - backend:
          serviceName: test-svc
          servicePort: 80

Применяем, и проверяем Ingress:

[simterm]

$ kubectl get ingress test-ingress
NAME           HOSTS   ADDRESS                                                                 PORTS   AGE
test-ingress   *       e172ad3e-default-testingre-5bb0-456442783.us-east-2.elb.amazonaws.com   80      30m

[/simterm]

Проверяем логи:

[simterm]

time="2020-11-14T12:47:16Z" level=info msg="Desired change: CREATE example.com A [Id: /hostedzone/Z07***ZM6]"
time="2020-11-14T12:47:16Z" level=info msg="Desired change: CREATE example.comm TXT [Id: /hostedzone/Z07***ZM6]"
time="2020-11-14T12:47:16Z" level=info msg="2 record(s) in zone example.com. [Id: /hostedzone/Z07***ZM6] were successfully updated"

[/simterm]

И сам домен:

Обновление существующей записи root-level домена

Хотел попробовать финт ушами.

Допустим, у нас уже есть домен example.com с IN A 1.1.1.1, и удалять запись мы не хотим – а хотим её обновить при деплое нового Ingress.

Если просто добавить example.com в описание Ingress – ExternalDNS не обновит существующую IN A запись, т.к. нет TXT-записи, котоаря указывает ему на то, что IN A управляется им и привязана к Ingress.

Идея заключалась в том, что бы к этой IN A 1.1.1.1 добавить TXT  указанием txt-owner-id и Ingress, т.е. “heritage=external-dns,external-dns/owner=bttrm-eks-dev-1-external-dns,external-dns/resource=ingress/default/test-ingress“, а потом задеплоить сам Ingress.

Но в таком случае ExternalDNS удаляет запись ещё до того, как я успел залить новый манифест Ingress.

Поэтому процесс может быть таким:

  1. на Route53 есть домен example.com с IN A 1.1.1.1
  2. деплоим манифест Ingress с корневым доменом
  3. ExternalDNS не обновляет запись на Route53, т.к. нет “связующей” TXT-записи
  4. создаём TXT вручную – ExternalDNS при следующей проверке видит, что домен “принадлежит” ему, и что IN A не соответствует нужному Ingress, и обновляет её

Пробуем – удаляем записи, созданные ExternalDNS в предыдущих тестах, вручную создаём example.com с IN A 1.1.1.1, деплоим Ingress с example.com – ExternalDNS говорит, что “All records are already up to date“, т.к. TXT-записи нет.

В Route53 сейчас запись выглядит так:

Теперь вручную создаём TXT:

Смотрим логи:

[simterm]

time="2020-11-14T13:39:44Z" level=info msg="Desired change: UPSERT example.com A [Id: /hostedzone/Z07***ZM6]"
time="2020-11-14T13:39:44Z" level=info msg="Desired change: UPSERT example.com TXT [Id: /hostedzone/Z07***ZM6]"
time="2020-11-14T13:39:45Z" level=info msg="2 record(s) in zone example.com. [Id: /hostedzone/Z07***ZM6] were successfully updated"

[/simterm]

ExternalDNS в этот раз выполнил UPSERT, и не DELETE или CREATE, и записи обновились:

Готово.