
Всем привет. Меня зовут Путилин Дмитрий (Добрый Кот) Telegram.
От коллектива FR-Solutions и при поддержке @irbgeo Telegram : Продолжаем серию статей о K8S.
В этой статье мы поделимся своим опытом разработки Managed K8S под Yandex Cloud и расскажем, как мы создали конфигурацию, которую можно легко адаптировать для запуска в любом облаке или on-premises решении, изменяя только некоторые настройки. Если вы заинтересованы в построении гибких и масштабируемых Kubernetes-кластеров, то этот материал обязательно для вас.
В предыдущих статьях
Базовая организации сертификатов в kubeadm — Сертификаты K8S или как распутать вермишель Часть 1.
Как начать использовать внешний PKI сторедж Vault для хранения и выписывания сертификатов для k8s control‑plane — Сертификаты K8S или как распутать вермишель Часть 2.
Как развернуть Kubernetes кластер по принципу Hard Way — Kubernetes the hard way.
Проблема
Из моего личного опыта могу сказать, что Managed решения в облаках или в онпрем‑серверах — это отличный инструмент для создания своего продукта, и зачастую этого достаточно. Однако, бывают ситуации, когда нужно больше гибкости и возможностей настройки, а Managed решение предоставляет ограниченный набор функций.
Для нас было критично использовать сетевой плагин Cilium с нашими настройками, также нам требовались флаги feature‑gates, которых по дефолту нет в Yandex K8S API.
В принципе, не беда, мы всегда можем развернуть стационарный K8S и закастомизировать его как нам угодно. Возникли следующие вопросы: какие инструменты взять, какой выстроить процесс и как сделать так, что бы создаваемые кластера были одинаковыми?
Выбор инструментов
Для данной задачи однозначно требуются cloud native инструменты, поэтому выбор пал на Terraform. Остались вопросы: как настраивать узлы, нужен ли нам Ansible, Puppet, SaltStack? После 3 месяцев поиска золотой пилюли мы поняли, что для создания кластера нам потребуется только Terraform и cloud‑init.
Архитектура
Так как в основе нашего продукта лежит Terraform, то одно из условий работы с ним — Сервисно‑ресурсная модель (СРМ).
Ресурсами выступают все его компоненты, от балансировщика нагрузки до конфигураци cloud‑init для нашего кластера.
Также CPM позволяет менять одинаковые типы ресурсов без потребности в смене процесса деплоя кластера, таким образом, описав модули создания инфраструктуры под Yandex Cloud, VK Cloud и т. п., и, поменяв намеример модуль Yandex cloud на модуль VK Cloud, получим тот же результат, но в другом окружении.

Сертификаты
Наиболее значимым и сложным этапом было разработать подход работы с сертификатами, проблема была упомянута в предыдущих статьях. Мы определили основные спецификации для сертификатов и описали ресурсы Vault, которые создаются на основе содержимого спецификации. Однако возник вопрос доставки ключей/токенов на мастер-узлы, чтобы клиент на узле мог запросить сертификаты, указанные в спецификации. Было рассмотрено несколько вариантов решения этой проблемы:
Для получения secret_id и role_id от Approle можно использовать временный токен, который имеет ограниченный доступ. Для этого токен должен иметь достаточно длительный срок жизни, чтобы виртуальная машина успела запустить клиента, или можно указать, что использование токена допустимо только один раз.
Использование сервиса IAM от облачного провайдера для сохранения secret_id/role_id для каждой машины в облаке. Затем, можно использовать cloud-cli для получения необходимых секретов прямо с хоста.
Мы предпочли второй вариант и выбрали его, так как он лучше подходил для нашего случая. Однако первый вариант может быть полезен в тех облаках, где нет поддержки сервиса IAM.
Переменные окружения
При написании кода мы поняли, что описывать каждый модуль с его входными и выходными переменными - это трудоемкий процесс, особенно когда возникают повторы. Через некоторое время мы решили, что имеет смысл выделить отдельный модуль, содержащий переменные, которые используются в нескольких модулях. Таким образом, мы смогли уменьшить объем входных аргументов каждого модуля и привести их к более компактному формату: каталог
variable "k8s_global_vars" { description = "module:K8S-GLOBAL VARS" type = any default = {} }
При создании структуры этого модуля мы также уделяли внимание принципу "записал - забыл" - это означает, что если мы хотим добавить только переменную, но нехотим добавлять соответствующий вывод в OUTPUT, нам нужно использовать структурные массивы, в которые мы добавляем только нужные нам переменные, а глобальный вывод остается единым на блок. Например:
locals { k8s-addresses = { local_api_address = format("%s.1", join(".", slice(split(".",local.k8s_network.service_cidr), 0, 3)) ) dns_address = format("%s.10", join(".", slice(split(".",local.k8s_network.service_cidr), 0, 3)) ) idp_provider_fqdn = format("auth.%s" , local.cluster_metadata.base_domain) base_cluster_fqdn = format("%s.%s" , local.cluster_metadata.cluster_name, local.cluster_metadata.base_domain) wildcard_base_cluster_fqdn = format("%s.%s.%s", "*" , local.cluster_metadata.cluster_name, local.cluster_metadata.base_domain) etcd_server_lb_fqdn = format("%s.%s.%s", "etcd" , local.cluster_metadata.cluster_name, local.cluster_metadata.base_domain) } } output "k8s-addresses" { value = local.k8s-addresses }
Генерация cloud-init конфигурации является не менее важным аспектом, поскольку эта конфигурация передается виртуальной машине при ее создании.
В первых версиях мы были вынуждены описывать каждый файл, создавать шаблоны для них и выносить их в отдельные модули по логическому смыслу, например, модуль containerd" включал в себя конфигурационные файлы и шаблоны для systemd сервисов. Однако, такой подход был слишком трудоемким в поддержке из-за большого количества модулей.
Мы решили использовать подход, подобный kubeadm. Сначала мы попытались развернуть кластер с помощью kubeadm, но выяснилось, что он не может выполнить первоначальную настройку системы, такую как установка пакетов, добавление конфигурационных файлов и запуск сервисов. Поэтому мы начали разработку инструмента, который бы мог настроить систему до требуемого состояния. Результатом этой работы стал fraimctl - инструмент, который заменил множество шаблонов одной командой fraimctl init.
Таким образом, нам оставалось описать:
базовый конфиг fraimctl (устанавливает все компоненты и готовит конфиги к ним);
базовый конфиг kubeadm (генерит статик под манифесты и чекает, что кластер поднят);
базовый конфиг key-keeper (клиент который запрашивает сертификаты).
У нас есть несколько задач, которые мы должны выполнить, чтобы полностью отказаться от kubeadm. Мы планируем перенести этап создания конфигурационных файлов key-keeper, kubeconfig и static pod manifests в fraimctl. Кроме того, мы добавим функционал для проверки готовности сертификатов и кластера, а также этап маркировки узлов. Это позволит нам полностью отказаться от использования kubeadm и не зависеть от этого инструмента.
Как уже упоминалось ранее, этот инструмент создан для возможности полного отказа от использования kubeadm и настройки кластеров без его использования.
Пример конфигурациооного файла:
fraimctl.conf
- apiVersion: fraima.io/v1alpha kind: Containerd spec: service: extraArgs: # This document provides the description of the CRI plugin configuration. # The CRI plugin config is part of the containerd config # Default: /etc/containerd/config.toml config: /etc/kubernetes/containerd/config.toml configuration: extraArgs: version: 2 plugins: io.containerd.grpc.v1.cri: containerd: runtimes: runc: # Runtime v2 introduces a first class shim API for runtime authors to integrate with containerd. # The shim API is minimal and scoped to the execution lifecycle of a container. runtime_type: "io.containerd.runc.v2" options: # While containerd and Kubernetes use the legacy cgroupfs driver for managing cgroups by default, # it is recommended to use the systemd driver on systemd-based hosts for compliance of the "single-writer" rule of cgroups. # To configure containerd to use the systemd driver, set the following option: SystemdCgroup: true downloading: - name: containerd src: https://github.com/containerd/containerd/releases/download/v1.6.6/containerd-1.6.6-linux-amd64.tar.gz checkSum: src: https://github.com/containerd/containerd/releases/download/v1.6.6/containerd-1.6.6-linux-amd64.tar.gz.sha256sum type: "sha256" path: /usr/bin/ owner: root:root permission: 0645 unzip: status: true files: - bin/containerd - bin/containerd-shim - bin/containerd-shim-runc-v1 - bin/containerd-shim-runc-v2 - bin/containerd-stress - bin/ctr - name: runc src: https://github.com/opencontainers/runc/releases/download/v1.1.3/runc.amd64 path: /usr/bin/ owner: root:root permission: 0645 starting: - systemctl enable containerd - systemctl start containerd
Каждый компонент имеет четыре стадии:
downloading (загружает бинарные файлы, проверяет контрольные суммы, распаковывает необходимые компоненты и размещает их в соответствующих папках.)
service (генерирует службу systemd, и с помощью параметра extraArgs можно настроить ее поведение под свои нужды.)
configuration (генерирует конфигурацию для службы systemd, и с помощью параметра extraArgs можно настроить ее поведение под свои нужды.)
starting (выполняет необходимые команды после первых трех этапов.)
Одной из ключевых особенностей этого инструмента является этап загрузки (Downloading), который загружает бинарные файлы компонентов. Это позволяет не зависеть от производителя операционной системы и разворачивать единым подходом на любом хосте, не нужно думать о множестве условий (if else) и о том какая операционная система в основе.
Также предусмотрены отдельные конфигурационные файлы для настройки sysctl и modprobe.
fraimctl.conf
- apiVersion: fraima.io/v1alpha kind: Sysctl spec: configuration: extraArgs: net.ipv4.ip_forward: 1 starting: - sudo sysctl --system - apiVersion: fraima.io/v1alpha kind: Modprob spec: configuration: extraArgs: - br_netfilter - overlay starting: - sudo modprobe overlay - sudo modprobe br_netfilter - sudo sysctl --system
Инфраструктура

Каждый кубик в Terraform представляет собой ресурс и логически определяется как класс в языке программирования. Мы можем определить класс, например, loadBalancer, который принимает определенный набор аргументов и возвращает структуру, которая также заранее определена. Это означает, что мы можем изменять кубики по нашему усмотрению, а при смене облака все компоненты будут взаимодействовать друг с другом благодаря структуре входных и выходных параметров.
Благодаря этой архитектуре мы можем обновлять операционные системы без проблем и даже менять производителя операционной системы на лету.
kubectl get no -o wide
root@master-2-cluster-2:/home/dkot# kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME master-1-cluster-2 Ready control-plane,master 2m50s v1.23.12 10.1.0.11 51.250.66.122 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.8 master-2-cluster-2 Ready control-plane,master 2m53s v1.23.12 10.2.0.33 84.201.139.95 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.8 master-3-cluster-2 Ready control-plane,master 2m55s v1.23.12 10.3.0.21 51.250.40.244 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.8 root@master-2-cluster-2:/home/dkot# kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME master-1-cluster-2 Ready control-plane,master 37s v1.23.12 10.1.0.12 62.84.119.244 Debian GNU/Linux 10 (buster) 4.19.0-18-amd64 containerd://1.6.8 master-2-cluster-2 Ready control-plane,master 12m v1.23.12 10.2.0.16 51.250.27.187 Debian GNU/Linux 10 (buster) 4.19.0-18-amd64 containerd://1.6.8 master-3-cluster-2 Ready control-plane,master 12m v1.23.12 10.3.0.13 51.250.45.49 Debian GNU/Linux 10 (buster) 4.19.0-18-amd64 containerd://1.6.8
Для каждого облака необходимо написать модуль, который повторяет структуру выше, чтобы у нас всегда была одинаковая архитектура на всех кластерах.
Реализация
Давайте рассмотрим базовый проект и то, как можно начать использовать этот инструмент.
Скачиваем репозиторий https://github.com/fraima/kubernetes
В этом репозитории есть несколько разделов
infrastructure-vault (создает рут PKI в Vault)
infrastructure-yandex (создает базовую конфигурацию в YC, которая включает в себя создание VPC, таблицы маршрутизации и создание сервисных аккаунтов по умолчанию)
infrastructure-keycloak (устанавливает базовую конфигурацию для Keycloak, которая позволяет авторизоваться в кластере через этот инструмент)
k8s-yandex-cluster (проект-шаблон, который используется для создания кластера.)
Заходим в каждый раздел по очереди и применяем, что прописано в Readme.
Подготовка
Для начала работы вам понадобятся переменные для подключения к Vault, YC и Keycloak.
environments
export TF_VAR_YC_CLOUD_ID="" export TF_VAR_YC_FOLDER_ID="" export TF_VAR_YC_TOKEN="" export TF_VAR_YC_ZONE="" export TF_VAR_VAULT_TOKEN="" export TF_VAR_VAULT_ADDR="" export TF_VAR_KEYCLOAK_REALM="" export TF_VAR_KEYCLOAK_CLIENT_ID="" export TF_VAR_KEYCLOAK_USER="" export TF_VAR_KEYCLOAK_PASSWORD="" export TF_VAR_KEYCLOAK_URL=""
Этот подход позволяет использовать Terraform в контейнере через инструмент CI/CD, не указывая реальные значения переменных в провайдерах.
Если вы работаете с чистым Terraform, не забывайте выделять каждый кластер в отдельный workspace.
terraform workspace new example terraform plan -var-file vars/example.tfvars terraform apply -var-file vars/example.tfvars terraform destroy -var-file vars/example.tfvars
Инит конфиг
Основная конфигурация зависит от двух файлов в проекте.
locals.defaults.tf - базовые значения, которые определены для всех наших кластеров.
vars/${cluster_name}.tf - переменные, которые специально указаны для конкретного кластера.
vars/${cluster_name}.tf
global_vars = { cluster_name = "example" pod_cidr = "10.102.0.0/16" serviceaccount_k8s_controllers_name = "yandex-k8s-controllers" kube_apiserver_flags = { oidc-issuer-url = "https://auth.dobry-kot.ru/auth/realms/master" oidc-client-id = "kubernetes-clusters" oidc-username-claim = "sub" oidc-groups-claim = "groups" oidc-username-prefix = "-" } kube_controller_manager_flags = { cluster-name = "kubernetes" } kube_scheduler_flags = { } addons = { cilium = { enabled = true extra_values = { cluster = { name = "example" id = 12 } } } vault-issuer = { enabled = true extra_values = {} } coredns = { enabled = true extra_values = {} } gatekeeper = { enabled = true extra_values = {} } certmanager = { enabled = true extra_values = {} } machine-controller-manager = { enabled = true extra_values = {} } yandex-cloud-controller = { enabled = true extra_values = {} } yandex-csi-controller = { enabled = true extra_values = {} } compute-instance = { enabled = true custom_values = { subnet_id = "e9bndv0b3c5asheadg09" zone = "ru-central1-a" image_id = "fd8ingbofbh3j5h7i8ll" replicas = 1 } extra_values = { metadata = { nodeLabels = { "node-role.kubernetes.io/worker" = "" "provider" = "yandex" } cloudLabels = { tair = "critical" } } } } } } cloud_metadata = { cloud_name = "cloud-uid-vf465ie7" folder_name = "example" } master_group = { name = "master" count = 3 default_subnet = "10.0.0.0/24" default_zone = "ru-central1-a" metadata = { # user_data_template = "fraima-hbf" user_data_template = "fraima" } }
Этот ENV-параметр дает возможность изменить значения, которые будут использованы в конфигурационных файлах или ресурсах в будущем.
Например, мы можем изменить или добавить флаги Kube-apiserver с помощью переменной "kube_apiserver_flags".
В файле с переменными на данный момент определены три группы.
"master_group" определяет, какие мастера следует заказать, в какой подсети они будут находиться, в какой зоне, будут ли они в разных зонах или нет, а также количество мастер-нод (это значение можно определить только один раз, изменить его с 1 на 3 в настоящее время невозможно).
"global_vars" определяет будущую конфигурацию кластера, включая его имя, подсети для подов, флаги для компонент, которые будут использоваться, а также какие аддоны будут добавлены.
"cloud_metadata" содержатся указатели на облачный провайдер, такие как cloud_name" и "folder_name".
Запускаем
time terraform apply -var-file vars/example.tfvars -auto-approve
По умолчанию будет развернут кластер с тремя мастер-нодами, каждая из которых имеет 6 CPU, 12 ГБ оперативной памяти и 100 ГБ дискового пространства, а также 10 ГБ для ETCD.
Для каждого кластера будет создан внешний балансер, к которому вы сможете подключиться. Также будут созданы аддоны, которые настроят сеть, базовые интеграции с YC, такие как CSI driver, Cloud Controller и Machine Controller Manager для заказа воркер-нод в облаке.
Через шесть минут вы получите полностью готовый кластер и инструкции о том, как подключиться к нему.
Apply complete! Resources: 86 added, 0 changed, 0 destroyed. Outputs: LB-IP = "kubectl config set-cluster cluster --server=https://158.160.63.64:443 --insecure-skip-tls-verify" real 6m4,698s user 0m24,182s sys 0m1,582s dk@dobry-kot-system:~/workspace/fraima/kubernetes/k8s-yandex-cluster-naked$ kubectl get nodes -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME master-50d858e0-1 Ready control-plane,master 9m34s v1.23.12 10.0.0.12 158.160.51.95 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6 master-50d858e0-2 Ready control-plane,master 9m33s v1.23.12 10.0.0.6 158.160.38.139 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6 master-50d858e0-3 Ready control-plane,master 9m34s v1.23.12 10.0.0.19 158.160.42.200 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6
Вы можете заметить, что кластер успешно запущен и функционирует. Кроме того, у узлов теперь есть внешние IP-адреса и свидетельствует о том, что интеграция с YC работает.
Если вы используете keycloak для подключения, не забудьте установить плагин "kubectl login" и воспользоваться универсальным kubeconfig.
kubeconfig
apiVersion: v1 clusters: - cluster: insecure-skip-tls-verify: true server: https://158.160.63.64:443 name: cluster contexts: - context: cluster: cluster namespace: kube-fraima-machine-controller-manager user: cluster name: cluster current-context: cluster kind: Config preferences: {} users: - name: cluster user: exec: apiVersion: client.authentication.k8s.io/v1beta1 args: - oidc-login - get-token - --oidc-issuer-url=https://$KEYCLOAK-SERVER/auth/realms/master - --oidc-client-id=kubernetes-clusters - --oidc-client-secret=kube-client-secret - --certificate-authority=/usr/local/share/ca-certificates/oidc-ca.pem - --skip-open-browser - --grant-type=password - --username=$USERNAME - --password=$PASSWORD command: kubectl env: - name: context value: $(kubectl config current-context) interactiveMode: IfAvailable provideClusterInfo: false
Наполнение
NAME STATUS AGE default Active 11m kube-fraima-certmanager Active 8m48s # CERTMANAGER kube-fraima-dns Active 9m57s # COREDNS kube-fraima-machine-controller-manager Active 8m55s kube-fraima-opa Active 9m44s # GATEKEEPER kube-fraima-sdn Active 10m # CILIUM kube-fraima-yandex-cloud-controller Active 11m kube-fraima-yandex-csi-controller Active 9m54s kube-node-lease Active 11m kube-public Active 11m kube-system Active 11m
Внимание
Одной из важных особенностей этих кластеров является отсутствие приватных ключей от СА на мастерах, так как они хранятся в VAULT. Однако, такой подход приводит к определенным проблемам.
Вы можете добавить любую ноду в кластер через csr bootstraping, где нода генерирует запрос на сертификат и отправляет его в API, а затем вы подтверждаете этот запрос и нода получает свои сертификаты и добавляется в кластер. Однако, в данной инсталляции это нельзя сделать стандартными средствами.
Поскольку kube-controller-manager занимается выдачей сертификатов для узлов, то без доступа к приватному ключу CA этот функционал теряется. Однако, мы нашли способ получить сертификаты, установив Certmanager и Gatekeeper, а затем настроив ClusterIssuer в Certmanager для интеграции с VAULT. С помощью этого ClusterIssuer можно будет выписывать сертификаты только для worker/master узлов. Затем в Gatekeeper настраиваем мутацию ресурса CSR, который изменит базовый SIGNERNAME с "kubernetes.io/kubelet-serving" на "clusterissuers.cert-manager.io/vault-issuer". Таким образом, мы сможем получить необходимые сертификаты.
dk@dobry-kot-system:~/Downloads$ kubectl get csr NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION csr-52cx7 12m kubernetes.io/kubelet-serving system:node:master-50d858e0-3 <none> Pending csr-lbhqf 12m kubernetes.io/kubelet-serving system:node:master-50d858e0-1 <none> Pending csr-n27p4 12m kubernetes.io/kubelet-serving system:node:master-50d858e0-2 <none> Pending node-csr-3l5VT-i7YinQWaTvbCY467d27GQLnSqnT_BYgk_PFII 8m17s clusterissuers.cert-manager.io/vault-issuer system:bootstrap:663273 <none> Pending
Как вы можете заметить, новый узел запросил сертификат через CSR, но SIGNERNAME у него установлен как "clusterissuers.cert-manager.io/vault-issuer". После подтверждения этого
запроса Certmanager выдаст сертификат, который будет храниться во внешнем хранилище Vault.
kubectl certificate approve node-csr-3l5VT-i7YinQWaTvbCY467d27GQLnSqnT_BYgk_PFII
После этого появится еще один запрос на сертификат, который также нужно подтвердить, и после этого узел будет добавлен в кластер.
dk@dobry-kot-system:~/Downloads$ kubectl get no -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME master-50d858e0-1 Ready control-plane,master 24m v1.23.12 10.0.0.12 158.160.51.95 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6 master-50d858e0-2 Ready control-plane,master 24m v1.23.12 10.0.0.6 158.160.38.139 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6 master-50d858e0-3 Ready control-plane,master 24m v1.23.12 10.0.0.19 158.160.42.200 Ubuntu 20.04.4 LTS 5.4.0-124-generic containerd://1.6.6 worker-yandex-compute-instance-68ffc-f2sd2 Ready worker 11m v1.23.12 10.154.0.11 51.250.72.216 Ubuntu 22.04.1 LTS 5.15.0-46-generic containerd://1.6.6
Планы
Расширить функционал Fraimctl, чтобы отказаться от использования Kubeadm.
Написать инфраструктурные модули для AWS и VK-Cloud.
Покрыть Terraform тестами.
Организовать модули более четко и удалить ненужное.
Перейти с использования Terraform + Helm на Terraform + Flux.
Написать расширение для K8S API для добавления нашего кастомного функционала.
Добавить инструмент для настройки узлов как Day2 операций.
У нашего коллектива амбициозные планы и мы нацелены на получение статуса CNCF.
Если вы оценили наш контент, присоединяйтесь к нашему чату, где вы сможете задать любые интересующие вас вопросы. Мы также будем рады любой помощи в нашем проекте.
Контакты
terraform modules: https://github.com/fraima/terraform-modules
terraform cluster: https://github.com/fraima/kubernetes
telegram community: https://t.me/fr_solution_ru
telegram me: https://t.me/Dobry_kot
