Меня заинтересовала тема Kubernetes, и я решил освоить его. На начальном этапе все шло хорошо, пока я изучал теорию.
Однако как только дело дошло до практики внезапно выяснилось что по каким то причинам самое быстрое и распространённое решение minicube просто отказывается разворачиваться на моей Fedora. Разворачивание просто зависало на одном из этапов. Причина подозреваю была в не отключенном по умолчанию swap разделе, но на тот момент я не додумал.
Попробовав несколько вариантов с разными виртуальными машинами, я решил что раз не работает minicube, значит надо развернуть более комплексное решение. Подумал и полез в интернет. После прочтения нескольких статей на нашем ресурсе я решил остановиться на этой:
@imbasoft «Гайд для новичков по установке Kubernetes»
В ней было всё разложено по полочкам, все шаги выполнялись легко и непринуждённо. В конце получался замечательный стенд для тренировок. Причем стенд вариативный, с разными движками контейнеризации, и вариантами запуска как аналог миникуба, так и пяти хостовый вариант с управляющими и рабочими нодами.
Но смущал меня один момент, в статье было описано развёртывание со многими снапшотами, чтобы можно было вернуться и переиграть по-новому.
Каждый раз откатываться по нескольким машинам мне не хотелось. А развернуть хотелось.
Решение пришло быстро. Ansible. Можно раскатываться и перераскатываться относительно быстро. В любой момент можно удалить стенд и начать заново.
До этого не работал с ansible, поэтому это так же показалось мне вполне неплохой практикой.
Возможно решение не самое правильное, надеюсь люди в комментариях меня поправят или подскажут более удачные варианты решений. Но пока представляю на суд то что получилось в итоге.
Я знаю что существуют готовые варианты типа kubespray, но тут больше хотелось поработать именно с ansible, так что я решил не упускать возможности. В любом случае не ошибается тот, кто ничего не делает, так что лучше я допущу десяток ошибок новичка, на которые мне может даже укажут, чем вообще не попробую.
Итак, с чего начать?
Во-первых надо было выбрать виртуализацию. KVM показался мне нормальным решением, он можно сказать родной для linux, есть возможность рулить из командной строки.
Я не буду описывать как настраивать KVM на машине и устанавливать ansible, статья не об этом. Предположим что у вас уже всё установлено.
Как я уже написал, опыта с ansible у меня не много, но даже с ним я понимаю что писать одну большую простыню кода не особо удобно, а отлаживать и того хуже. Было решено разбить её на несколько простыней поменьше посредством ролей.
В целом если прочитать оригинальную статью то можно выделить 3 этапа:
1) Подготовка виртуальных машин
2) Установка движка контейнеризации
3) Установка all_in_one/ha_cluster
Исходя из этого будем готовить 4 роли со своими тасками.
— vm_provision
— driver_provision
— k8s_all_in_one
— k8s_ha_cluster
Создаём каталог, у меня он называется kvmlab, и в нем файл setup_k8s.yaml
Это будет главный playbook, из него будут подтягиваться остальные по мере необходимости. Tак же нам понадобится inventory и файл с переменными которыми мы будем управлять развёртыванием. Ну и конечно же роли.
В каталоге выполним, для создания ролей.
ansible-galaxy role init vm_provision ansible-galaxy role init driver_provision ansible-galaxy role init k8s_ha_cluster ansible-galaxy role init k8s_all_in_one
Файл inventory описывает наши ansible_host для подключения:
all: children: management: hosts: node1: ansible_host: 172.30.0.201 node2: ansible_host: 172.30.0.202 node3: ansible_host: 172.30.0.203 workers: hosts: node4: ansible_host: 172.30.0.204 node5: ansible_host: 172.30.0.205
my_vars.yml как видно из названия описывает переменные, параметры развертывания, параметры виртуальных машин, каталоги хранения iso и дисков VM:
variant: all-in-one #[all-in-one, ha-cluster] engine: cri-o #[container-d, cri-o, docker] libvirt_pool_dir: "/home/alex/myStorage/storage_for_VMss" libvirt_pool_images: "/home/alex/myStorage/iso_imagess" vm_net: k8s_net ssh_key: "/home/alex/.ssh/id_rsa.pub" ansible_ssh_common_args: "-o StrictHostKeyChecking=no" version: "1.26" os: "Debian_11" vm_info: vm_names: - name: node1 memory: 2048 cpu: 2 ipaddr: 172.30.0.201 - name: node2 memory: 2048 cpu: 2 ipaddr: 172.30.0.202 - name: node3 memory: 2048 cpu: 2 ipaddr: 172.30.0.203 - name: node4 memory: 3072 cpu: 4 ipaddr: 172.30.0.204 - name: node5 memory: 3072 cpu: 4 ipaddr: 172.30.0.205
Рассмотрим переменные чуть подробнее:
variant — это наш вариант установки будем ли мы устанавливать кластер или ограничимся одной машиной и сделаем аналог minikube.
engine — собственно движок контейнеризации
libvirt_pool_dir и libvirt_pool_images каталоги хранения дисков виртуальных машин и скачанных образов соответственно.
vm_net — имя создаваемой сети для ваших машин.
ssh_key — ваш публичный ключ, подкидывается на ВМ в процессе подготовки и дальнейшие действия выполняются вашим логином из под root.
ansible_ssh_common_args — отключение проверки хеша ключа.
Теперь вернемся к setup_k8s.yaml:
Первый play выполняется на localhost, требует повышенных прав и состоит из 6 task:
-
Подготовка окружения — на этом этапе мы устанавливаем необходимые пакеты для управления libvirt
-
Настройка сети — машины будут использовать свою сеть, но её надо предварительно создать.
-
Подготовка шаблона для ВМ — все машины будут с одинаковой OS, в моём случае с debian 11, у них будет одинаковый набор начальных пакетов. Каждый раз разворачивать с нуля долго, поэтому надо подготовить шаблон VM и переиспользовать его при необходимости.
-
Создание ВМ нод из шаблонного образа. Создание нужного количества VM для развертывания.
-
Перезагрузка созданных машин
-
Создание снапшота. Эта таска опциональна, при дальнейшем развёртывании часто случались ошибки и надо было начинать сначала, снапшот решал эту проблему. в целом сейчас он уже не нужен, но я оставил. Для подготовки будем использовать роль vm_provision о ней чуть позже, а сейчас посмотрим на то что получилось:
kvmlab/setup_k8s.yaml:
--- - name: Подготовка ВМ к развёртыванию k8s hosts: localhost gather_facts: yes become: yes tasks: - name: Подготовка окружения package: name: - libguestfs-tools - python3-libvirt state: present - name: Настройка сети include_role: name: vm_provision tasks_from: create_network.yml - name: Подготовка шаблона для ВМ include_role: name: vm_provision tasks_from: prepare_images_for_cluster.yml - name: Создание ВМ нод из шаблонного образа. include_role: name: vm_provision tasks_from: create_nodes.yml vars: vm_name: "{{ item.name }}" vm_vcpus: "{{ item.cpu }}" vm_ram_mb: "{{ item.memory }}" ipaddr: "{{ item.ipaddr }}" with_items: "{{ vm_info.vm_names }}" when: variant == 'ha-cluster' or (variant == 'all-in-one' and item.name == 'node1') - name: Ожидание загрузки всех ВМ из списка wait_for: host: "{{ hostvars[item].ansible_host }}" port: 22 timeout: 300 state: started when: variant == 'ha-cluster' or item == 'node1' with_items: "{{ groups['all'] }}" - name: Создаем снимок host_provision include_role: name: vm_provision tasks_from: create_snapshot.yml vars: vm_name: "{{ item.name }}" snapshot_name: "host_provision" snapshot_description: "Нода подготовлена к установке движка" when: variant == 'ha-cluster' or item.name == 'node1' with_items: "{{ vm_info.vm_names }}"
Визуально не много, давай разберём что скрывается под include_role.
А под ролью у нас скрывается:
Дефолтные настройки на случай если какие то переменные не заполнены, по иерархии если не ошибаюсь стоят в самом низу, т.е. если эти переменные прилетят откуда от еще, их приоритет будет выше:
kvmlab/roles/vm_provision/defaults/main.yml:
--- # defaults file for vm_provision base_image_name: debian-11-generic-amd64-20230124-1270.qcow2 base_image_url: https://cdimage.debian.org/cdimage/cloud/bullseye/20230124-1270/{{ base_image_name }} base_image_sha: 8db9abe8e68349081cc1942a4961e12fb7f94f460ff170c4bdd590a9203fbf83 libvirt_pool_dir: "/var/lib/libvirt/images" libvirt_pool_images: "/var/lib/libvirt/images" vm_vcpus: 2 vm_ram_mb: 2048 vm_net: vmnet vm_root_pass: test123 ssh_key: /root/.ssh/id_rsa.pub
2 шаблона:
roles/vm_provision/templates/
vm-template.xml.j2 — Шаблон по которому создается виртуальная машина в xml формате. при создании параметры заполняются из заданных переменных.
vm_network.xml.j2 — Шаблон для создания сети которую будут использовать VM.
Я не буду их приводить, вы сможете забрать их в репозитории.
Ну и наконец roles/vm_provision/tasks/
create_network.yml — набор задач для создания сети
create_nodes.yml — набор задач для создания нод
create_snapshot.yml — создание снапшотов
prepare_images_for_cluster.yml — подготовка шаблона
Начнем с подготовки шаблона:
состоит из 4 задач:
-
Создание каталога для хранения исходного образа(если конечно он не существует)
-
Скачивание и проверка базового образа. Каждый раз качать его нет смысла, поэтому скачивается один раз, при повторном запуске, если файл уже лежит на месте эта часть скипается.
-
Базовый образ уже можно подключить к ВМ и работать с ним, однако тогда он перестанет быть базовым, а уже будет кастомизированным. Оставим его как есть, но скопируем его как шаблон для ВМ
-
первичная настройка шаблона. Часть библиотек и ПО для любого варианта развертывания будет одна и та же. Поэтому проще накатить их сразу в шаблон. Так же заполним hosts, по-хорошему его бы заполнять динамически в зависимости от количества нод, но я прописал 5 штук сразу. Сильно не мешает.
kvmlab/roles/vm_provision/tasks/prepare_images_for_cluster.yml:
--- # tasks file vm_provision, создание шаблона ВМ - name: Создание каталога {{ libvirt_pool_images }} если не существует. file: path: "{{ libvirt_pool_images }}" state: directory mode: 0755 - name: Скачивание базового образа если его нет в хранилище get_url: url: "{{ base_image_url }}" dest: "{{ libvirt_pool_images }}/{{ base_image_name }}" checksum: "sha256:{{ base_image_sha }}" - name: Создание копии базового образа, для шаблона copy: dest: "{{ libvirt_pool_images }}/template_with_common_settings.qcow2" src: "{{ libvirt_pool_images }}/{{ base_image_name }}" force: no remote_src: yes mode: 0660 register: copy_results - name: Первичная настройка шаблона. command: | virt-customize -a {{ libvirt_pool_images }}/template_with_common_settings.qcow2 \ --root-password password:{{ vm_root_pass }} \ --ssh-inject 'root:file:{{ ssh_key }}' \ --uninstall cloud-init \ --run-command 'apt update && apt install -y ntpdate gnupg gnupg2 curl software-properties-common wget keepalived haproxy' \ --append-line '/etc/hosts:172.30.0.201 node1.internal node1\n172.30.0.202 node2.internal node2\n172.30.0.203 node3.internal node3\n172.30.0.204 node4.internal node4\n172.30.0.205 node5.internal node5' \ --run-command 'curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmour -o /etc/apt/trusted.gpg.d/cgoogle.gpg' \ --run-command 'apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"' \ --run-command 'apt update && apt install -y kubeadm kubectl' \ --run-command 'echo 'overlay' > /etc/modules-load.d/k8s.conf && echo 'br_netfilter' >> /etc/modules-load.d/k8s.conf' \ --run-command 'echo -e "net.bridge.bridge-nf-call-ip6tables = 1\nnet.bridge.bridge-nf-call-iptables = 1\nnet.ipv4.ip_forward = 1" > /etc/sysctl.d/10-k8s.conf' when: copy_results is changed
Отлично, шаблон готов. далее на очереди создание сети, ибо сеть используется в шаблоне создания ВМ, и если её не будет, то чуда не случится.
Тут все просто, используя virsh мы проверяем создавалась ли сеть ранее. если да, то скипаем, если же нет, то используя шаблон в который будет подставлено имя сети из переменных средствами всё той же virsh будет создана, запущена и выставлена в автозапуск сеть.
kvmlab/roles/vm_provision/tasks/create_network.yml:
--- # tasks file for vm_provision, пересоздание сети - name: Получение списка сетей KVM command: virsh net-list --all register: net_list_output - name: Проверка наличия сети {{ vm_net }} shell: echo "{{ net_list_output.stdout }}" | grep -w "{{ vm_net }}" register: network_check ignore_errors: true - name: Создание и настройка сети {{ vm_net }} block: - name: Копирование шаблона сети template: src: vm_network.xml.j2 dest: /tmp/vm_network.xml - name: Создание сети {{ vm_net }} command: virsh net-define /tmp/vm_network.xml - name: Запуск сети {{ vm_net }} command: virsh net-start {{ vm_net }} - name: Автостарт сети {{ vm_net }} command: virsh net-autostart {{ vm_net }} when: network_check.rc != 0
Так. Шаблон ВМ есть, сеть есть. Ничего не мешает нам создать ноду или ноды:
Создание нод запускается циклом по переменным. (vm_info.vm_names)
ноды создаются по одной и проходят следующие этапы:
-
Опять же создается каталог для хранения дисков виртуальных машин, если он не существует.
-
Каждая машина перед созданием проверяется на наличие её в уже существующих, если она есть, то создание пропускается, так что если у вас осталась машина с прошлого стенда то лучше её пересоздать.
-
Копируется шаблонный образ диска и переименовывается в соответствии с именем ВМ
-
Изменяется размер диска, расширяется до 10 GB, этого объема мне хватило для установки всех вариантов. Значение захардкожено, но при желании его можно так же параметризовать.
-
Начальное конфигурирование ноды. Тут у нод появляется индивидуальность, имя, ip и свой ssh ключ
-
Когда все составные части готовы, создается машина из шаблона xml
-
Запуск ВМ
kvmlab/roles/vm_provision/tasks/create_nodes.yml:
--- # tasks file for vm_provision, создание нод - name: Создание каталога {{ libvirt_pool_dir }} если не существует. file: path: "{{ libvirt_pool_dir }}" state: directory mode: 0755 - name: Получаем список существующих ВМ community.libvirt.virt: command: list_vms register: existing_vms changed_when: no - name: Создание ВМ если её имени нет в списке block: - name: Копирование шаблонного образа в хранилище copy: dest: "{{ libvirt_pool_dir }}/{{ vm_name }}.qcow2" src: "{{ libvirt_pool_images }}/template_with_common_settings.qcow2" force: no remote_src: yes mode: 0660 register: copy_results - name: Изменение размера виртуального диска shell: "qemu-img resize {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 10G" - name: Начальное конфигурирование hostname:{{ vm_name }}, ip:{{ ipaddr }} command: | virt-customize -a {{ libvirt_pool_dir }}/{{ vm_name }}.qcow2 \ --hostname {{ vm_name }}.internal \ --run-command 'echo "source /etc/network/interfaces.d/*\nauto lo\niface lo inet loopback\nauto enp1s0\niface enp1s0 inet static\naddress {{ ipaddr }}\nnetmask 255.255.255.0\ngateway 172.30.0.1\ndns-nameservers 172.30.0.1" > /etc/network/interfaces' --run-command 'ssh-keygen -A' when: copy_results is changed - name: Создание ВМ из шаблона community.libvirt.virt: command: define xml: "{{ lookup('template', 'vm-template.xml.j2') }}" when: "vm_name not in existing_vms.list_vms" - name: Включение ВМ community.libvirt.virt: name: "{{ vm_name }}" state: running register: vm_start_results until: "vm_start_results is success" retries: 15 delay: 2
Отлично, машины созданы. теперь перезагрузим их, дождемся пока все загрузятся и сделаем снапшоты.
kvmlab/roles/vm_provision/tasks/create_snapshot.yml:
--- # tasks file for vm_provision, создание снапшотов - name: Создание снапшота {{ snapshot_name }} shell: "virsh snapshot-create-as --domain {{ vm_name }} --name {{ snapshot_name }} --description '{{ snapshot_description }}'" register: snapshot_create_status ignore_errors: true
Если ничего не забыл, то первый этап выполнен.
У вас есть одна или пять нод, все готовы к дальнейшей работе.
Причем если удалить все ВМ и запустить создание повторно, то из за наличия готового шаблона процесс пройдёт гораздо быстрее.
Отлично. переходим к установке движка:
вернемся в setup_k8s.yaml и добавим следующий play:
- name: Установка движка контейнеризации [cri-o, container-d, docker] hosts: all gather_facts: true become: true remote_user: root tasks: - name: Синхронизация даты/времени с NTP сервером shell: ntpdate 0.europe.pool.ntp.org - name: Установка cri-o include_role: name: driver_provision tasks_from: install_crio.yml when: engine == "cri-o" - name: Установка container-d include_role: name: driver_provision tasks_from: install_container_d.yml when: engine == "container-d" - name: Установка docker cri include_role: name: driver_provision tasks_from: install_docker_cri.yml when: engine == "docker"
В целом всё просто, используем роль driver_provision, но в зависимости от установленных параметров запускаем одну из трех последовательностей.
Вся последовательность действий для каждого из движков была взята из статьи указанной вначале.
Я не буду подробно комментировать таски, в целом их имена отражают суть всех действий.
приведём все три варианта:
kvmlab/roles/driver_provision/tasks/install_container_d.yml:
--- # tasks file for driver_provision, установка container-d - name: Скачиваем containerd get_url: url: "https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-linux-amd64.tar.gz" dest: "/tmp/containerd-1.7.0-linux-amd64.tar.gz" - name: Распаковываем архив unarchive: src: /tmp/containerd-1.7.0-linux-amd64.tar.gz dest: /usr/local copy: no - name: Удаляем скачаный архив за ненадобностю file: path: "/tmp/containerd-1.7.0-linux-amd64.tar.gz" state: absent - name: Создание директории для конфигурации containerd file: path: /etc/containerd/ state: directory - name: Проверяем создан ли каталог stat: path: /etc/containerd register: containerd_dir - name: Создание конфиг файла containerd become: true command: "sh -c 'containerd config default > /etc/containerd/config.toml'" when: containerd_dir.stat.exists - name: конфигурирование cgroup driver replace: path: "/etc/containerd/config.toml" regexp: "SystemdCgroup = false" replace: "SystemdCgroup = true" - name: Скачиваем containerd systemd service file get_url: url: "https://raw.githubusercontent.com/containerd/containerd/main/containerd.service" dest: "/etc/systemd/system/containerd.service" - name: Скачиваем и устанавливаем runc get_url: url: "https://github.com/opencontainers/runc/releases/download/v1.1.4/runc.amd64" dest: "/usr/local/sbin/runc" mode: "u+x" - name: Скачиваем CNI plugins get_url: url: "https://github.com/containernetworking/plugins/releases/download/v1.2.0/cni-plugins-linux-amd64-v1.2.0.tgz" dest: "/tmp/cni-plugins-linux-amd64-v1.2.0.tgz" - name: Распаковываем CNI plugins archive unarchive: src: "/tmp/cni-plugins-linux-amd64-v1.2.0.tgz" dest: "/opt/cni/bin" copy: no - name: Удаляем CNI plugins archive file: path: "/tmp/cni-plugins-linux-amd64-v1.2.0.tgz" state: absent - name: Перезагрузка systemd systemd: daemon_reload: yes - name: Запуск и активация containerd service systemd: name: containerd state: started enabled: yes
kvmlab/roles/driver_provision/tasks/install_crio.yml:
--- # tasks file for driver_provision, установка cri-o - name: Установка ключа репозитория cri-o apt_key: url: https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/{{ version }}/{{ os }}/Release.key state: present - name: Установка репозитория cri-o apt_repository: repo: 'deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/{{ os }}/ /' filename: devel:kubic:libcontainers:stable.list - name: Установка репозитория cri-ostable/cri-o apt_repository: repo: 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/{{ version }}/{{ os }}/ /' filename: 'devel:kubic:libcontainers:stable:cri-o:{{ version }}.list' - name: Установка cri-o apt: name: ['cri-o', 'cri-o-runc'] state: latest - name: Создание каталога /var/lib/crio file: path: /var/lib/crio state: directory - name: Перезагрузка systemd systemd: daemon_reload: yes - name: запуск служб crio systemd: name: crio enabled: yes state: started
kvmlab/roles/driver_provision/tasks/install_docker_cri.yml:
--- # tasks file for driver_provision, установка docker + cri - name: Create directory /etc/apt/keyrings file: path: /etc/apt/keyrings state: directory mode: '0755' - name: Add GPG key Docker ansible.builtin.shell: curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg --yes - name: Get dpkg architecture shell: "dpkg --print-architecture" register: architecture - name: Get lsb release shell: "lsb_release -cs" register: release_output - name: Add Docker repository apt_repository: repo: "deb [arch={{ architecture.stdout_lines | join }} signed-by=/etc/apt/trusted.gpg.d/docker.gpg] https://download.docker.com/linux/debian {{ release_output.stdout_lines | join }} stable" state: present register: docker_repo - name: Apt Update ansible.builtin.apt: update_cache: yes - name: Install Docker apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin state: present - name: Download plugin cri-dockerd get_url: url: "https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.1/cri-dockerd-0.3.1.amd64.tgz" dest: "/tmp/cri-dockerd.tgz" - name: Unpack cri-dockerd unarchive: src: "/tmp/cri-dockerd.tgz" dest: "/tmp/" copy: no - name: Copy unpacked bin cri-dockerd copy: dest: "/usr/local/bin/" src: "/tmp/cri-dockerd/cri-dockerd" force: no remote_src: yes mode: 0660 register: copy_results - name: change alc on cri-dockerd file: path: "/usr/local/bin/cri-dockerd" mode: "0755" - name: Download config file on cri-dockerd.service get_url: url: "https://raw.githubusercontent.com/Mirantis/cri-dockerd/master/packaging/systemd/cri-docker.service" dest: "/etc/systemd/system/cri-docker.service" - name: Download config file on cri-dockerd.socket get_url: url: "https://raw.githubusercontent.com/Mirantis/cri-dockerd/master/packaging/systemd/cri-docker.socket" dest: "/etc/systemd/system/cri-docker.socket" - name: Update cri-docker.service ansible.builtin.shell: "sed -i -e 's,/usr/bin/cri-dockerd,/usr/local/bin/cri-dockerd,' /etc/systemd/system/cri-docker.service" - name: daemon reload systemd: daemon_reload: yes - name: enable cri-docker.service systemd: name: cri-docker.service enabled: yes state: started - name: enable cri-dockerd.socket systemd: name: cri-docker.socket enabled: yes state: started
Так, готово. после этапа установки движка идёт еще один play для localhost для создания снапшота.
- name: Создаем снапшот driver_provision hosts: localhost become: yes tasks: - name: Создаем снимки include_role: name: vm_provision tasks_from: create_snapshot.yml vars: vm_name: "{{ item.name }}" snapshot_name: "driver_provision" snapshot_description: "Движок установлен, нода подготовлена к инициализации k8s" when: variant == 'ha-cluster' or item.name == 'node1' with_items: "{{ vm_info.vm_names }}"
В целом так же опциональный, можно удалить.
Bтак, осталось самое важное, ради чего всё это начиналось.
Инициализация кубера!
возвращаемся в setup_k8s.yaml и дописываем следующий play.
- name: Настройка kubernetes [all-in-one либо ha-cluster] hosts: all gather_facts: true become: true remote_user: root tasks: - name: Установка all-in-one include_role: name: k8s_all_in_one tasks_from: all_in_one.yml when: variant == "all-in-one" and inventory_hostname == 'node1' - name: Подготовка нод для ha-cluster include_role: name: k8s_ha_cluster tasks_from: ha_cluster_prepare_managers.yml when: variant == "ha-cluster" - name: Установка первой ноды include_role: name: k8s_ha_cluster tasks_from: ha_cluster_first_node.yml when: variant == "ha-cluster" and inventory_hostname == 'node1' and inventory_hostname in groups['management'] register: first_node_result - name: Передача команд на остальные ноды set_fact: control_plane_join_command: "{{ hostvars['node1']['control_plane_join_command'] }}" worker_join_command: "{{ hostvars['node1']['worker_join_command'] }}" when: variant == "ha-cluster" and inventory_hostname != 'node1' - name: вывод команд подключения debug: msg: | control_plane_join_command: {{ control_plane_join_command }} worker_join_command: {{ worker_join_command }} when: variant == "ha-cluster" and inventory_hostname == 'node1' - name: Использование команды control_plane_join_command block: - name: Подключение управляющих нод для ['container-d', 'cri-o'] ansible.builtin.shell: cmd: "{{ control_plane_join_command }}" until: result.rc == 0 register: result retries: 5 delay: 30 when: engine in ['container-d', 'cri-o'] - name: Подключение управляющих нод для docker ansible.builtin.shell: cmd: "{{ control_plane_join_command }} --cri-socket unix:///var/run/cri-dockerd.sock" until: result.rc == 0 register: result retries: 5 delay: 30 when: engine == 'docker' when: variant == "ha-cluster" and inventory_hostname != 'node1' and inventory_hostname in groups['management'] - name: Использование команды worker_join_command block: - name: Подключение рабочих нод для ['container-d', 'cri-o'] ansible.builtin.shell: cmd: "{{ worker_join_command }}" until: result.rc == 0 register: result retries: 5 delay: 30 when: engine in ['container-d', 'cri-o'] - name: Подключение рабочих нод для docker ansible.builtin.shell: cmd: "{{ worker_join_command }} --cri-socket unix:///var/run/cri-dockerd.sock" until: result.rc == 0 register: result retries: 5 delay: 30 when: engine == 'docker' when: variant == "ha-cluster" and inventory_hostname != 'node1' and inventory_hostname in groups['workers'] - name: Скачивание конфига с первой ноды (подходит для обоих вариантов all-in-one и ha-cluster) ansible.builtin.fetch: src: /etc/kubernetes/admin.conf dest: /tmp/ flat: yes force: yes when: inventory_hostname == 'node1' - name: Перезагрузка всех машин ansible.builtin.reboot: reboot_timeout: 300
Тут для установки используются две роли (можно было и одной обойтись но так нагляднее).
Начнем пожалуй с all-in-one варианта установки, он самый простой:
roles/k8s_all_in_one/tasks/all_in_one.yml:
--- - name: Проверка наличия файла конфига stat: path: /etc/kubernetes/admin.conf register: file_info - name: Инициализация кластера если конфиг не обнаружен. block: - name: Инициализация кластера для движков ['container-d', 'cri-o'] shell: kubeadm init --pod-network-cidr=10.244.0.0/16 when: engine in ['container-d', 'cri-o'] register: kubeadm_output - name: Инициализация кластера для движка docker shell: | kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --cri-socket unix:///var/run/cri-dockerd.sock when: engine == 'docker' register: kubeadm_output - name: Установка KUBECONFIG в enviroment become: true lineinfile: dest: /etc/environment line: 'export KUBECONFIG=/etc/kubernetes/admin.conf' - name: Установка KUBECONFIG в bashrc become: true lineinfile: dest: '~/.bashrc' line: 'export KUBECONFIG=/etc/kubernetes/admin.conf' - name: Подождем пока всё запустится wait_for: host: localhost port: 6443 timeout: 300 - name: Установка сетевого плагина Flannel shell: kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml - name: Снятие ограничения на запуск рабочих нагрузок c {{ ansible_hostname }} shell: "kubectl taint nodes --all node-role.kubernetes.io/control-plane-" become:roles/k8s_all_in_one/tasks/all_in_one.yml: true register: taint_result failed_when: - "'error: taint \"node-role.kubernetes.io/control-plane\" not found' not in taint_result.stderr" - "'node/' + ansible_hostname + '.internal untainted' not in taint_result.stdout" when: not file_info.stat.exists - name: Проверка инициализации shell: "export KUBECONFIG=/etc/kubernetes/admin.conf && kubectl get nodes" register: kubectl_output ignore_errors: true - name: Инициализация завершена. debug: msg: 'Инициализация завершена! выполните комманду export KUBECONFIG=/etc/kubernetes/admin.conf, проверьте вывод команды kubectl get nodes' when: kubectl_output.rc == 0
Что в нем происходит.
Проверяем есть ли файл конфига. логика проста, если файл есть то с большой долей вероятности инициализация уже была и отчасти успешна. в этом случае если не работает, то лучше убить ноду и собрать заново.
Если же файла нет, то в зависимости от движка идёт команда инициализации (для докера она идёт с доп параметрами).
Устанавливается сетевой плагин, снимаются ограничения и проверяется установка.
всё, стенд готов.
Теперь давай пробежимся по ha_cluster.
Тут всё немного сложнее.
первое что надо сделать это подготовить ноды, а именно настроить keepalived и haproxy, для обеспечения отказоустойчивости и балансировки нагрузки.
roles/k8s_ha_cluster/tasks/ha_cluster_prepare_managers.yml
- name: Синхронизация даты/времени с NTP сервером shell: ntpdate 0.europe.pool.ntp.org - name: Копируем настройку демона keepalived template: src: templates/keepalived.conf.j2 dest: /etc/keepalived/keepalived.conf mode: '0644' - name: Копируем скрипт check_apiserver.sh, предназначенный для проверки доступности серверов. template: src: templates/check_apiserver.sh.j2 dest: /etc/keepalived/check_apiserver.sh mode: '0755' - name: запуск службы keepalived systemd: name: keepalived enabled: yes state: restarted - name: Копируем настройку демона haproxy template: src: templates/haproxy.cfg.j2 dest: /etc/haproxy/haproxy.cfg mode: '0644' - name: запуск службы haproxy systemd: name: haproxy enabled: yes state: restarted
Вторым шагом удет установка первой ноды. важный процесс, ибо после инициализации кубера он выдает команды для добавления новых хостов, которые нам надо будет передать на выполнение следующим нодам.
В целом суть та же, проверяем конфиг, если его нет, то делаем инициализацию. для docker команда чуть побольше.
После инициализации фильтруем вывод регуляркой и сохраняем для передачи остальным нодам. экспортируем конфиг, устанавливаем сетевой плагин и идём дальше.
ha_cluster_first_node.yml: - name: Проверка наличия файла конфига stat: path: /etc/kubernetes/admin.conf register: file_info - name: Инициализация кластера если конфиг не обнаружен. block: - name: Инициализация кластера для движков ['container-d', 'cri-o'] shell: | kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --control-plane-endpoint "172.30.0.210:8888" \ --upload-certs register: init_output_containerd_crio when: engine in ['container-d', 'cri-o'] - name: Инициализация кластера для движка ['docker'] shell: | kubeadm init \ --cri-socket unix:///var/run/cri-dockerd.sock \ --pod-network-cidr=10.244.0.0/16 \ --control-plane-endpoint "172.30.0.210:8888" \ --upload-certs register: init_output_docker when: engine == 'docker' - name: Сохранение значения init_output для дальнейшего использования set_fact: init_output: "{{ init_output_containerd_crio if init_output_containerd_crio is defined and init_output_containerd_crio.stdout is defined else init_output_docker }}" - name: Фильтрация вывода kubeadm init set_fact: filtered_output: "{{ init_output.stdout | regex_replace('(\\n|\\t|\\\\n|\\\\)', ' ') }}" - name: Фильтр комманд для добавления управляющих и рабочих нод set_fact: control_plane_join_command: "{{ filtered_output | regex_search('kubeadm join(.*?--discovery-token-ca-cert-hash\\s+sha256:[\\w:]+.*?--control-plane.*?--certificate-key.*?[\\w:]+)')}}" worker_join_command: "{{ filtered_output | regex_search('kubeadm join(.*?--discovery-token-ca-cert-hash\\s+sha256:[\\w:]+)')}}" - name: Установка KUBECONFIG в enviroment lineinfile: dest: /etc/environment line: 'export KUBECONFIG=/etc/kubernetes/admin.conf' - name: Установка KUBECONFIG в bashrc lineinfile: dest: '~/.bashrc' line: 'export KUBECONFIG=/etc/kubernetes/admin.conf' - name: Подождем пока всё запустится wait_for: host: localhost port: 6443 timeout: 300 - name: Установка сетевого плагина Flannel shell: kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml when: not file_info.stat.exists
После успешной инициализации кластера и получения команд для добавления нод, мы используем эти команды чтобы добавить управляющие и рабочие ноды.
И снова ветвление ибо у докера есть доп параметры при установке.
Есть нюанс, управляющие ноды иногда по неизвестной мне причине не добавлялись. при этом при повторном запуске команды всё проходило нормально. Поэтому я добавил 5 попыток подключения. обычно хватает двух.
С воркерами такого не наблюдалось, однако я всё равно добавил те же 5 попыток.
Воркер или управляющая нода определяется из группы в inventory.
Готово, перезагружаем все машины и ждем загрузки.
Последний play скопирует конфиг с первой ноды на вашу локальную машину. чтобы можно было управлять кластером непосредственно с хоста. он так же опционален, можно просто зайти на первую ноду и запускать деплои оттуда.
- name: Настройка хостовой машины, чтобы не лазить постоянно на виртуальные. hosts: localhost gather_facts: false tasks: - name: Переместить файл ansible.builtin.file: src: /tmp/admin.conf dest: /etc/kubernetes/admin.conf state: link force: yes become: true - name: Установка KUBECONFIG в enviroment lineinfile: dest: /etc/environment line: 'export KUBECONFIG=/etc/kubernetes/admin.conf' - name: Установка KUBECONFIG в bashrc lineinfile: dest: '~/.bashrc' line: 'export KUBECONFIG=/etc/kubernetes/admin.conf'
Бонусом идёт удаление стенда. раз он быстро создаётся то должен быстро и исчезать.
Удаляются только ВМ их диски и снапшоты, шаблоны и образы остаются в каталогах хранения
remove_stand.yml:
--- - name: Удаление стенда kubernetes hosts: localhost become: trueс vars_files: - my_vars.yml tasks: - name: Получаем список существующих ВМ community.libvirt.virt: command: list_vms register: existing_vms changed_when: no - name: Удаление машин block: - name: Полностью останавливаем ВМ community.libvirt.virt: command: destroy name: "{{ item.name }}" loop: "{{ vm_info.vm_names }}" when: "item.name in existing_vms.list_vms" ignore_errors: true - name: Удаляем снапшоты shell: | virsh snapshot-delete --domain {{ item.name }} --snapshotname host_provision virsh snapshot-delete --domain {{ item.name }} --snapshotname driver_provision ignore_errors: true loop: "{{ vm_info.vm_names }}" when: "item.name in existing_vms.list_vms" - name: Отменяем регистрацию ВМ community.libvirt.virt: command: undefine name: "{{ item.name }}" loop: "{{ vm_info.vm_names }}" when: "item.name in existing_vms.list_vms" - name: Удаление диска виртуальной машины ansible.builtin.file: path: "{{libvirt_pool_dir}}/{{ item.name }}.qcow2" state: absent loop: "{{ vm_info.vm_names }}" when: "item.name in existing_vms.list_vms"
В целом всё готово. можно запускать.
Установка стенда:
ansible-playbook -K ./setup_k8s.yaml -i ./inventory --extra-vars "@my_vars.yml"
Удаление стенда:
ansible-playbook -K ./remove_stand.yml
В общем и целом я просто перенёс готовый гайд на рельсы автоматизации, отсебятины я добавил по минимуму. где то иначе добавляются репозитории и ключи, где то запускаю синхронизацию времени, из за того что при восстановлении со снапшота у меня начинались проблемы с ключами из за неверной текущей даты.
Добавляю ссылку на репозиторий со всем этим добром.
В решении я попробовал много различных элементов управления ansible, создание каталогов, циклы, ветвления, установки и прочие кирпичи из которых вырастает система.
Буду рад если вы подскажете какие решения были удачными, а какие не очень. эта информация будет очень полезна для меня.
Спасибо что осилили и прочли до конца!)
Отдельное спасибо @imbasoft за отличную и понятную статью.
ссылка на оригинал статьи https://habr.com/ru/articles/751582/
Добавить комментарий