И вообще 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 адрес сетевого интерфейса был статическим.
Пожалуй, осталось только напомнить разослать 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 первого мастер узла
Обратите внимание в ansible.cfg параметр
[defaults]
forks = 50
Ориентируйтесь на общее количество узлов в inventory, асинхронные задачи плейбука выполняют одинаковую работу на всех узлах.
Для повышения производительности, будет показано далее, мастер узлы и рабочие узлы разворачиваются в разных пайплайнах. Это связано с особенностями ansible. При таком подходе можно гораздо быстрее приступить к присоединению рабочих узлов. Не следует раздавать скачанные тарболы и прочие объемные файлы с мастер узла. Там запустится etcd, для него критично время доступа к своей БД.
PLAYs:
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
# 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
Приветствую всех! Недавнее масштабное обновление 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 для инициализации мастер узла
- как обычно, указана совместимость версий компонентов
- введен новый код для ubuntu, всвязи с deprecated apt-key
Используем принципы простоты для написания ролей и лучшего кода. Когда его, желательно, вообще нет. Для начала структурируем нашу исходную информацию для шага 3 в defaults:
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
# 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
Раскатка k8s 1.26 ansible+jenkins
И вообще managed cluster своими руками за 1000 и один человеко-час. Приветствую всех! Недавнее масштабное обновление github (когда там часами не работало ничто) побудило меня поделиться своим опытом...
habr.com