И вообще 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 адресам в вашей подсети. Иными словами, составьте план статических адресов для своих узлов кластера.
Пожалуй, осталось только напомнить разослать 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
Обратите внимание в ansible.cfg параметр
[defaults]
forks = 50
Ориентируйтесь на общее количество узлов в inventory, асинхронные задачи плейбука выполняют одинаковую работу на всех узлах.
Для повышения производительности, будет показано далее, мастер узлы и рабочие узлы разворачиваются в разных пайплайнах. Это связано с особенностями ansible. При таком подходе можно гораздо быстрее приступить к присоединению рабочих узлов. Не следует раздавать скачанные тарболы и прочие объемные файлы с мастер узла. Там запустится etcd, для него критично время доступа к своей БД.
PLAYs:
Применение системных требований включает удаление ненужных сервисов, установку нужных пакетов, отключение swap
Установка containerd, среды исполнения runc, сетевых интерфейсов
Конфигурирование драйвера cgroup в linux systemd
Установка репозитория и компонентов kubernetes
Запуск 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)
web3_Venture
28.01.2023 11:03microk8s k0s, kind, k3s и Minikube etc
Вы разбирали другие варианты? почему именно этот выбрали ?
angapov
28.01.2023 16:23+1Почему не kubespray?
itoracle Автор
28.01.2023 17:03Мы же самую свежую версию кубера рассматриваем.
Не уверен, что разработчики сценариев быстро все учитывают. Тут еще в ubuntu последнее китайское предупреждение о депрекации apt-key.
Плюс хочется самому контролировать версии всех колесиков внутри.
Свое решение позволяет пилить и ускорять процесс деплоя кластера. Например, два, три ansible хоста, если один действительно на 100 узлов намного медленнее работает. Всегда будет 3 минуты на все. Тут была свежая публикация, как только недавно докер из кластера выкинули, и заодно перешли на версию 1.21 =) legacy оно такое
vuidji
30.01.2023 05:36Если верно понимаю потребность (как часть CI/CD шустро развертывать свой маленький, но настоящий k8s кластер и прогонять на нем какие-нибудь тесты), то не подходит ли тут идеально k3d от Rancher? Развертывает одной командой k3s кластер заданной конфигурации в докере.
chemtech
Спасибо за пост. Допишите пожалуйста краткое описание "sonobuoy conformance"
itoracle Автор
Утилита диагностики. Кластер можно создать формально работающий, в логах ошибок нет, crashloop нет. Но, например, ingress admission contoller api будет недоступен, попытка создания endpoint хотя-бы вернет ошибку. (Это я сам кластер свой шатал)
В sonobuoy 300+ тестов на мыслимые сценарии работы ресурсов, в том числе на разных рабочих узлах. Только для этого и нужен в тестах второй рабочий