Обновление кластера Kubernetes на физических серверах без автоматизации - это часы ручного труда, риск неконсистентной конфигурации и потенциальный простой сервисов. Ansible решает эту проблему, превращая процесс в воспроизводимый, контролируемый и быстрый сценарий. Вы получаете единый набор плейбуков, который выполняет drain нод, обновляет компоненты control-plane и worker-группы, а затем возвращает узлы в работу. В этой статье мы разберем готовые роли, шаблоны конфигурации и механизмы обработки ошибок, которые позволяют обновлять production-кластеры с соблюдением требований SOC2 и PCI DSS.
Автоматизация обновления Kubernetes на bare metal с помощью Ansible строится вокруг трех ключевых этапов: подготовка инвентаря и шаблонов, последовательное обновление control-plane, а затем циклическая обработка worker-нод с контролем доступности приложений. Каждый шаг сопровождается проверенными примерами кода, которые вы можете адаптировать под свою среду. Материал ориентирован на инженеров, управляющих кластерами в средах со строгим контролем версий и высокими требованиями к безопасности.
Введение: зачем автоматизировать обновление Kubernetes на bare metal
Ручное обновление bare metal кластера Kubernetes сопряжено с повторяющимися операциями на каждом сервере: перевод узла в режим обслуживания, установка новых версий kubeadm, kubelet и kubectl, проверка совместимости конфигураций, возврат узла в кластер. На кластере из десяти нод администратор выполняет десятки команд, каждая из которых может привести к ошибке из-за опечатки или несовпадения версий. В средах, подпадающих под SOC2 и PCI DSS, такие ошибки создают не только технические, но и комплаенс-риски: аудитор запросит доказательства контролируемого процесса изменений.
Ansible устраняет эти проблемы через подход «инфраструктура как код». Плейбуки описывают точную последовательность действий, шаблоны фиксируют конфигурацию, а встроенное логирование создает аудиторский след. После первого успешного прогона все последующие обновления выполняются идентично, независимо от того, кто из команды их запускает. Если вы еще не работали с Ansible для управления конфигурациями, рекомендуем изучить сравнение Terraform и Ansible - это поможет понять, почему для обновления существующих серверов мы выбираем именно Ansible, а не инструменты оркестрации инфраструктуры.
Готовые плейбуки из этой статьи решают конкретную задачу: обновить версию Kubernetes на всех узлах физического кластера с минимальным влиянием на рабочую нагрузку. Вы получите роли для control-plane и worker-нод, механизмы drain/uncordon и обработку сбоев.
Подготовка среды Ansible для управления bare metal кластером
Перед запуском обновления необходимо настроить управляющий узел Ansible и определить инвентарь физических серверов. Управляющий узел - это отдельная машина или рабочая станция администратора с установленным Ansible (версия 2.14 или новее). На всех целевых серверах кластера должен быть настроен SSH-доступ по ключу, а пользователь - обладать правами sudo без пароля. Управляющий узел также нуждается в доступе к kubeconfig кластера для выполнения команд kubectl drain и kubectl uncordon.
Структура проекта и inventory для физических серверов
Организация файлов напрямую влияет на удобство поддержки и переиспользования кода. Рекомендуемая структура каталогов для проекта обновления Kubernetes:
k8s-upgrade/
├── ansible.cfg
├── inventory/
│ └── production/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all.yml
│ │ ├── control_plane.yml
│ │ └── workers.yml
│ └── host_vars/
│ ├── cp-node-01.yml
│ └── worker-node-01.yml
├── roles/
│ ├── upgrade-control-plane/
│ │ ├── tasks/
│ │ │ └── main.yml
│ │ └── templates/
│ │ └── kubeadm-config.yaml.j2
│ └── upgrade-worker/
│ └── tasks/
│ └── main.yml
└── playbooks/
├── upgrade-cluster.yml
└── rollback.yml
Файл инвентаря hosts.yml определяет группы серверов. Для bare metal кластера это выглядит так:
all:
children:
control_plane:
hosts:
cp-node-01:
ansible_host: 192.168.10.11
cp-node-02:
ansible_host: 192.168.10.12
cp-node-03:
ansible_host: 192.168.10.13
workers:
hosts:
worker-node-01:
ansible_host: 192.168.10.21
worker-node-02:
ansible_host: 192.168.10.22
worker-node-03:
ansible_host: 192.168.10.23
vars:
ansible_user: k8s-admin
ansible_become: yes
target_kubernetes_version: "1.29.0"
Переменная target_kubernetes_version задает целевую версию для всего кластера. Групповые переменные в control_plane.yml и workers.yml переопределяют специфичные параметры, например флаги drain для рабочих узлов.
Использование шаблонов Ansible для конфигурации Kubernetes
При обновлении Kubernetes критически важно, чтобы конфигурационные файлы kubeadm соответствовали целевой версии. Ручное редактирование YAML-файлов на каждом сервере - прямой путь к расхождениям. Шаблоны Ansible решают эту проблему: вы описываете конфигурацию один раз с подстановкой переменных, а при запуске плейбука каждый узел получает идентичный файл.
Пример шаблона kubeadm-config.yaml.j2 для роли upgrade-control-plane:
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: {{ target_kubernetes_version }}
controlPlaneEndpoint: "{{ control_plane_endpoint }}"
networking:
podSubnet: "{{ pod_subnet }}"
serviceSubnet: "{{ service_subnet }}"
etcd:
local:
dataDir: /var/lib/etcd
apiServer:
extraArgs:
audit-log-path: /var/log/kubernetes/audit.log
audit-log-maxage: "30"
audit-log-maxbackup: "10"
audit-log-maxsize: "100"
Переменные target_kubernetes_version, control_plane_endpoint, pod_subnet и service_subnet определяются в group_vars. При запуске плейбука Ansible подставляет актуальные значения, генерируя корректный файл для целевой версии Kubernetes. Хранение шаблона в Git вместе с переменными обеспечивает полную прослеживаемость: аудитор видит, кто, когда и какую конфигурацию применил. Это прямое требование SOC2 к управлению изменениями.
Пошаговое обновление control-plane нод с помощью Ansible
Control-plane - самый чувствительный компонент кластера. Ошибка при обновлении может оставить кластер без API-сервера или с рассогласованным состоянием etcd. Плейбук для control-plane выполняет строгую последовательность: drain, обновление пакетов, применение kubeadm upgrade, uncordon. Каждый шаг проверяется перед переходом к следующему.
Роль Ansible для обновления control-plane: пошаговый разбор
Роль upgrade-control-plane содержит полный набор задач для обновления одного control-plane узла. Ниже - основной файл tasks/main.yml с пояснениями к каждому блоку:
---
# 1. Проверка текущей версии Kubernetes
- name: Get current Kubernetes version
command: kubectl version --short
register: current_version
changed_when: false
- name: Display current version
debug:
msg: "Current version: {{ current_version.stdout_lines }}"
# 2. Drain control-plane ноды
- name: Drain control-plane node
command: >
kubectl drain {{ inventory_hostname }}
--ignore-daemonsets
--delete-emptydir-data
--force
--grace-period=120
delegate_to: "{{ groups['control_plane'][0] }}"
register: drain_result
- name: Wait for pods to reschedule
pause:
seconds: 30
when: drain_result.changed
# 3. Обновление пакетов Kubernetes
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install target version of kubeadm
apt:
name: "kubeadm={{ target_kubernetes_version }}-00"
state: present
force: yes
- name: Install target version of kubelet and kubectl
apt:
name:
- "kubelet={{ target_kubernetes_version }}-00"
- "kubectl={{ target_kubernetes_version }}-00"
state: present
force: yes
- name: Hold kubelet, kubeadm, kubectl at target version
dpkg_selections:
name: "{{ item }}"
selection: hold
loop:
- kubelet
- kubeadm
- kubectl
# 4. Применение kubeadm upgrade
- name: Apply kubeadm upgrade plan
command: "kubeadm upgrade plan {{ target_kubernetes_version }}"
register: upgrade_plan
changed_when: false
- name: Execute kubeadm upgrade
command: "kubeadm upgrade apply {{ target_kubernetes_version }} -y"
register: upgrade_result
# 5. Перезапуск kubelet и uncordon
- name: Restart kubelet
systemd:
name: kubelet
state: restarted
daemon_reload: yes
- name: Uncordon control-plane node
command: "kubectl uncordon {{ inventory_hostname }}"
delegate_to: "{{ groups['control_plane'][0] }}"
- name: Verify node status
command: kubectl get node {{ inventory_hostname }}
register: node_status
changed_when: false
retries: 10
delay: 10
until: "' Ready' in node_status.stdout"
Ключевые моменты роли: drain выполняется с делегированием на первый control-plane узел, к которому есть доступ по kubeconfig. Флаг --ignore-daemonsets обязателен, так как DaemonSet-поды не эвакуируются стандартным drain. Пакеты устанавливаются с точной фиксацией версии через apt hold - это предотвращает случайное обновление при следующем запуске системного обновления. Финальная проверка статуса ноды с повторными попытками гарантирует, что узел вернулся в состояние Ready.
Обработка ошибок и откат при сбое обновления control-plane
Сбой на этапе обновления control-plane - риск потери управляемости кластером. Ansible предоставляет блоки block, rescue и always для структурированной обработки ошибок. Расширенная версия роли с обработкой сбоев:
---
- name: Upgrade control-plane node with error handling
block:
- name: Drain node
command: "kubectl drain {{ inventory_hostname }} --ignore-daemonsets --delete-emptydir-data"
register: drain
- name: Upgrade packages
apt:
name: "kubeadm={{ target_kubernetes_version }}-00"
state: present
- name: Apply kubeadm upgrade
command: "kubeadm upgrade apply {{ target_kubernetes_version }} -y"
register: upgrade
- name: Uncordon node
command: "kubectl uncordon {{ inventory_hostname }}"
rescue:
- name: Log failure details
copy:
content: |
Timestamp: {{ ansible_date_time.iso8601 }}
Node: {{ inventory_hostname }}
Target version: {{ target_kubernetes_version }}
Error: {{ ansible_failed_result.msg | default('Unknown error') }}
dest: "/var/log/k8s-upgrade/{{ inventory_hostname }}-failure.log"
- name: Attempt uncordon on failure
command: "kubectl uncordon {{ inventory_hostname }}"
ignore_errors: yes
- name: Fail playbook with clear message
fail:
msg: "Upgrade failed on {{ inventory_hostname }}. Check /var/log/k8s-upgrade/ for details."
always:
- name: Record attempt in audit log
copy:
content: |
Node: {{ inventory_hostname }}
Action: control-plane upgrade
Version: {{ target_kubernetes_version }}
Status: {{ 'FAILED' if ansible_failed_result is defined else 'SUCCESS' }}
Time: {{ ansible_date_time.iso8601 }}
dest: "/var/log/k8s-upgrade/audit-{{ ansible_date_time.date }}.log"
mode: '0640'
Блок rescue срабатывает при любой ошибке в block. Он записывает детали сбоя в структурированный лог, пытается вернуть ноду в кластер и завершает плейбук с понятным сообщением. Блок always выполняется независимо от результата и создает запись аудита с временной меткой, именем узла и статусом операции. Такой подход удовлетворяет требованиям PCI DSS к протоколированию всех изменений конфигурации.
Автоматизация обновления worker-нод с drain и uncordon
Worker-ноды обновляются после control-plane. Порядок важен: kubelet на worker-нодах не может быть новее, чем kube-apiserver. Стратегия обновления - последовательная, с контролем пересоздания подов после каждой ноды. Это гарантирует, что приложения остаются доступными на протяжении всего процесса.
Роль Ansible для worker-нод: drain, обновление и возврат в кластер
Роль upgrade-worker адаптирована под особенности рабочих узлов. В отличие от control-plane, здесь не выполняется kubeadm upgrade apply - достаточно обновить kubelet и перезапустить его. Полный список задач:
---
# 1. Cordon и drain worker-ноды
- name: Cordon worker node
command: "kubectl cordon {{ inventory_hostname }}"
delegate_to: "{{ groups['control_plane'][0] }}"
- name: Drain worker node
command: >
kubectl drain {{ inventory_hostname }}
--ignore-daemonsets
--delete-emptydir-data
--grace-period=60
--timeout=300s
delegate_to: "{{ groups['control_plane'][0] }}"
register: drain_worker
- name: Wait for pod rescheduling
pause:
seconds: 20
when: drain_worker.changed
# 2. Обновление kubelet
- name: Update kubelet to target version
apt:
name: "kubelet={{ target_kubernetes_version }}-00"
state: present
force: yes
- name: Hold kubelet version
dpkg_selections:
name: kubelet
selection: hold
# 3. Перезапуск kubelet и uncordon
- name: Restart kubelet service
systemd:
name: kubelet
state: restarted
daemon_reload: yes
- name: Wait for kubelet to register
pause:
seconds: 15
- name: Uncordon worker node
command: "kubectl uncordon {{ inventory_hostname }}"
delegate_to: "{{ groups['control_plane'][0] }}"
# 4. Проверка готовности
- name: Verify worker node is Ready
command: kubectl get node {{ inventory_hostname }} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
register: node_ready
retries: 12
delay: 10
until: node_ready.stdout == "True"
Флаг --timeout=300s при drain задает жесткое ограничение: если поды не эвакуируются за 5 минут, операция прерывается. Это защищает от бесконечного ожидания при проблемах с PodDisruptionBudget. Проверка готовности через jsonpath-запрос к API точно определяет состояние узла, исключая ложные срабатывания при парсинге текстового вывода.
Параллельное обновление нескольких worker-нод: риски и ограничения
Параллельное обновление worker-нод ускоряет процесс, но создает риск деградации сервисов. Ключевой параметр - директива serial в плейбуке. Она определяет, сколько узлов обрабатывается одновременно. Для production-сред с высокими требованиями к доступности рекомендуется serial: 1 - строго последовательное обновление. Главный плейбук upgrade-cluster.yml:
---
- name: Upgrade control-plane nodes
hosts: control_plane
serial: 1
roles:
- upgrade-control-plane
- name: Upgrade worker nodes
hosts: workers
serial: 1
roles:
- upgrade-worker
Значение serial: 1 гарантирует, что следующая нода начнет обновляться только после успешного завершения предыдущей. Если в кластере есть приложения с PodDisruptionBudget, допускающим отключение только одного пода, параллельное обновление двух нод приведет к отказу drain на второй. Ansible в этом случае остановит плейбук с ошибкой, сохраняя целостность сервисов.
Для кластеров с избыточным количеством worker-нод и некритичными нагрузками можно увеличить serial до 2 или 3, но только после анализа PDB всех рабочих нагрузок. Безопасная стратегия - начать с serial: 1, измерить реальное время обновления одной ноды и затем принимать решение об оптимизации.
Обеспечение соответствия стандартам безопасности (SOC2, PCI DSS) при обновлении
Стандарты SOC2 и PCI DSS требуют документированного, повторяемого и контролируемого процесса внесения изменений в инфраструктуру. Автоматизация обновления Kubernetes через Ansible закрывает три ключевых требования: неизменяемость процедуры (плейбуки в Git), полный аудиторский след (логирование каждого действия) и контроль версий компонентов (фиксация пакетов в переменных).
Ведение журнала и аудит действий Ansible
Ansible по умолчанию выводит результаты выполнения задач в stdout, но для аудита этого недостаточно - нужен структурированный журнал с временными метками и статусами. Настройка ведется через ansible.cfg:
[defaults]
log_path = /var/log/ansible/k8s-upgrade.log
callback_whitelist = log_plays, timer
stdout_callback = yaml
[callback_log_plays]
log_folder = /var/log/ansible/plays/
Плагин log_plays записывает отдельный файл для каждого запуска плейбука с детализацией по задачам и узлам. Плагин timer добавляет информацию о длительности каждой задачи - это полезно для анализа производительности и выявления узлов, на которых обновление занимает аномально много времени.
Дополнительно в каждой роли реализована запись в аудиторский лог через блок always, как показано в разделе про control-plane. Итоговый журнал содержит: идентификатор узла, действие (drain/upgrade/uncordon), целевую версию, фактический статус и временную метку. При аудите SOC2 такой журнал служит доказательством контролируемого процесса изменений.
Контроль версий и неизменяемость инфраструктуры
Хранение всех плейбуков, ролей, шаблонов и переменных в Git-репозитории обеспечивает соблюдение принципа неизменяемости инфраструктуры. Каждое обновление кластера выполняется строго из зафиксированного коммита. Это означает, что процедура не зависит от состояния рабочей станции администратора или ситуативных правок «на лету». Для аудитора такая практика демонстрирует зрелость процесса управления изменениями.
Рекомендуемая структура Git-репозитория включает теги для каждой процедуры обновления. Перед обновлением кластера создается тег с указанием целевой версии Kubernetes, например upgrade-1.29.0-2026-08-06. После завершения обновления и проверки стабильности кластера тег помечается как релизный. Это позволяет в любой момент восстановить точную конфигурацию, которая была применена к кластеру в конкретный момент времени.
Переменные с версиями пакетов в group_vars дополняют эту практику. Вместо разрозненных apt-команд на каждом сервере, целевая версия kubeadm, kubelet и kubectl задается в одном месте и применяется ко всем узлам. Для сред с требованиями PCI DSS это критически важно: стандарт обязывает отслеживать версии всех компонентов, обрабатывающих данные держателей карт.
Заключение: полный цикл автоматизированного обновления Kubernetes
Автоматизация обновления Kubernetes на bare metal с помощью Ansible сводится к трем этапам: подготовка инвентаря и шаблонов, последовательное обновление control-plane нод, циклическое обновление worker-нод с контролем доступности. Каждый этап реализован в виде готовых ролей, которые вы можете скопировать и адаптировать под свой кластер.
Ключевые результаты внедрения подхода: время обновления кластера из десяти нод сокращается с нескольких часов до 30-40 минут, исключаются ошибки ручного ввода, создается полный аудиторский след для SOC2 и PCI DSS. Плейбуки и роли хранятся в Git, версии пакетов зафиксированы в переменных, логирование настроено на уровне Ansible и на уровне задач.
Для дальнейшего развития автоматизации инфраструктуры изучите практическое руководство по автоматизации для DevOps - там собраны проверенные сценарии для мониторинга, резервного копирования и масштабирования. Если вы управляете гетерогенной средой, обратите внимание на архитектуру универсальной системы обновлений с интеграцией Kubernetes и Ansible. Для развертывания тестовых кластеров и отработки плейбуков перед запуском в production удобно использовать облачную инфраструктуру - Timeweb Cloud предоставляет готовые серверы и управляемый Kubernetes для экспериментов без риска для боевого окружения.