Ansible: модуль community.kubernetes и установка Helm-чарта с ExternalDNS

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

В посте Kubernetes: обновление DNS в Route53 при создании Ingress выполнили ручную установку ExternalDNS, и посмотрели, как он работает – пора добавить автоматизацию его установки на кластера.

В роли Configuration Management Tool у нас используется Ansible, для которого существует модуль community.kubernetes – используем его.

Вообще, есть много модулей для работы с Helm, например – helm module или community.general, но у них есть недостаток – нельзя передать кастомный kubeconfig, тогда как у нас во время провижена кластера генерируется файл, которым потом пользуется kubectl/helm.

Собственно, что будем делать:

  • создадим IAM-политику для доступа к Route53
  • IAM роль, к которой подключим эту политику – она будет использоваться через AussemeRole подом с ExternalDNS
  • создадим IAM пользователя с Programmatic Access, который сможет ассюмить эту роль
  • обновим Ansible-роль, которая у на занимается установкой всех контроллеров в создаваемые кластера

AWS IAM

ExternalDNS policy

Переходим в AWS > IAM > Policies, создаём политику, которая разрешает доступ к Route53:

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

Назовём её iam-bttrm-external-dns-route53-policy, сохраняем.

ExternalDNS role

Далее – создаём роль:

В Permissions находим созданную политику:

Сохраняем:

Открываем её Trust relationships, редактируем:

Разрешаем AssumeRole этой роли всем IAM-пользователям нашего AWS-аккаунта – “arn:aws:iam::534***385:root“, где 534***385 – ID аккаунта:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::534***385:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

AssumeRole Allow policy

Создаём политику, которая будет разрешать выполнение AssumeRoleэтой роли:

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "arn:aws:iam::534***385:role/iam-bttrm-eks-external-dns-role"
    }
}

Сохраняем её:

IAM User

И наконец-то создаём пользователя, используя ACCESS и SECRET ключи которого ExternalDNS под будет выполнять API-запросы к Route53:

Подключаем ему политику, которая позволит ему выполнять AssumeRole роли “arn:aws:iam::534***385:role/iam-bttrm-eks-external-dns-role“:

Сохраняем его ключи, и переходим к Ansible.

Ansible

Сначала создадим переменные, потом обновим tasks нашей Ansible-роли.

Переменные и ansible-vault

Будем использовать три переменные – external_dns_iam_role_arn, external_dns_iam_user_access_key и external_dns_iam_user_secret_key.

Значение для external_dns_iam_user_secret_key зашифруем используя ansible-vault, т.к. файлы Ansible хранятся в репозитории, хоть и приватном, откуда потом используются в Jenkins-джобе.

Вызываем ansible-vault, шифруем строку с AWS_ACCESS_SECRET_KEY:

[simterm]

$ ansible-vault encrypt_string
New Vault password: 
Confirm New Vault password: 
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
gVo***JN1
!vault |
          $ANSIBLE_VAULT;1.1;AES256
          39303830623230306361633733343661393761323135313165393363646461646566306661633464
          ...
          3636333864656336396631396263313766633930386236633365
Encryption successful

[/simterm]

Редактируем файл с переменными, в нашем случае всё хранится в group_vars/all.yml, добавляем три новых:

external_dns_iam_role_arn: "arn:aws:iam::534***385:role/iam-bttrm-eks-external-dns-role"
external_dns_iam_user_access_key: "AKI***RUN"
external_dns_iam_user_secret_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          39303830623230306361633733343661393761323135313165393363646461646566306661633464
          ...
          3636333864656336396631396263313766633930386236633365

Ansible роль

Для установки всяких контроллеров типа ALB Ingress Controller у нас написана отдельная Ansible-роль, сюда же впишем и установку ExternalDNS.

Для этого пишем три task – установка модуля community.kubernetes, добавление Helm-репозитория Bitnami, и собственно установка Helm-чарта с ExternalDNS:

- name: "Install Ansible community.kubernetes plugin"
  command: "ansible-galaxy collection install community.kubernetes"

- name: "Add Bitnami chart repo"
  community.kubernetes.helm_repository:
    name: "bitnami"
    repo_url: "https://charts.bitnami.com/bitnami"

- name: "Deploy ExternalDNS chart inside {{ eks_env }}-devops-external-dns-ns namespace (and create it)"
  community.kubernetes.helm:
    kubeconfig: "{{ kube_config_path }}"
    name: "external-dns"
    chart_ref: "bitnami/external-dns"
    release_namespace: "{{ eks_env }}-devops-external-dns-ns"
    create_namespace: true
    values:
      domainFilters:
      - example.com
      aws:
        credentials:
          accessKey: "{{ external_dns_iam_user_access_key }}"
          secretKey: "{{ external_dns_iam_user_secret_key }}"
        assumeRoleArn: "{{ external_dns_iam_role_arn }}"

kube_config_path передаётся из другой Ansible-роли, которая после создания кластера с eksctl генерирует kubeconf, см. AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl.

eks_env передаётся из параметров Jenkins-джобы, в котором задаётся имя создаваемого кластера, в данном случае это будет dev-2, используя который формируем значение для переменной release_namespacedev-2-devops-external-dns-ns.

В values передаём значения для чарта – параметры самого ExternalDNS.

Jenkins job

Тут подробно останавливаться не буду – описано в посте Helm: пошаговое создание чарта и деплоймента из Jenkins.

В двух словах – есть stage Apply:

...
            stage("Apply") { 
                provision.ansibleApply( "${PLAYBOOK}", "${env.TAGS}", "${PASSFILE_ID}")
            }
...

Который вызывает функцию ansibleApply():

def ansibleApply(playbookFile='1', tags='2', passfile_id='3') {

    withCredentials([file(credentialsId: "${passfile_id}", variable: 'passfile')]) {

        docker.image('projectname/kubectl-aws:4.4').inside('-v /var/run/docker.sock:/var/run/docker.sock --net=host') {

            sh """
                aws sts get-caller-identity
                ansible-playbook ${playbookFile} --tags ${tags} --vault-password-file ${passfile}
            """
        }
    }
}

Запускаем:

SUCCESS:

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

[simterm]

$ kubectl -n dev-2-devops-external-dns-ns get pod
NAME                           READY   STATUS    RESTARTS   AGE
external-dns-6b9765fd4-fv5lj   1/1     Running   0          46s

[/simterm]

И его логи:

[simterm]

$ kubectl -n dev-2-devops-external-dns-ns logs -f external-dns-6b9765fd4-fv5lj
...
time="2020-11-24T08:59:03Z" level=info msg="Instantiating new Kubernetes client"
time="2020-11-24T08:59:03Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2020-11-24T08:59:03Z" level=info msg="Created Kubernetes client https://172.20.0.1:443"
time="2020-11-24T08:59:05Z" level=info msg="Assuming role: arn:aws:iam::534**385:role/iam-bttrm-eks-external-dns-role"
time="2020-11-24T08:59:10Z" level=info msg="All records are already up to date"
time="2020-11-24T09:00:11Z" level=info msg="All records are already up to date"

[/simterm]

msg=”Assuming role: arn:aws:iam::534**385:role/iam-bttrm-eks-external-dns-role” – ага, отлично.

Проверка ExternalDNS

И используем манифест из прошлого поста:

---
apiVersion: v1
kind: Namespace
metadata:
  name: test-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
  namespace: test-namespace
  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
      dnsConfig:
        nameservers:
        - 169.254.20.10
      dnsPolicy: None
---
apiVersion: v1
kind: Service
metadata:
  name: test-svc
  namespace: test-namespace
spec:
  type: NodePort
  selector:
    app: test
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  namespace: test-namespace
  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 tests/test-deployment.yaml 
namespace/test-namespace created
deployment.apps/test-deployment created
service/test-svc created
ingress.extensions/test-ingress created

[/simterm]

Проверяем Ingress:

[simterm]

$ kk -n test-namespace get ingress
NAME           CLASS    HOSTS   ADDRESS                                           PORTS   AGE
test-ingress   <none>   *       ***testnamespace***.us-east-2.elb.amazonaws.com   80      23s

[/simterm]

Логи ExternalDNS:

[simterm]

time="2020-11-24T09:18:24Z" level=info msg="Desired change: CREATE test-dns.examplecom A [Id: /hostedzone/Z07***M6]"
time="2020-11-24T09:18:24Z" level=info msg="Desired change: CREATE test-dns.example.com TXT [Id: /hostedzone/Z07***M6]]"
time="2020-11-24T09:18:25Z" level=info msg="2 record(s) in zone example.com. [Id: /hostedzone/Z07***M6]] were successfully updated"

[/simterm]

И работу нового домена:

[simterm]

$ dig +short test-dns.example.com
3.23.41.8
3.12.246.228

[/simterm]

Готово.