School

← To the program

Managed Kubernetes в облаке: то, чего не показать на kind

Lesson from the course “Курс подготовки DevOps-инженеров”

Хронометраж: занятие 60 минут — теория 30 минут, практика 30 минут. Домашняя работа: 1–2 часа.

Краток о чём урок: Управляемый кластер Yandex Cloud под Terraform, три вещи, которых нет на kind (LoadBalancer с настоящим адресом, облачный StorageClass/CSI, GitOps на реальный кластер), доступ к API и уборка облачных ресурсов, созданных вне Terraform.


1. Что изучаем

После урока вы сможете:

  • поднимать управляемый кластер Yandex Cloud под Terraform и понимать, что берёт на себя сервис;
  • получать доступ к API управляемого кластера, в том числе когда прямой путь закрыт;
  • показывать Service LoadBalancer с настоящим внешним адресом (на kind это невозможно);
  • заказывать динамический том на облачном StorageClass/CSI и понимать отличие от local-path;
  • запускать GitOps (Argo CD) на управляемый кластер и убирать облачные ресурсы, созданные вне Terraform.

2. Зачем это нужно и где применяется

Весь блок 14 (и блок 13) шёл на kind — локальном кластере в контейнере. kind честно показывает почти всё, но три вещи он показать не может по устройству: у него нет облачного контроллера, нет облачного диска и нет настоящего внешнего адреса. Ровно в этих точках управляемый кластер в облаке ведёт себя иначе, и именно их стоит увидеть перед тем, как эксплуатировать прод. Урок собирает вместе блок 12 (Terraform) и блок 14 (эксплуатация, GitOps) на настоящем облаке.

Ситуации, из-за которых существует этот урок:

  • «На kind сервис LoadBalancer вечно <pending>» — в облаке он получает настоящий адрес;
  • «PVC на kind не соблюдает размер» — облачный CSI даёт настоящий диск;
  • «Кто отвечает за мастер?» — управляемый control plane против самосбора (13.13);
  • «kubectl не достучался до кластера» — доступ к API и обходной путь;
  • «Снёс кластер, а в счёте остались адрес и диск» — ресурсы, созданные вне Terraform.

3. Теория (30 минут)

Файлы для работы: blocks/15-kubernetes-cloud/examples/15.01-managed-k8s-cloud/

Стенд: настоящее облако Yandex Cloud (каталог course), Terraform 1.5.7, провайдер yandex через зеркало (terraformrc, рецепт из 12.09). Кластер поднят и сразу снесён по F30. Конфигурация — infra/main.tf и модуль infra/modules/k8s/main.tf (variables.tf, outputs.tf); управляющая ВМ — infra/control.tf с драйвером control-driver.sh; проверяемые манифесты — manifests/app-lb.yaml и manifests/pvc.yaml.

3.1. Что берёт на себя сервис, а что остаётся вам

Управляемый кластер делит ответственность по границе «control plane / узлы» (та же граница, что в 13.13, только теперь мастер держит облако). Проверено на созданном кластере:

  • control plane (мастер) — забота сервиса. Вы выбираете версию и релизный канал (STABLE — версии 1.32–1.35 на момент урока), зональность и доступность; сам мастер, его etcd, обновления и починку ведёт облако. В kubectl get nodes мастера не видно (11.08);
  • группа узлов — ваша забота: тип ВМ, число, версия, прерываемость. Это обычные ВМ (11.03), и платите вы за них;
  • сеть, сервисный аккаунт, роли — тоже ваши, и в этом уроке они описаны кодом.

Всё это поднимается одним Terraform (модуль infra/modules/k8s/main.tf): yandex_vpc_network + yandex_vpc_subnet, сервисный аккаунт yandex_iam_service_account с ролями (k8s.clusters.agent, vpc.publicAdmin, load-balancer.admin, container-registry.images.puller), yandex_kubernetes_cluster (зональный, публичный эндпоинт) и yandex_kubernetes_node_group (одна прерываемая нода). Замер: terraform apply создал 9 ресурсов, кластер поднялся в RUNNING/HEALTHY, группа узлов RUNNING; создание заняло около 10–15 минут (мастер — основная часть). Роли обязательны и выдаются до кластера (depends_on): без vpc.publicAdmin и load-balancer.admin облачный контроллер не сможет создать LoadBalancer (§3.3).

Про версии и обновления. Версию k8s задаёт релизный канал: RAPID получает новые версии первым, STABLE — последним, но проверенными; на момент урока во всех трёх каналах доступны 1.32–1.35 (yc managed-kubernetes list-versions). В коде версия зафиксирована (1.35), потому что версия вне канала уронит apply. Обновление здесь — это не пересоздание, как на kind (14.09): у управляемого кластера сначала обновляется мастер (это делает сервис по вашей команде), затем — группа узлов, и порядок именно такой (13.13, документация Yandex). Группа узлов в примере — одна прерываемая нода (дешевле в разы, 11.03) с диском 30 ГБ; в проде число узлов и тип задаёте вы, а прерываемость убираете. То есть «управляемый» снимает с вас мастер и починку, но выбор мощности, версии и момента обновления остаётся решением, которое принимаете вы.

3.2. Доступ к API: когда прямой путь закрыт

Кластер поднят с публичным эндпоинтом (external_v4_endpoint), и первое, что хочется, — получить доступ: yc managed-kubernetes cluster get-credentials <имя> --external. Но здесь всплыло ограничение среды, которое стоит знать: с рабочей машины автора TCP до внешнего адреса API (порт 443) не проходит — путь идёт через VPN, и соединение до части адресов облака рвётся (та же находка, что в 11.08). Проверка однозначна: bash -c 'echo > /dev/tcp/<ip>/443' → отказ, kubectl виснет. То есть kubectl с машины владельца к этому кластеру не работает вовсе.

Это не «поломка курса», а реальный операционный случай: сеть между вами и кластером может быть закрыта (корпоративный VPN, приватный эндпоинт, отсутствие маршрута). Обход, который работает всегда, — зайти изнутри облака: небольшая управляющая ВМ в той же подсети (infra/control.tf) видит внутренний эндпоинт мастера. На ней сработала связка из 11.08: IAM-токен из metadata сервисного аккаунта ВМ (curl http://169.254.169.254/.../token) как bearer к API кластера, а RBAC привязывает этот SA к cluster-admin (роль k8s.cluster-api.cluster-admin). Замер: сервер в kubeconfig стал https://10.130.0.21 (внутренний адрес), kubectl get nodes ответил.

Здесь стоит различать два разных доступа, которые легко спутать (и на этом легко потерять время). Первый — доступ к облачному API (yc, Terraform): он идёт на *.api.cloud.yandex.net и у ВМ работает через её сервисный аккаунт из metadata, без ключей. Второй — доступ к API самого кластера (kubectl): он идёт на эндпоинт мастера, и вот он-то и был закрыт с рабочей машины. Роль k8s.cluster-api.cluster-admin даёт именно второй доступ (не первый), а k8s.clusters.agent из §3.1 — это про право управлять кластером как облачным объектом, а не про вход в его API. Разные роли для разных дверей — частая путаница при выдаче прав. Всё, что дальше в уроке, выполнено с управляющей ВМ одним прогоном (control-driver.sh): kubectl там настроен на внутренний эндпоинт со встроенным IAM-токеном (без exec-плагина yc, иначе под cloud-init он не находит нужный контекст), а результат снят с серийной консоли (yc compute instance get-serial-port-output) — тот же приём, что для сетевых опытов в блоке 11, потому что и ssh к ВМ по тому же VPN-пути ненадёжен.

3.3. LoadBalancer с настоящим адресом (на kind нельзя)

На kind Service типа LoadBalancer навсегда остаётся EXTERNAL-IP: <pending> — некому выдать адрес (13.01, 13.06). В облаке за это отвечает облачный контроллер: он видит такой Service и создаёт сетевой балансировщик (NLB) с настоящим внешним адресом.

Проверено манифестом app-lb.yaml (nginx + Service type: LoadBalancer): через минуту у сервиса появился EXTERNAL-IP 130.193.44.58, в облаке возник NLB в статусе ACTIVE со слушателем на этом адресе и порте 80, а curl http://130.193.44.58/ из публичного интернета вернул 200. То есть это не внутрикластерная фикция, а настоящий адрес, доступный снаружи. Тонкость, которую стоит помнить: сразу после создания (первые секунды) балансировщик отвечает 000 — проверки живости целей ещё не прошли; адрес уже выдан, но трафик пойдёт, когда цели станут здоровыми (та же механика health-check, что у NLB в 11.05). Отсюда практический вывод: EXTERNAL-IP появился — это ещё не «уже отвечает», нужно дождаться здоровья целей.

3.4. Облачный StorageClass и CSI (на kind был local-path)

На kind класс по умолчанию — local-path, и он не соблюдает размер тома: dd записал 300 МБ в том на 100Mi без ошибки (13.11), а данные привязаны к одному узлу. В облаке класс по умолчанию — настоящий CSI поверх облачных дисков.

Проверено: kubectl get storageclass показал пять классов, все на провайдере disk-csi-driver.mks.ycloud.io, класс по умолчанию — yc-network-hdd (WaitForFirstConsumer, allowVolumeExpansion: true). PVC из pvc.yaml на 1Gi перешёл в Bound, под него создался PV, а в облаке появился настоящий диск 1 ГиБ в статусе READY (виден в yc compute disk list). Отличия от local-path принципиальные: диск существует как облачный ресурс, переживает переезд пода на другой узел (сетевой диск, а не привязка к узлу), поддерживает расширение и, в отличие от local-path, соблюдает размер. Классов несколько (hdd, ssd, nvme, ssd-nonreplicated) — выбор класса это выбор носителя и цены, чего на kind просто нет.

Одно свойство совпадает с kind и стоит того, чтобы его заметить: volumeBindingMode у всех классов — WaitForFirstConsumer, то есть PVC остаётся Pending, пока не появится под-потребитель (13.11). Это не дефект, а норма: облако создаёт диск в той зоне, где реально встанет под, и не раньше. Отсюда практический порядок разбора «PVC висит»: сначала смотрят, есть ли под, и лишь потом — класс и провайдер. А Delete как reclaimPolicy по умолчанию означает, что при удалении PVC через кластер диск уберётся сам (в отличие от сценария §3.6, где кластер сносят целиком и диск остаётся сиротой). То есть у облачного CSI аккуратная уборка встроена — но работает она, только если удалять PVC, а не выдёргивать кластер из-под него.

3.5. GitOps на управляемый кластер

Механика Argo CD разобрана в 14.10 на kind; здесь важно, что она так же работает на управляемом кластере, а отличия — в окружении, а не в инструменте. Проверено: на кластер поставлена минимальная сборка Argo CD (core-install.yaml v3.5.3, как в 14.10), создан проект default (в core-install его нет — находка 14.10) и Application, указывающий на публичный репозиторий примеров Argo (argocd-example-apps, приложение guestbook). Приложение дошло до Synced/Healthy за десятки секунд.

Что здесь то же, что на kind: модель синхронизации, Application, AppProject, права через RBAC (14.03). Что иначе: во-первых, узлы кластера имеют выход в интернет (NAT), поэтому Argo тянет манифесты из внешнего git без ухищрений (на kind в 14.10 приходилось поднимать git-демон в кластере); во-вторых, доступ к самому Argo и к API — через реальный эндпоинт и реальный RBAC, то есть все ограничения из 14.03/14.10 действуют буквально. Вывод: GitOps не зависит от того, где кластер, — но окружение управляемого кластера (сеть, RBAC, эндпоинт) настоящее, и это меняет обвязку вокруг Argo, а не сам Argo.

3.6. Уборка: облако создаёт ресурсы вне Terraform

Самая ценная эксплуатационная находка урока: не всё, что появилось, создано Terraform. NLB для LoadBalancer и диск для PVC создал облачный контроллер кластера, а не Terraform, — их нет в его state. Поэтому terraform destroy снёс кластер, узлы, сеть и ВМ (12 ресурсов), но NLB и облачный диск остались бы сиротами и продолжали тратить деньги: адрес NLB — 0,26 ₽/ч, диск — по часам.

Проверено, поэтому в уборке два шага. Сначала снимаются ресурсы, созданные контроллером: NLB и его target-group (иначе, пока NLB держит сеть, terraform destroy не удалит сеть), — правильнее всего удалить их через сам кластер (kubectl delete svc/pvc, тогда контроллер уберёт NLB и диск сам), а если кластер уже сносится — добить через yc. Затем terraform destroy. После — инвентаризация каталога (yc ... list по всем видам): в этом уроке после destroy остался один сиротский диск, снят отдельно, и только тогда каталог стал пуст. Правило F30 в облачном k8s звучит так: инвентаризация после destroy обязательна, потому что часть ресурсов живёт вне Terraform.

Цена всей проверки: кластер + прерываемая нода + управляющая ВМ + NLB + адрес + два диска за ~40 минут обошлись меньше чем в несколько рублей; всё снесено сразу (F30). Порядок сноса важен и сам по себе: сначала — то, что создал контроллер (Service и PVC через кластер, либо NLB и диск через yc), потом — Terraform, и только в конце — инвентаризация. Пропустишь первый шаг — заплатишь за то, чего в коде нет.

3.7. Сводка: что показали замеры

Утверждение Что показал замер
«managed = я ничего не делаю» мастер — сервиса; узлы, сеть, роли — ваши, всё под Terraform (9 ресурсов)
«kubectl всегда достучится» TCP 443 к внешнему API с машины владельца закрыт (VPN, 11.08) — зашёл изнутри
«LoadBalancer как на kind» настоящий NLB ACTIVE, внешний IP 130.193.44.58, curl 200 из интернета
«том как local-path» CSI yc-network-hdd, PVC Bound → настоящий диск 1 ГиБ READY, размер соблюдается
«GitOps в облаке — что-то особое» тот же Argo CD (14.10) → Synced/Healthy; иначе только окружение
«destroy убрал всё» NLB и диск создал контроллер, не Terraform — остались сиротами, добил yc

3.8. Примеры и варианты реализации

Один управляемый кластер — в трёх средах (здесь среда и есть предмет урока).

Среда Как решается Что нужно иметь
Локальный стенд (kind) LoadBalancer/CSI/облачный контроллер недоступны — это и есть граница kind Docker
VPS / bare-metal кластер самосбором (kubeadm, 13.13); LB и CSI — свои (MetalLB, Ceph/CSI) серверы, время
Yandex Cloud управляемый кластер под Terraform, облачные LB и CSI из коробки (пример урока) каталог, ключ/токен

Локальный стенд. kind показывает всё, кроме облачных интеграций; именно поэтому этот урок — в облаке.

VPS / bare-metal. Кластер и control plane поднимаете и обслуживаете сами (13.13), а LoadBalancer и динамические тома добавляете отдельными компонентами — их в облаке даёт сервис.

Yandex Cloud. Управляемый кластер + облачные LB/CSI из коробки; ценой этого — привязка к облаку и плата за узлы, мастер-вычисления, балансировщик и диски. Дорогое создаётся под замер и сразу сносится (F30), а доступ к API может требовать обхода (§3.2).

4. Практика на занятии (30 минут)

Курс проходят самостоятельно, поэтому каждый шаг говорит, что сделать (или что ввести), зачем, что должно получиться и что делать, если вышло не так.

Опыты — в 15.01-managed-k8s-cloud/. Стенд — настоящий управляемый кластер Yandex Cloud (каталог course, F30), поднятый Terraform; доступ — через управляющую ВМ с IAM-токеном из metadata (§3.2). Полный цикл создания и сноса кластера длиннее занятия (создание — около 10–15 минут) и вынесен в домашнюю работу: там же метка course=lNNNN на всех ресурсах, дорогое — под замер и с ценой заранее, а после terraform destroy обязательна инвентаризация yc (облачный контроллер создаёт NLB и диск вне state Terraform, §3.6). На занятии кластер не поднимаем — разбираем код и уже снятый вывод драйвера (маркеры ===...=== из control-driver.sh), процитированный в теории.

Шаг 1 (7 минут). Кто за что отвечает: разбор Terraform-кода.

  • Что сделать: прочитайте infra/main.tf и модуль infra/modules/k8s/main.tf; выпишите, какие ресурсы создаёт Terraform, а что после этого держит облако само.
  • Зачем: без этой границы непонятно, что вообще проверяют следующие два шага — облачные интеграции работают именно там, где заканчивается ваш код и начинается сервис.
  • Ожидаемый результат: список из трёх групп — control plane (версия, канал, зональность — решаете вы, сам мастер и etcd — сервис), группа узлов (полностью ваша), сеть/SA/роли (ваши, описаны в модуле); yandex_resourcemanager_folder_iam_member стоит в depends_on у yandex_kubernetes_cluster.
  • Если не так: если не нашли depends_on — он там не для красоты: без ролей, выданных до кластера, облачный контроллер не сможет создать LoadBalancer в шаге 3 (§3.1).
  • Ответьте: какая из ролей в locals.roles нужна именно для публикации LoadBalancer, а не для управления кластером как таковым?

Шаг 2 (7 минут). Доступ к API: почему прямой путь не работает.

  • Что сделать: прочитайте §3.2 и начало control-driver.sh до строки kubectl config use-context ctx — как ВМ получает IAM-токен из metadata и собирает kubeconfig без exec-плагина yc.
  • Зачем: это рабочий обход для любой ситуации, где сеть до API закрыта (VPN, приватный эндпоинт, свой корпоративный фаервол), а не специфика курса.
  • Ожидаемый результат: вы можете назвать разницу между ролью k8s.clusters.agent (управлять кластером как облачным объектом) и k8s.cluster-api.cluster-admin (войти в API самого кластера) и объяснить, зачем токен подставлен в kubeconfig как --token, а не через exec-плагин.
  • Если не так: если кажется, что достаточно одной роли на всё — перечитайте абзац «Здесь стоит различать два разных доступа» в §3.2: это разные двери, и одна роль другую не заменяет.
  • Ответьте: с рабочей машины bash -c 'echo > /dev/tcp/<ip>/443' даёт отказ. Что именно это доказывает и чего не доказывает?

Шаг 3 (11 минут). Три вещи, которых нет на kind: манифест → вывод драйвера.

  • Что сделать: сопоставьте манифесты app-lb.yaml и pvc.yaml с фрагментами вывода control-driver.sh, процитированными в §3.3–3.5: ===LB-IP===, ===PVC===/===STORAGECLASS===, ===ARGO===. Для каждого из трёх пунктов назовите ОДНО поле или код ответа, которое доказывает результат, а не просто «применилось».
  • Зачем: «применилось» (код 0) не значит «работает» — это уже встречалось на PSA (14.04, Deployment применился, а подов ноль) и на GitOps (14.10, Synced не гарантирует восстановимость данных) — здесь та же проверка, только доказательства настоящие (внешний IP, статус диска в облаке).
  • Ожидаемый результат: Service → EXTERNAL-IP 130.193.44.58 и curl код 200; PVC → phase: Bound плюс диск READY в yc compute disk list; Argo Application → Synced/Healthy.
  • Если не так: curl сразу после появления адреса вернул 000 — это не авария, цели балансировщика ещё не прошли health-check (§3.3), подождите и повторите. PVC остался Pending — это тоже норма при WaitForFirstConsumer, пока нет пода-потребителя (§3.4), а не признак сломанного CSI.
  • Ответьте: почему yc-network-hdd — облачный диск, а не аналог local-path — соблюдает заявленный размер тома, а local-path из 13.11 нет?

Шаг 4 (5 минут). Уборка: что Terraform не видит.

  • Что сделать: перечитайте §3.6 и найдите в манифестах, какие два ресурса (LoadBalancer и PVC) порождают объекты, которых нет в коде Terraform.
  • Зачем: это самая дорогая ошибка урока — «снёс кластер, а счёт продолжает идти» — и единственный способ её избежать: знать заранее, что облачный контроллер создаёт ресурсы вне state.
  • Ожидаемый результат: верный порядок уборки — сначала kubectl delete svc/pvc (или ресурсы через yc, если кластер уже сносится), потом terraform destroy, и только в конце — инвентаризация yc ... list по всем видам ресурсов.
  • Если не так: если сделали terraform destroy первым и решили, что этого достаточно, — проверьте инвентаризацией: в этом уроке после такого порядка сиротой остался диск.
  • Ответьте: почему reclaimPolicy: Delete у CSI не спасает в сценарии, где кластер сносят целиком, а не удаляют PVC отдельно (§3.4)?

Что обсудить в конце занятия. Вы сносите кластер terraform destroy и видите нулевой счёт за кластер, но через месяц приходит счёт за диск и адрес. Что случилось и как это предотвратить?

5. Домашняя работа (1–2 часа)

Задание. Если у вас есть каталог в Yandex Cloud, соберите управляемый кластер по заготовке managed-k8s-plan.md: поднимите кластер Terraform; получите доступ к API (при закрытом прямом пути — изнутри облака); покажите Service LoadBalancer с настоящим адресом, PVC на облачном StorageClass со связанным диском и Argo CD с Synced/Healthy; снесите всё и проверьте инвентаризацией, что каталог пуст, приведите оценку цены. Если облака нет — разберите те же шаги по коду примеров и объясните, почему каждый из трёх пунктов невозможен на kind.

Критерии приёмки.

  • Кластер поднят Terraform (yc/kubectl показывают узлы и RUNNING/HEALTHY).
  • Описан доступ к API (в том числе обходной путь, если прямой закрыт).
  • Service LoadBalancer получил настоящий внешний адрес; показан ответ по нему.
  • PVC на облачном StorageClass связан с настоящим диском (yc показывает диск).
  • Показана/описана работа GitOps на управляемом кластере и отличие окружения от kind.
  • Всё снесено, инвентаризация каталога пуста, приведена оценка цены (F30).

6. Частые ошибки и подводные камни

Ждать LoadBalancer на kind. Симптом: EXTERNAL-IP <pending> навсегда. Причина: нет облачного контроллера. Что делать: LoadBalancer — свойство облака; на kind проверяют NodePort/Gateway (13.06).

kubectl виснет к управляемому кластеру. Симптом: таймаут, Unable to connect. Причина: сеть до API закрыта (VPN, приватный эндпоинт). Что делать: зайти изнутри облака (ВМ в подсети, внутренний эндпоинт, IAM-токен как bearer), проверить TCP до порта 443 отдельно.

terraform destroy не убрал всё. Симптом: после destroy остались адрес и диск, идёт счёт. Причина: NLB и CSI-диск создал облачный контроллер, их нет в state Terraform. Что делать: удалять Service/PVC через кластер до destroy, после — инвентаризация yc (§3.6).

Роли выданы после кластера. Симптом: LoadBalancer не создаётся, PVC висит. Причина: у сервисного аккаунта нет vpc.publicAdmin/load-balancer.admin в момент работы контроллера. Что делать: выдавать роли до кластера (depends_on).

Захардкожена версия, которой нет в канале. Симптом: apply падает на версии k8s. Причина: версия вне релизного канала. Что делать: сверять yc managed-kubernetes list-versions (на момент урока STABLE — 1.32–1.35).

Оставили кластер «на потом». Симптом: счёт за мастер-вычисления, узлы, адрес, диски. Причина: не снесли после проверки. Что делать: terraform destroy сразу, дорогое — под замер с ценой заранее (F30).

7. Проверка усвоения

  1. Что в управляемом кластере берёт на себя сервис, а что остаётся вам?
  2. Почему kubectl с вашей машины может не достучаться до кластера и как зайти иначе?
  3. Почему Service LoadBalancer работает в облаке и не работает на kind?
  4. Чем облачный StorageClass/CSI отличается от local-path из 13.11?
  5. Что в GitOps на управляемом кластере такое же, как на kind, а что иначе?
  6. Почему после terraform destroy нужна инвентаризация каталога?
  7. Как обосновать цену такой проверки и как её минимизировать?

8. Материалы


← Предыдущий: 14.14. Эксплуатация ambient: политики, наблюдаемость, отладка, цена решения · Блок 15 · Следующий: 15.02. Управляемый кластер под Terraform: узлы, группы узлов, автомасштабирование узлов →

Practice materials

Scripts, cheat sheets and starter files for the lesson tasks. The lesson text is on this page.