И вообще managed cluster своими руками за 1000 и один человеко-час.

Приветствую всех! Недавнее масштабное обновление github (когда там часами не работало ничто) побудило меня поделиться своим опытом автоматизации установки k8s на bare metal.

Итак. Задача: развернуть кластер kubernetes последней на данный момент версии 1.26 средствами CI/CD за минимальное время (на моем оборудовании около 3 минут), и вообще, начать с этого построение своих инструментов управления кластером.

Для этого потребуется от 3 серверов под управлением ubuntu, чтобы прошли тесты sonobuoy conformance (адаптация под rhel потребует небольших доработок).

Разработанный процесс больше подойдет для разворачивания тестовой среды, именно для этого акцент сделан на скорости, исключен любой шаг, без которого все и так полностью будет функционировать. Для production кластера, как минимум, придется добавить дополнительные отдельные etcd узлы, данный процесс за рамками этой статьи. Однако, etcd узлы на мастер нодах созданы будут в рассматриваемом процессе.

О роли ansible inventory. 

[masters]
k8s
[master]
k8s
[etcd]
k8s
[workers]
r01
r02
[jenkins]
k8s
[grafana]
k8s

Давайте сделаем его простым, очень простым. Чтобы это осуществить, настройте dhcp службу вручную или с помощью API своего маршрутизатора. Будет нелишним настроить домен маршрутизатора, просто укажите любой идентификатор, который будет добавлен к именам ваших узлов. Вам потребуется добавить mac адреса сетевых интерфейсов своих узлов в список соответствия IP адресам в вашей подсети. Иными словами, составьте план статических адресов для своих узлов кластера. 


Для виртуальных машин проверьте, чтобы mac адрес сетевого интерфейса был статическим.
Для виртуальных машин проверьте, чтобы mac адрес сетевого интерфейса был статическим.


Пожалуй, осталось только напомнить разослать public ssh ключи вашего хоста ansible на узлы будущего кластера.

Стратегия: выделить из общего процесса установки и настройки кластера асинхронные, независимые друг от друга и описать их в виде ролей ansible. Данные процессы будут выполняться в параллельных stage пайплайна jenkins. 

pipeline { 
    agent any
    options {
        parallelsAlwaysFailFast()
    }
    stages { 
        stage('Deploy Kubernetes Cluster conforms to kubeadm 1.26 kubernetes.io official'){ 
           parallel {
             stage('Apply system requirements'){
                   steps { 
                     sh 'ansible-playbook -i files/hosts init-phd.yaml'
                   }
             }
             stage('Installing containerd container runtime'){
                   steps{
                     sh 'ansible-playbook -i files/hosts init-phb.yaml'
                   }
             }
             stage('Configuring systemd cgroup driver'){
                   steps{
                     sh 'ansible-playbook -i files/hosts init-phc.yaml'
                   }
             }
             stage('Installing kubeadm, kubelet and kubectl'){
                   steps { 
                     sh 'ansible-playbook -i files/hosts init-pha.yaml'
                   }
             }
           }
        }
         stage('Bootstrap cluster with kubeadm'){
            steps { 
                sh 'ansible-playbook -i files/hosts init-masters.yaml'
            }
         }
    }
}

То есть у нас 5 PLAYs: 4 асинхронных и последний PLAY


Загрузка с помощью kubeadm первого мастер узла
Загрузка с помощью kubeadm первого мастер узла

Обратите внимание в ansible.cfg параметр 

[defaults]
forks = 50

Ориентируйтесь на общее количество узлов в inventory, асинхронные задачи плейбука выполняют одинаковую работу на всех узлах.

Для повышения производительности, будет показано далее, мастер узлы и рабочие узлы разворачиваются в разных пайплайнах. Это связано с особенностями ansible. При таком подходе можно гораздо быстрее приступить к присоединению рабочих узлов. Не следует раздавать скачанные тарболы и прочие объемные файлы с мастер узла. Там запустится etcd, для него критично время доступа к своей БД.

PLAYs:

  1. Применение системных требований включает удаление ненужных сервисов, установку нужных пакетов, отключение swap

  2. Установка containerd, среды исполнения runc, сетевых интерфейсов

  3. Конфигурирование драйвера cgroup в linux systemd

  4. Установка репозитория и компонентов kubernetes

  5. Запуск kubeadm для инициализации мастер узла

Теперь к материалам официального сайта kubernetes. В версии 1.26:

  • как обычно, указана совместимость версий компонентов

  • введен новый код для ubuntu, всвязи с deprecated apt-key


    Используем принципы простоты для написания ролей и лучшего кода. Когда его, желательно, вообще нет. Для начала структурируем нашу исходную информацию для шага 3 в defaults:

# defaults file for containerd
content:
    github.com/containerd/containerd/releases/download/v1.6.4/ : 
      name: containerd-1.6.4-linux-amd64.tar.gz
      path: /usr/local
      tag:  tar
    github.com/containernetworking/plugins/releases/download/v1.1.1/ : 
      name: cni-plugins-linux-amd64-v1.1.1.tgz
      path: /opt/cni/bin
      tag:  tar
    github.com/opencontainers/runc/releases/download/v1.1.4/ : 
      name: runc.amd64
      path: /usr/local/sbin/runc
      tag:  install
    raw.githubusercontent.com/containerd/containerd/main/ : 
      name: containerd.service
      path: /usr/local/lib/systemd/system
      tag:  service
    containerd-file-config.toml:
      name: ./roles/containerd/files/config.toml
      path: /etc/containerd
      tag: file
    cni-template-j2file:
      name: ./roles/containerd/templates/10-containerd-net.conflist.j2
      path: /etc/cni/net.d/10-containerd-net.conflist
      tag: j2t
    cni-lo-file:
      name: ./roles/containerd/files/99-loopback.conf
      path: /etc/cni/net.d
      tag: file
  • Ссылки на тарболы официально рекомендуемых версий имеют тэг ‘tar’

  • Модуль runc рекомендуется заносить через утилиту install

  • Файл с описанием сервиса containerd с тэгом ‘service’

  • Конфигурационный файл сетевого интерфейса в виде шаблона jinja тэг ‘j2t’

  • Уже готовая конфигурация containerd под systemd и loopback интерфейс для сетей контейнеров с тэгом file

Более половины некоторых плейбуков состоят из рутинных скачиваний архивов, распаковок, скачиваний сервисов, вызовов install, создания каталогов, совершенно одинакового кода перезапуска systemd и service restart+enable, отработки шаблонов и копирования файлов роли. Мои плейбуки никогда не содержат такого кода, хотя и делают тоже самое. Просто все эти действия совершаются разработанной мной ролью ansible на основании тегов. Объявления переменных в defaults для ansible внутри PLAY составляют общее пространство. Роль:

# tasks file for wgi

- name: Wget content
  get_url:
    url: "https://{{ item.key }}{{ item.value.name }}"
    dest: "/tmp/{{ item.value.name }}"
    force: false
  loop: "{{ content | dict2items |
            rejectattr('value.tag', 'search', 'file' ) |
            rejectattr('value.tag', 'search', 'j2t' ) |
            list }}"
  loop_control:
     label: "{{ item.key }}"
 
- block:

  - name: Creates directory
    file:
      path: "{{
               item.value.path
                  if not (item.value.tag in ['install','j2t'])
                     else ( item.value.path | dirname )
              }}"
      state: directory
      owner: root
      group: root
      mode: 0755
    loop: "{{ content | dict2items  }}"
    loop_control:
      label: "{{ item.key }}"

  - name: Copy services content
    copy:
      src:  "/tmp/{{ item.value.name }}"
      dest: "{{ item.value.path }}"
      remote_src: yes
    register: content_restart
    notify:
      - reload systemd
      - restart systemd
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'service' ) | list }}"
    loop_control:
      label: "{{ item.key }}"
        
  - name: Extract archived
    unarchive:
      src:  "/tmp/{{ item.value.name }}"
      dest: "{{ item.value.path }}"
      owner: root
      group: root
      mode:  0755
      remote_src: yes
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'tar' ) | list }}"
    loop_control:
      label: "{{ item.key }}"

  - name: Install module
    shell: install -m 755 "/tmp/{{ item.value.name }}" "{{ item.value.path }}"
    args:
      creates: "{{ item.value.path }}"
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'install' ) | list }}"
    loop_control:
      label: "{{ item.key }}"
      
  - name: Copy plain artifacts
    copy:
      src:  "/tmp/{{ item.value.name }}"
      dest: "{{ item.value.path }}"
      owner: root
      group: root
      mode: u=rw,g=r,o=r
      remote_src: yes
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'plain' ) | list }}"
    loop_control:
      label: "{{ item.key }}"

  - name: Copy role files
    copy:
      src:  "{{ item.value.name }}"
      dest: "{{ item.value.path }}"
      owner: root
      group: root
      mode: u=rw,g=r,o=r
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'file' ) | list }}"
    loop_control:
      label: "{{ item.key }}"
      
  - name: Copy role templates
    template:
      src:  "{{ item.value.name }}"
      dest: "{{ item.value.path }}"
      owner: root
      group: root
      mode: u=rw,g=r,o=r
    loop: "{{ content | dict2items | selectattr('value.tag', 'search', 'j2t' ) | list }}"
    loop_control:
      label: "{{ item.key }}"

  become: yes

Возможно, нужно пояснить, перезагрузка systemd происходит через notify. Записи для changed сервисов будут в зарегистрированном массиве, на основании которого handler restart systemd перезапустит перечисленные в массиве сервисы и выполнит команду enabled. Handlers описаны в файле данной роли, при необходимости их можно переопределить, ansible так и работает.

Шаблон jinja2 в templates производит вычисление адреса, который будет присвоен сетевому интерфейсу cni.

В общем, tasks/main.yml для роли containerd оказывается совсем без кода. Кода нет, а чувство удовлетворения есть.

Что важного в файле конфигурации config.toml на основании официальных требований:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]

  ...

  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

    SystemdCgroup = true

И

[plugins."io.containerd.grpc.v1.cri"]

  sandbox_image = "registry.k8s.io/pause:3.2"

Файл конфигурации по умолчанию, сгенерированный containerd не содержит требуемых параметров.

Разбираем 4 пункт.

Опять поможет вышеприведенная роль. Скачает ключ для подписания репозитория.

content:
  packages.cloud.google.com/apt/doc/:
    name: apt-key.gpg
    path:  /etc/apt/keyrings/
    tag: plain
  Add-the-Kubernetes-apt-repository:
    name: ./roles/k8s-install/files/kubernetes.list
    path: /etc/apt/sources.list.d
    tag: file

Файл kubernetes.list из официального источника для роли теперь выглядит так: 

deb [signed-by=/etc/apt/keyrings/apt-key.gpg] https://apt.kubernetes.io/ kubernetes-xenial main

Кода опять нет, роль из шага 4 содержит фактически только defaults.

Только как обычно, не забудьте отработать вот это:

net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1

Остальное при установке осталось по-прежнему в шаге 5:

- hosts: master
  become: yes
  pre_tasks:
    - name: initialize the cluster
      shell: kubeadm init --pod-network-cidr=10.244.0.0/16
      args:
        chdir: $HOME
        creates: /etc/kubernetes/admin.conf
      register: kubeadm

    - debug: 
        var: kubeadm.stdout_lines
  roles:
    - k8s-copy-admin

Проверка creates позволяет потом присоединить мастер узлы просто указав группу masters, повторно kubeadm не запустится.

Рабочие узлы добавляем как всегда
Рабочие узлы добавляем как всегда

Запуск пайплайна присоединения рабочих узлов можно производить параллельно с запуском мастер узла. Рабочих узлов обычно больше, чем мастер узлов, поэтому общее время выполнения асинхронной части должно быть не менее времени выполнения на самом медленном рабочем узле. Синхронизация запуска kubeadm нужна, осуществляется проверкой наличия сгенерированного файла конфигурации на мастер узле.

Возможность быстро восстановить кластер позволяет проверить сотни самых невероятных гипотез и конфигураций, которых нет и никогда не будет в облаках, разворачивать среды тестирования и многое другое.

Больше материалов по данной теме в моем репозитории https://itoracl.github.io/k8s

Комментарии (7)


  1. chemtech
    28.01.2023 06:55

    Спасибо за пост. Допишите пожалуйста краткое описание "sonobuoy conformance"


    1. itoracle Автор
      28.01.2023 16:45

      Утилита диагностики. Кластер можно создать формально работающий, в логах ошибок нет, crashloop нет. Но, например, ingress admission contoller api будет недоступен, попытка создания endpoint хотя-бы вернет ошибку. (Это я сам кластер свой шатал)

      В sonobuoy 300+ тестов на мыслимые сценарии работы ресурсов, в том числе на разных рабочих узлах. Только для этого и нужен в тестах второй рабочий


  1. web3_Venture
    28.01.2023 11:03

    microk8s  k0s, kind, k3s и Minikube etc

    Вы разбирали другие варианты? почему именно этот выбрали ?


  1. angapov
    28.01.2023 16:23
    +1

    Почему не kubespray?


    1. itoracle Автор
      28.01.2023 17:03

      Мы же самую свежую версию кубера рассматриваем.

      • Не уверен, что разработчики сценариев быстро все учитывают. Тут еще в ubuntu последнее китайское предупреждение о депрекации apt-key.

      • Плюс хочется самому контролировать версии всех колесиков внутри.

      • Свое решение позволяет пилить и ускорять процесс деплоя кластера. Например, два, три ansible хоста, если один действительно на 100 узлов намного медленнее работает. Всегда будет 3 минуты на все. Тут была свежая публикация, как только недавно докер из кластера выкинули, и заодно перешли на версию 1.21 =) legacy оно такое


  1. vuidji
    30.01.2023 05:36

    Если верно понимаю потребность (как часть CI/CD шустро развертывать свой маленький, но настоящий k8s кластер и прогонять на нем какие-нибудь тесты), то не подходит ли тут идеально k3d от Rancher? Развертывает одной командой k3s кластер заданной конфигурации в докере.


  1. NiceITMan
    30.01.2023 11:21

    очень помогли, спасибо