Инфраструктура как код: OpenTofu
Презентация
Ссылка на презентацию Последняя редакция: 06.10.26
Теория
OpenTofu: инфраструктура как код
Всё то, что в 06-K8s-cloud-cluster и
07-K8s-cloud-ingress делалось отдельными yc/консольными
шагами - сеть, сервисные аккаунты, кластер, группа узлов, DNS-зона, ключи для cert-manager и
external-dns - можно один раз описать декларативно и дальше поднимать и сносить одной командой
Разница в самом подходе: команды yc - императивные - каждая описывает одно действие
("создай вот это"), и результат зависит от того, в каком порядке и сколько раз их выполнили
Файл .tf - декларативный - описывает, что должно существовать в итоге ("вот сеть, вот
кластер"), а разбираться, что для этого создать, изменить или удалить, - работа самого
инструмента
Здесь используется OpenTofu - открытый форк Terraform (появился после того, как в 2023 году
Terraform сменил лицензию на BSL), синтаксис и провайдеры у них общие, команда tofu - прямая
замена terraform
Зачем это в учебном контексте: в 06-K8s-cloud-cluster уже
говорилось, что дорож е всего - не конфигурация узлов, а время простоя мастера, а после
07-K8s-cloud-ingress к нему добавились DNS-зона, аккаунты с
ключами и балансировщик перед Ingress
Ручных шагов на удаление становится больше, и забыть один из них - значит продолжать платить
за него, даже не используя
tofu destroy сносит всё, что было создано tofu apply, одной командой, независимо от того,
сколько ресурсов в итоге получилось - с одной оговоркой про то, что Tofu не создавал, она
разобрана ниже в разделе "Что Tofu не знает: порядок сноса"
Установка на macOS - через Homebrew
brew install opentofu
tofu -version
Аутентификация - отдельный шаг, yc init её не даёт: провайдеру yandex нужен собственный
токен, даже если yc уже на строен
export YC_TOKEN=$(yc iam create-token)
YC_TOKEN - переменная окружения, которую провайдер yandex подхватывает сам, без явного
упоминания в .tf-файлах
Токен из yc iam create-token живёт ограниченное время (несколько часов) - если tofu apply
посреди работы вдруг перестал аутентифицироваться, хотя в начале всё работало, скорее всего
дело именно в нём, и команду нужно повторить
Анатомия .tf-файла: ресурсы, ссылки, состояние
Прежде чем читать целый main.tf, полезно разобрать один блок resource отдельно
resource "yandex_vpc_subnet" "this" {
name = "conduit-subnet"
zone = "ru-central1-a"
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.10.0.0/24"]
}
resource "<тип>" "<локальное имя>" - тип - конкретный вид ресурса у провайдера (какой именно
объект будет создан в облаке), локальное имя - имя ресурса только внутри .tf-файлов, для
ссылок на него из других блоков, а не имя объекта в самом облаке (то отдельно, полем name)
Ссылка на другой ресурс - <тип>.<локальное имя>.<атрибут>
yandex_vpc_network.this.id значит "id той сети, что описана выше как yandex_vpc_network с
локальным именем this" - по таким ссылкам Tofu сам строит граф зависимостей и создаёт ресурсы
в нужном порядке, вручную прописывать "сначала сеть, потом подсеть" не нужно
Когда зависимость есть, а через атрибут не выражается (роль должна быть выдана раньше, чем
создаётся кластер, хотя сам кластер её id никуда не подставляет) - для этого явный
depends_on = [...], он и в примере ниже
Ещё одна вещь, которой не было у yc-команд, - состояние (terraform.tfstate)
После первого tofu apply появляется файл, в котором Tofu записывает, каким именно облачным
объектам соответствует каждый блок resource - по нему plan/apply в следующий раз понимают,
что уже существует и не нужно создавать заново, а destroy - что именно нужно удалить
Отсюда два практических следствия
- Если удалить что-то из созданного руками (
yc ... deleteили консолью), Tofu об этом сам не узнает - состояние продолжит считать ресурс существующим, и следующийapplyлибо попытается пересоздать его, либо сообщит об ошибке несовпадения - Если потерять файл состояния, Tofu "забывает", что он создал -
tofu destroyбольше не сможет снести эти ресурсы автоматически, и именно то, ради чего вообще стоило вводить Tofu (гарантированная уборка одной командой), перестаёт работать - удалять придётся руками, ровно как и без Tofu
Каталог и облако, в которых всё это создаётся, - тоже через переменные, а не как захардкоженные строки
variable "cloud_id" {}
variable "folder_id" {}
Значения - в terraform.tfvars (файл рядом с main.tf, не должен уходить в git, если в нём
может оказаться что-то приватное) или через переменные окружения TF_VAR_cloud_id/
TF_VAR_folder_id
Какие файлы нужны и что в каждом
Tofu читает все .tf из каталога как один набор ресурсов, поэтому деление на файлы нужно
только людям - так проще найти нужное
Для нашего стенда своих файлов шесть, а после tofu init и tofu apply рядом появляются ещё
три, которые создаёт сам Tofu
conduit-tofu/
├── .gitignore - что не должно попасть в git
├── terraform.tfvars - значения переменных (cloud_id, folder_id)
├── variables.tf - объявление этих переменных
├── main.tf - провайдеры, сеть, группа безопасности, аккаунты, кластер, узлы
├── dns.tf - DNS-зона, аккаунт и ключ для cert-manager и external-dns
├── gwin.tf - аккаунт, ключ и группа безопасности для gwin
├── .terraform/ - после tofu init: скачанные провайдеры
├── .terraform.lock.hcl - после tofu init: версии провайдеров
└── terraform.tfstate - после tofu apply: состояние
Порядок файлов и строк на порядок создания не влияет - его определяют ссылки между
ресурсами, как в разделе выше
Ссылки при этом свободно ходят между файлами: gwin.tf берёт yandex_vpc_network.this и
yandex_vpc_subnet.this из main.tf, а все три файла берут var.folder_id из
variables.tf
.gitignore
.terraform/
*.tfstate*
terraform.tfvars
*-key.json
.terraform/- скачанные провайдеры, они тяжёлые и восстанавливаются командойtofu init*.tfstate*- состояние и его резервные копии, в них приватные ключи аккаунтов открытым текстомterraform.tfvars- вашиcloud_idиfolder_id*-key.json- файлы ключей, которые создаёт Tofu
.terraform.lock.hcl в список не входит специально: в нём только версии провайдеров и их
контрольные суммы, секретов там нет, и этот файл принято коммитить
variables.tf и terraform.tfvars
В variables.tf лежат два объявления из раздела выше - variable "cloud_id" {} и
variable "folder_id" {}
Значения подставляет terraform.tfvars: Tofu подхватывает файл с таким именем сам, а в
остальных файлах они читаются как var.cloud_id и var.folder_id
Проще всего собрать его командой, чтобы не копировать id руками
cat > terraform.tfvars <<EOF
cloud_id = "$(yc config get cloud-id)"
folder_id = "$(yc config get folder-id)"
EOF
Если значения нигде нет, tofu plan спросит его в терминале
yc config get folder-id возвращает каталог, который сейчас выбран в yc, поэтому перед
запуском стоит убедиться, что это тот каталог, где должен жить стенд
От yc-команд к OpenTofu
Минимальный main.tf под сеть, группу безопасности, сервисные аккаунты, кластер и группу
узлов из 06-K8s-cloud-cluster (провайдер yandex-cloud/yandex)
Всё, что относится к 07-K8s-cloud-ingress - DNS-зона, аккаунт и
ключ для cert-manager и external-dns, а затем аккаунт и группа безопасности для gwin - вынесено
в отдельные файлы dns.tf и gwin.tf ниже
Сам main.tf разобран по частям: это один файл, части просто идут друг за другом, а после
каждой стоит проверка, чтобы ошибку было видно сразу, а не в конце
Часть 1 - какие плагины нужны и как подключаться к облаку
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
}
local = {
source = "hashicorp/local"
}
}
}
provider "yandex" {
cloud_id = var.cloud_id
folder_id = var.folder_id
zone = "ru-central1-a"
}
required_providers- список плагинов (провайдеров), которые скачаетtofu init
Про Yandex Cloud сам Tofu ничего не знает, все ресурсыyandex_*описаны в провайдереyandex-cloud/yandex, а адрес записывается какавтор/имя- Провайдер
hashicorp/localнужен, чтобы Tofu мог записать файл ключа на диск (ресурсlocal_sensitive_fileвdns.tfиgwin.tf) provider "yandex"- настройки подключения: облако и каталог берутся из переменных, аzone- зона по умолчанию для ресурсов, у которых своя не указана- Токена в файле нет специально: провайдер берёт его из
YC_TOKEN, как описано выше
Проверка: tofu init, затем tofu validate - должно быть Success! The configuration is valid
Часть 2 - сеть и группа безопасности
resource "yandex_vpc_network" "this" {
name = "conduit-network"
}
resource "yandex_vpc_subnet" "this" {
name = "conduit-subnet"
zone = "ru-central1-a"
network_id = yandex_vpc_network.this.id
v4_cidr_blocks = ["10.10.0.0/24"]
}
resource "yandex_vpc_security_group" "this" {
name = "conduit-sg"
network_id = yandex_vpc_network.this.id
ingress {
protocol = "TCP"
predefined_target = "loadbalancer_healthchecks"
from_port = 0
to_port = 65535
}
ingress {
protocol = "ANY"
predefined_target = "self_security_group"
from_port = 0
to_port = 65535
}
egress {
protocol = "ANY"
predefined_target = "self_security_group"
from_port = 0
to_port = 65535
}
ingress {
protocol = "TCP"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 6443
}
ingress {
protocol = "TCP"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 443
}
ingress {
protocol = "TCP"
v4_cidr_blocks = ["0.0.0.0/0"]
from_port = 30000
to_port = 32767
}
ingress {
protocol = "TCP"
v4_cidr_blocks = yandex_vpc_subnet.this.v4_cidr_blocks
port = 10501
}
egress {
protocol = "ANY"
v4_cidr_blocks = ["0.0.0.0/0"]
from_port = 0
to_port = 65535
}
}
v4_cidr_blocks = ["10.10.0.0/24"]- адресный диапазон подсети, это список в квадратных скобках, потому что диапазонов у подсети может быть несколько- Вложенные блоки
ingress { ... }иegress { ... }- по одному на правило группы безопасности, это те же флаги--ruleизyc vpc security-group create
В правиле либо один порт (port), либо диапазон (from_portиto_port)
Источник - либо готовая цель (predefined_target), л ибо список адресов (v4_cidr_blocks) - Правила по порядку
loadbalancer_healthchecksна все порты - проверки состояния от балансировщиковself_security_groupна вход и на выход - трафик внутри одной группы, между мастером и узлами6443и443из интернета - доступkubectlк API-серверу30000-32767из интернета - диапазон портовNodePort10501из подсети кластера - проверки состояния бэкендов от ALB- исходящий трафик куда угодно - чтобы узлы могли скачивать образы
yandex_vpc_subnet.this.v4_cidr_blocksв правиле10501- ссылка на атрибут подсети
Диапазон берётся из подсети, а не дублируется руками, и при смене адресов правило подстроится само
Проверка: tofu plan - в конце Plan: 3 to add, 0 to change, 0 to destroy
Часть 3 - сервисные аккаунты и роли
resource "yandex_iam_service_account" "cluster" {
name = "k8s-resources-sa"
}
resource "yandex_iam_service_account" "nodes" {
name = "k8s-nodes-sa"
}
resource "yandex_resourcemanager_folder_iam_member" "cluster_agent" {
folder_id = var.folder_id
role = "k8s.clusters.agent"
member = "serviceAccount:${yandex_iam_service_account.cluster.id}"
}
resource "yandex_resourcemanager_folder_iam_member" "cluster_public_ip" {
folder_id = var.folder_id
role = "vpc.publicAdmin"
member = "serviceAccount:${yandex_iam_service_account.cluster.id}"
}
resource "yandex_resourcemanager_folder_iam_member" "nodes_puller" {
folder_id = var.folder_id
role = "container-registry.images.puller"
member = "serviceAccount:${yandex_iam_service_account.nodes.id}"
}
yandex_iam_service_account- сам аккаунт, от его имени кластер и узлы работают с облакомyandex_resourcemanager_folder_iam_member- выдача одной роли одному участнику на каталог, то естьyc resource-manager folder add-access-binding
Один ресурс - одна пара "роль и участник", поэтому три роли - три ресурса"serviceAccount:${yandex_iam_service_account.cluster.id}"- строка с подстановкой
Всё внутри${ ... }вычисляется и вставляется в строку, получаетсяserviceAccount:и id аккаунта- Роли по смыслу
k8s.clusters.agent- аккаунт кластера создаёт и обслуживает ресурсы кластераvpc.publicAdmin- аккаунт кластера выдаёт мастеру публичный адресcontainer-registry.images.puller- аккаунт узлов скачивает образы из Container Registry
Проверка: tofu plan - Plan: 8 to add, 0 to change, 0 to destroy
Часть 4 - кластер и группа узлов
resource "yandex_kubernetes_cluster" "this" {
name = "conduit-cluster"
network_id = yandex_vpc_network.this.id
master {
zonal {
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.this.id
}
public_ip = true
security_group_ids = [yandex_vpc_security_group.this.id]
}
service_account_id = yandex_iam_service_account.cluster.id
node_service_account_id = yandex_iam_service_account.nodes.id
release_channel = "REGULAR"
depends_on = [
yandex_resourcemanager_folder_iam_member.cluster_agent,
yandex_resourcemanager_folder_iam_member.cluster_public_ip,
]
}
resource "yandex_kubernetes_node_group" "this" {
cluster_id = yandex_kubernetes_cluster.this.id
name = "conduit-nodes"
instance_template {
platform_id = "standard-v3"
resources {
cores = 2
core_fraction = 20
memory = 2
}
boot_disk {
type = "network-hdd"
size = 64
}
network_interface {
subnet_ids = [yandex_vpc_subnet.this.id]
nat = true
security_group_ids = [yandex_vpc_security_group.this.id]
}
scheduling_policy {
preemptible = true
}
}
scale_policy {
fixed_scale {
size = 2
}
}
depends_on = [
yandex_resourcemanager_folder_iam_member.nodes_puller,
]
}
master { zonal { ... } }- где живёт мастер: одна зона и подсеть
public_ip = trueдаёт ему внешний адрес, чтобыkubectlподключался снаружи
security_group_ids- список групп, поэтом у ссылка стоит в квадратных скобкахservice_account_idиnode_service_account_id- ссылки на два аккаунта из части 3release_channel = "REGULAR"- канал обновлений версии Kubernetesdepends_onу кластера - ссылки на роли в самом блоке нет, но создавать кластер раньше ролей нельзя: аккаунту не хватит прав
Поэтому порядок "роль, потом кластер" прописан руками, у группы узлов то же самое с рольюcontainer-registry.images.pullercluster_id = yandex_kubernetes_cluster.this.id- группа узлов привязывается к кластеру по ссылке, и этим же задаётся порядок: сначала кластерinstance_template- шаблон виртуальной машины узлаplatform_id = "standard-v3"- платформа (поколение процессоров)cores,core_fraction,memory- 2 ядра с гарантированной долей 20% и 2 ГБ памяти, недорогой вариант для учебного стендаboot_disk- загрузочный диск 64 ГБ на HDDnetwork_interface- сеть узла
nat = trueвыдаёт узлу публичный адрес, без него узлы не смогут скачивать образы
security_group_ids- та же группа, что у мастераscheduling_policy.preemptible = true- прерываемые машины, они дешевле, но облако может остановить их в любой момент
scale_policy.fixed_scale.size = 2- фиксированное число узлов
Проверка: tofu plan - Plan: 10 to add, 0 to change, 0 to destroy, это и есть весь main.tf
Сопоставление с уже знакомыми флагами yc - тот же смысл, просто описан один раз в файле,
а не отдельной командой
yc k8s cluster create --network-name→network_id = yandex_vpc_network.this.id--master-location zone=...,subnet-id=...→master.zonal.zone/master.zonal.subnet_id--service-account-name/--node-service-account-name→service_account_id/node_service_account_id(ссылки на ресурсыyandex_iam_service_account)--public-ip→master.public_ip = trueyc k8s node-group create --platform-id/--cores/--core-fraction/--memory→instance_template.platform_id/resources.cores/resources.core_fraction/resources.memory--network-interface "...,ipv4-address=nat"→instance_template.network_interface.nat = true--preemptible→instance_template.scheduling_policy.preemptible = true--fixed-size→scale_policy.fixed_scale.sizeyc vpc security-group create --rule "...,predefined=loadbalancer_healthchecks"→ingress { predefined_target = "loadbalancer_healthchecks" }(и так же дляself_security_groupиv4-cidrs→v4_cidr_blocks)--security-group-idsуcluster createиsecurity-group-ids=...в--network-interfaceу группы узлов →master.security_group_ids/instance_template.network_interface.security_group_ids
Сами роли (k8s.clusters.agent, vpc.publicAdmin, container-registry.images.puller) в
облаке выдаются командой yc resource-manager folder add-access-binding --service-account-id ... --role ..., по разу на каждую роль - в main.tf выше это те же три назначения, только
объявленные ресурсами yandex_resourcemanager_folder_iam_member
Рабочий цикл
tofu init # один раз - скачивает провайдер
tofu plan # что именно будет создано/изменено/удалено - ничего не меняет в облаке
tofu apply # тот же план + запрос подтверждения + реальное создание
# ...проверка, практика...
tofu destroy # снести всё, что записано в состоянии, в обратном порядке
В выводе plan/apply каждый ресурс помечен знаком - + создать, - удалить, ~ изменить
на месте, -/+ удалить и пересоздать (когда поле нельзя обновить без пересоздания)
apply всегда сначала показывает этот план и ждёт yes - есть возможность остановиться до
того, как что-то реально создалось, если план выглядит не так, как ожидалось
dns.tf: DNS-зона, аккаунт и ключ для cert-manager и external-dns
Вторая часть конфигурации - то, что в 07-K8s-cloud-ingress
делалось командами yc dns zone create, yc iam service-account create,
yc resource-manager folder add-access-binding и yc iam key create
Лежит отдельным файлом рядом с main.tf, var.folder_id берётся из тех же переменных
resource "yandex_dns_zone" "this" {
name = "conduit-zone"
zone = "example.com."
public = true
}
resource "yandex_iam_service_account" "dns_editor" {
name = "dns-editor-sa"
}
resource "yandex_resourcemanager_folder_iam_member" "dns_editor" {
folder_id = var.folder_id
role = "dns.editor"
member = "serviceAccount:${yandex_iam_service_account.dns_editor.id}"
}
resource "yandex_iam_service_account_key" "dns_editor" {
service_account_id = yandex_iam_service_account.dns_editor.id
}
resource "local_sensitive_file" "dns_editor_key" {
filename = "${path.module}/dns-editor-key.json"
file_permission = "0600"
content = jsonencode({
id = yandex_iam_service_account_key.dns_editor.id
service_account_id = yandex_iam_service_account_key.dns_editor.service_account_id
created_at = yandex_iam_service_account_key.dns_editor.created_at
key_algorithm = yandex_iam_service_account_key.dns_editor.key_algorithm
public_key = yandex_iam_service_account_key.dns_editor.public_key
private_key = yandex_iam_service_account_key.dns_editor.private_key
})
}
Что здесь происходит
zone = "example.com."- домен, который нужно заменить на свой
Точка в конце обязательна: так в DNS записывается полное имя, вyc dns zone create --zoneто же самое
public = trueделает зону публичной, то есть видимой из интернетаyandex_iam_service_account_keyсоздаёт ключ для аккаунтаdns_editor
Приватная часть ключа существует только в состоянии Tofu, потом достать её из облака нельзя, поэтому файл для Marketplace собирается сразуlocal_sensitive_file- ресурс провайдераhashicorp/local, он пишет файл на диск там, где запущенtofu
Словоsensitiveзначит, что содержимое не печатается вplanиapply
file_permission = "0600"оставляет права на чтение и запись только владельцу"${path.module}/dns-editor-key.json"- путь к файлу
path.module- каталог, где лежит этот.tf, поэтому ключ окажется рядом с файлами конфигурации, а не в том каталоге, откуда запустили командуjsonencode({ ... })превращает объект HCL в строку JSON
Поля внутри - те же, что вkey.jsonизyc iam key create, значения берутся из атрибутов ключаyandex_iam_service_account_key.dns_editor- Локальное имя
dns_editorповторяется у аккаунта, роли и ключа - это нормально, имя должно быть уникальным только среди ресурсов одного типа
Проверка: tofu plan - если main.tf ещё не применялся, в конце будет
Plan: 15 to add, 0 to change, 0 to destroy
Соответствие yc-командам из 07
yc dns zone create --name --zone --public-visibility→yandex_dns_zone(name,zone,public = true)yc iam service-account create --name→yandex_iam_service_accountyc resource-manager folder add-access-binding --role→yandex_resourcemanager_folder_iam_memberyc iam key create --output→yandex_iam_service_account_key, а сам файл для форм установки cert-manager и external-dns в Marketplace собираетlocal_sensitive_file
dns-editor-key.json - тот же авторизованный ключ, что и key.json из yc iam key create (поля
id, service_account_id, created_at, key_algorithm, public_key, private_key), его
содержимое вставляется в те же поля Marketplace
Формат стоит сверить один раз при первом применении: если вебхук cert-manager не принимает ключ,
первым делом сравните поля двух файлов
Токен из yc iam create-token действует от имени того же пользователя, что и yc, поэтому для
yandex_resourcemanager_folder_iam_member нужны те же права на выдачу ролей, что и для
yc resource-manager folder add-access-binding в 06
Приватный ключ при этом оказывается в двух местах - в dns-editor-key.json и открытым текстом
в terraform.tfstate
Ни файл ключа, ни состояние нельзя коммитить в git: в .gitignore нужны как минимум
*.tfstate*, terraform.tfvars и *-key.json
У yandex_dns_zone есть побочный эффект, которого не было, пока зона создавалась
только yc-командой отдельно: tofu destroy теперь снесёт и её
NS-записи у регистратора от этого не откатятся сами - они как были, так и останутся указывать
на ns1.yandexcloud.net/ns2.yandexcloud.net, а зоны с этим доменом в Yandex Cloud уже не
будет, и домен перестанет резолвиться целиком, а не только сервисы кластера
Чтобы домен ожил обратно, нужно либо руками вернуть NS-записи у регистратора на его собственные
(там же, где NS меняли при делегировании - см. раздел "Домен и перенос NS" в
07-K8s-cloud-ingress), либо пересоздать зону с тем же --zone/zone
gwin.tf: аккаунт и группа безопасности для gwin
Третья часть конфигурации - то, что нужно gwin из
07-K8s-cloud-ingress: сервисный аккаунт с ролями, ключ для него и
группа безопасности самого балансировщика
Роли те же, что в 07, но вместо цикла в bash - один for_each по списку
Роль certificate-manager.editor обязательна: по ней gwin создаёт в Certificate Manager
сертификат по Secret от cert-manager, а без неё ALB не строится, и ошибка видна только в логах
контроллера
Каталог в folder_id - тот же, что потом указывается в параметре "ID каталога" при установке
gwin, иначе у контроллера не окажется прав
locals {
gwin_roles = [
"alb.editor",
"vpc.publicAdmin",
"certificate-manager.certificates.downloader",
"certificate-manager.editor",
"compute.viewer",
"k8s.viewer",
]
}
resource "yandex_iam_service_account" "gwin" {
name = "gwin-sa"
}
resource "yandex_resourcemanager_folder_iam_member" "gwin" {
for_each = toset(local.gwin_roles)
folder_id = var.folder_id
role = each.value
member = "serviceAccount:${yandex_iam_service_account.gwin.id}"
}
resource "yandex_iam_service_account_key" "gwin" {
service_account_id = yandex_iam_service_account.gwin.id
}
resource "local_sensitive_file" "gwin_key" {
filename = "${path.module}/gwin-sa-key.json"
file_permission = "0600"
content = jsonencode({
id = yandex_iam_service_account_key.gwin.id
service_account_id = yandex_iam_service_account_key.gwin.service_account_id
created_at = yandex_iam_service_account_key.gwin.created_at
key_algorithm = yandex_iam_service_account_key.gwin.key_algorithm
public_key = yandex_iam_service_account_key.gwin.public_key
private_key = yandex_iam_service_account_key.gwin.private_key
})
}
resource "yandex_vpc_security_group" "alb" {
name = "alb-sg"
network_id = yandex_vpc_network.this.id
ingress {
protocol = "TCP"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 80
}
ingress {
protocol = "TCP"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 443
}
egress {
protocol = "TCP"
v4_cidr_blocks = yandex_vpc_subnet.this.v4_cidr_blocks
from_port = 0
to_port = 65535
}
}
output "alb_security_group_id" {
value = yandex_vpc_security_group.alb.id
}
Что здесь происходит
locals { ... }- блок локальных значений: имяgwin_rolesобъявляется один раз внутри файла и дальше читается какlocal.gwin_roles
Блок называется во множественном числе, а ссылка идёт в единственном - так принятоfor_each = toset(local.gwin_roles)- один блокresourceпревращается в несколько экземпляров, по одному на каждую роль из списка
for_eachпринимает множество или словарь, а не список, поэтомуtosetпревращает список в множество
Внутри блока текущий элемент доступен какeach.value, и роль в каждом экземпляре своя- Экземпляры в плане называются по значению, например
yandex_resourcemanager_folder_iam_member.gwin["alb.editor"]
Если добавить в список седьмую роль, Tofu создаст ровно один новый ресурс, а остальные не тронет - Остальное - те же конструкции, что в
dns.tf: аккаунт, ключ иlocal_sensitive_fileсjsonencode - В группе
alb-sgссылки идут вmain.tf:yandex_vpc_network.this.idиyandex_vpc_subnet.this.v4_cidr_blocks
Поэтомуgwin.tfбезmain.tfне соберётся output "alb_security_group_id"- значение, которое Tofu печатает послеapplyи отдаёт по запросу черезtofu output
Флаг-rawвыводит строку без кавычек, так её удобно подставлять в аннотациюIngress
Проверка: tofu plan - если ничего ещё не применялось, в конце будет
Plan: 25 to add, 0 to change, 0 to destroy
Из них 10 ресурсов из main.tf, 5 из dns.tf и 10 из gwin.tf (шесть из них - роли)
Содержимое gwin-sa-key.json вставляется в то же поле установки gwin в Marketplace, что и
sa-key.json в 07, *-key.json из .gitignore выше его уже покрывает
alb-sg повторяет правила из yc-команды в 07 - приём 80 и 443 из интернета и исходящий
трафик к узлам из подсети кластера
Правило для порта 10501 (проверки состояния бэкендов) лежит не здесь, а прямо в conduit-sg в
main.tf, потому что оно открывает порт на узлах
Id группы для аннотации gwin.yandex.cloud/securityGroups в Ingress больше не нужно искать
через yc, его отдаёт output
tofu output -raw alb_security_group_id
Что Tofu не знает: порядок сноса
Tofu сносит только то, что создал сам
Всё, что создали приложения внутри кластера, в его состоянии не числится
- Балансировщик перед
Ingress- сетевой (его облако создаёт дляServiceтипаLoadBalancer) или L7 ALB (его создаёт gwin) - DNS-записи, которые external-dns добавил в зону
- Сами приложения из Marketplace и сертификаты, выпущенные cert-manager
cert-manager, external-dns и контроллер Ingress из 07-K8s-cloud-ingress
в этих файлах специально не описаны - они ставятся через Marketplace, а не напрямую ресурсом
Terraform/OpenTofu
Управлять ими декларативно тоже можно (у провайдера yandex есть ресурсы под приложения
Marketplace, у Helm - свой провайдер hashicorp/helm), но это уже выходит за рамки вводного
знакомства
Пока такой балансировщик жив, он держит за собой подсеть и группы безопасности (в том числе alb-sg), и tofu destroy
может упереться в ошибку на их удалении
А если снести кластер вместе с живым Service типа LoadBalancer, балансировщик может остаться
висеть в облаке и дальше тарифицироваться
Поэтому порядок сноса такой
- Удалить в кластере всё, что создаёт балансировщики -
Ingress(ALB от gwin уйдёт вместе с ним) иServiceтипаLoadBalancer, а приложение из Marketplace со своимServiceэтого типа - удалить в консоли - Убедиться, что в каталоге не осталось балансировщиков
- Только после этого
tofu destroy
Команды для первого шага
kubectl delete ingress --all --all-namespaces
kubectl get svc --all-namespaces -o json \
| jq -r '.items[] | select(.spec.type=="LoadBalancer") | "\(.metadata.namespace) \(.metadata.name)"' \
| while read -r ns name; do kubectl delete svc "$name" -n "$ns"; done
Первая команда удаляет все Ingress во всех пространствах имён, вторая - все Service типа
LoadBalancer (jq отбирает их по типу, цикл удаляет по одному), в учебном кластере это то, что
нужно
Выполнять их нужно, пока gwin ещё за пущен: ALB убирает сам контроллер, и если удалить приложение
gwin или кластер раньше Ingress, балансировщик останется в облаке и продолжит тарифицироваться
Приложения из Marketplace, которые мы ставим (cert-manager, external-dns, gwin), Service типа
LoadBalancer обычно не создают, но если у вас есть приложение с таким Service, удалять его
нужно в консоли, иначе оно создаст Service заново
Проверка перед tofu destroy
yc load-balancer network-load-balancer list
yc alb load-balancer list
Оба вывода должны быть пустыми, при этом ALB контроллер убирает уже после удаления Ingress,
поэтому список может опустеть не сразу - повторите проверку через пару минут
ALB, который создал gwin, принадлежит самому gwin (в его документации просят не менять и не
удалять такой ALB в обход контроллера), поэтому и убирать его нужно удалением Ingress, а не
вручную в консоли
Атрибуты yandex_kubernetes_cluster/yandex_kubernetes_node_group выше даны по схеме
провайдера на момент написания - перед занятием стоит свериться с
registry.terraform.io/providers/yandex-cloud/yandex/latest, у облачных провайдеров схема
ресурсов время от времени меняется
Примеры
Весь цикл одного занятия - от пустого каталога до чистого облака (файлы main.tf, dns.tf и
gwin.tf из теории лежат в текущем каталоге, в terraform.tfvars заполнены cloud_id и
folder_id)
export YC_TOKEN=$(yc iam create-token)
tofu init
tofu plan
tofu apply
tofu output -raw alb_security_group_id # id для аннотации gwin.yandex.cloud/securityGroups
yc k8s cluster get-credentials conduit-cluster --external
kubectl config use-context yc-conduit-cluster
# ...практика из 06 и 07...
# перед сносом - убрать то, что Tofu не создавал
kubectl delete -f nginx-test.yaml
yc load-balancer network-load-balancer list
yc alb load-balancer list
# если в списках что-то осталось - удалить то, что это создало (Service, приложение из
# Marketplace), и проверить списки ещё раз
tofu destroy
После tofu destroy стоит проверить, что в облаке ничего не осталось
yc k8s cluster list
yc vpc network list