Задача: при создании 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.
Поэтому процесс может быть таким:
- на Route53 есть домен example.com с IN A 1.1.1.1
- деплоим манифест Ingress с корневым доменом
- ExternalDNS не обновляет запись на Route53, т.к. нет “связующей” TXT-записи
- создаём 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
, и записи обновились:
Готово.