Terraform: от незнания к best practices

от автора

Всем привет! Меня зовут Виктор, я DevOps‑инженер компании Nixys, которая помогает другим компаниям внедрять в их IT‑решения передовые практики DevOps, MLOps и DevSecOps.

Сегодня я приглашаю вас вместе со мной пройти путь «от незнания к best practices» в работе с Terraform. Этот материал подготовлен для серии наших одноименных видеороликов на YouTube, но мы решили дополнить его и предложить вам более детальное описание процесса в этой статье.

Не забывайте следить за нашими обновлениями на YouTube, Habr и подписывайтесь на наш Telegram‑канал DevOps FM — мы всегда рады новым друзьям. Начнём?

Чем мне поможет Terraform? Так ли он мне нужен?

Ответ однозначный — да! Если у вашего провайдера есть возможность работы с этим инструментом, обязательно используйте его.

Почему? В первую очередь, Terraform — это автоматизация. А всё, что можно автоматизировать, надо автоматизировать. Да‑да, тавтология, но, надеюсь, так лучше закрепится мысль. Вы же не задаетесь вопросом: «а зачем конвейерные линии на производстве ставят», верно? Вот и здесь принцип тот же. Да, вам нужен оператор этой линии (в нашем случае толковый DevOps‑инженер с нужными скиллами), но просто вдумайтесь, сколько проблем вы этим решаете!

  • Предоставление инфраструктуры и решение долгих рутинных задач. Наверняка перед вами когда‑то возникала проблема, что нужно обеспечить несколько сред для разработки своего продукта, так как параллельно у вас 20 разрабов сражаются за то, чтобы запустить свой код на единственном сервере (а в большинстве случаев они просто будут ждать пока под них не настроят окружение). Вы же в это время получаете простой в разработке, неконкурентоспособность на рынке и т. д. и т. п. Если это про вас, тогда вам определенно нужен Terraform, ведь он автоматизирует процесс создания и предоставления ресурсов инфраструктуры — таких, как серверы, сети, балансировщики нагрузки и базы данных. И вуаля — вам не нужно тратить кучу времени на новое окружение.

  • Снижение человеческого фактора. У вас было такое, что в процессе работы проекта, возникали ошибки по вине человека? Это может быть что угодно: опечатка в названии сервиса при обращении к нему, или же просто кто‑то мог поторопиться, мискликнуть и случайно удалить не ту машину. Вот 100% что было, все мы люди. Тогда опишите свою инфраструктуру кодом, вы снизите шанс подобного к минимуму. Не до нуля, нет, но на несколько порядков меньше — это факт.

  • Обновление. Ох, когда‑нибудь сталкивались с тем, что на рабочем проекте нужно обновить софт? И ладно если это что‑то несущественное, а вдруг надо прям куб обновлять, да ещё через несколько мажорных версий? В общем, Terraform и тут поможет: потратьте время на описание своей инфраструктуры в коде и вы забудете эту боль. Это не снимет с вас обязанность продумать все нюансы. Но во‑первых, Terraform изначально заставляет вас организовать инфраструктуру особым образом, и с такой организацией легче обновляться, а во‑вторых — запускаете две команды, и вы подняли полный клон своей инфраструктуры — можете там ломать, откатывать, переделывать всё, что вашей душе угодно. И если у вас получилось обновиться, с вероятностью, крайне близкой к 100%, вы без проблем обновите и свой прод.

  • Стандартизация. Если у вас команда из двух и более человек, работающих с инфраструктурой, то вы наверняка сталкивались с тем, что они работают по‑разному. Да есть нормы, но они разнятся от компании к компании, и в регламенты всё не затолкаешь, и по итогу получаете вроде бы одну инфраструктуру, но часть поднята одним способом, часть — другим. Это может быть что угодно — от нестандартизированных имён сервисов до использования разных ОС для однотипных хостов. С Terraform вы это решаете, так или иначе. Даже если у вас нет регламента к написанию кода, всё устроено таким образом, что инженерам всё равно придётся работать по одному стандарту.

  • Очевидность. Нанимали нового человека? Сколько времени ему нужно на то, чтобы влиться в работу? С инфраструктурой, которая поднята руками, всё ещё сложнее — там может быть столько неочевидных вещей, что обнаружить их можно будет только когда что‑то уже сломалось, и инженеру приходится носом землю рыть в поисках неисправности. Если вы описали всё с помощью кода, это означает, что больше нет кучи мест, где может быть реализован тот или иной функционал — всё здесь, в манифестах. Да, они могут быть сложны и запутанны, но, по крайней мере, это не какой‑нибудь пользовательский cron на 101-м сервере ходит в прод и забивает весь пул соединений — если он есть, то описан в коде, и мы знаем где его искать. Есть один универсальный источник — это код: ознакомился с ним, значит ознакомился со всей инфраструктурой.

  • Работа в команде. Здесь всё просто — система контроля версий. Если есть код, мы и работаем с ним, как с кодом. Заливаем его в GitHub, GitLab или где вы привыкли работать — и всё, мы обеспечили необходимый функционал для нормальной работы в команде. Если у вас всё руками поднималось, забудьте про такие плюшки.

В целом, Terraform — это мощный инструмент, который решает множество проблем как IT‑отдела в частности, так и, соответственно, бизнеса в целом.

Теперь повторим основы

Terraform — это инструмент, который позволяет описывать инфраструктуру в виде кода и автоматизировать её развёртывание, модификацию и удаление. Он поддерживает множество провайдеров — таких, как AWS, Google Cloud, Azure, Yandex Cloud и других.

Одним из основных преимуществ Terraform является возможность описать желаемое состояние инфраструктуры в виде кода, и затем он автоматически приводит её в это состояние, а вы не заботитесь о ручном управлении каждым ресурсом.

Чтобы начать эффективно работать с этим инструментом, важно понимать основные концепции — такие, как манифесты, провайдеры и команды. Далее в статье мы рассмотрим их, а также принципы работы с Terraform. Кроме того, я расскажу про best practices, которые помогут вам создавать качественный и надежный код.

Итак, Terraform использует манифесты, чтобы описать состояние инфраструктуры, которую вы хотите создать или управлять. Манифесты Terraform — это файлы с расширением.tf, которые содержат описание ресурсов, их атрибутов и зависимостей. Для описания манифестов используется HashiCorp Configuration Language (HCL). HCL является удобным и понятным языком, который позволяет описывать ресурсы, зависимости между ними и указывать значения атрибутов.

После того, как манифесты написаны, Terraform может использовать их для управления ресурсами в провайдере. Это позволяет вам автоматизировать процесс работы с инфраструктурой, улучшать ее надежность и снижать вероятность возникновения ручных ошибок.

Terraform использует несколько основных элементов для описания инфраструктуры:

  1. Providers: провайдеры — это интерфейсы для взаимодействия с ресурсами, такими, как AWS, Google Cloud, Microsoft Azure и т. д.

  1. Resources: ресурсы — это объекты, которые вы хотите создать, удалить или изменить. Например, виртуальная машина, балансировщик нагрузки, база данных.

  1. Variables: переменные — это значения, которые можно передать в Terraform для конфигурирования вашей инфраструктуры.

  1. Data Sources: источники данных — это инструменты, которые позволяют Terraform получать данные из внешних источников, таких как API, базы данных или другие службы.

  1. Outputs: выводы — это результаты, которые Terraform выдает после выполнения конфигурации, например, IP‑адреса, идентификаторы ресурсов и т. д.

Дополнительные элементы Terraform, которые стоит упомянуть:

  1. Модули. Позволяют повторно использовать код и упрощают управление сложными инфраструктурами.

  1. Варианты окружения. Terraform позволяет управлять различными версиями инфраструктуры в зависимости от окружения.

  1. Выражения. Terraform использует выражения для вычисления значений и проверки условий.

Итак, мы понимаем, что с помощью Terraform можно описать инфраструктуру в коде, этот код содержится в файлах, что логично?, так давайте поговорим с вами о них.

Основные типы файлов, которые вы можете использовать в Terraform, включают в себя:

  1. Файлы конфигурации (.tf файлы) — это основные файлы Terraform, которые содержат описание ресурсов, которые вы хотите создать или управлять.

  1. Файлы переменных (.tfvars файлы) — это файлы, которые содержат значения переменных, использующихся в файлах конфигурации.

Файл состояния (.tfstate файл): это файл, который хранит текущее состояние вашей инфраструктуры. Он обновляется каждый раз, когда вы запускаете apply (конечно, если вы что‑то изменили в коде).

Также, стоит отдельно рассказать про файл состояния ‑.tfstate. Terraform сохраняет информацию о текущем состоянии вашей инфраструктуры в файле состояния. Этот файл содержит информацию о ресурсах, которые уже были созданы в вашей инфраструктуре, и их текущих свойствах.

Terraform использует этот файл, чтобы определить, какие ресурсы необходимо изменить, какие создать, а какие необходимо удалить, когда вы запускаете команду apply. Грубо говоря, он помогает Terraform определять, какие изменения необходимо внести в инфраструктуру, чтобы ее состояние соответствовало описанному в конфигурационных файлах.

Файл состояния может храниться локально или удаленно, например, в объектном хранилище. Это позволяет вам хранить общую копию файла состояния и управлять им с нескольких хостов.

Важно отметить, что файл состояния является важным аспектом управления вашей инфраструктурой, поэтому необходимо обеспечить его блокировку на время работы с ним.

Также, нужно понимать, что файл состояния является конфиденциальным, и его необходимо хранить в безопасном месте, так как он содержит важную информацию о всей вашей инфраструктуре.

Кроме всего этого, хочу упомянуть, что если вам пришлось вручную залезть в tfstate‑файл, то, как говорит мой коллега: «Вы где‑то что‑то сделали не так в этой жизни» ?

Теперь давайте поговорим с вами о том, на какие best practices стоит обратить внимание:

  1. Не храните всю инфраструктуру в одном месте, разбейте её на части;

  1. Изолируйте несвязанные компоненты друг от друга. Разделяйте логически свою инфраструктуру, сеть, сервисы, мониторинг и т. д.;

  1. Не храните файл состояния локально;

  1. Блокируйте файл состояния при работе;

  1. Пишите код так, чтобы потом его можно было использовать в другом месте, используйте модули.

  1. Пользуйтесь переменными — удобнее использовать переменные для управления конфигурацией инфраструктуры.

  1. Пишите документацию, она всегда пригодится.

Чуть подробнее о том, что мы здесь перечислили

Когда вы пишите код своей инфраструктуры, достаточно написать всё в одном файле, к примеру в файле main.tf:

my_project/  └── main.tf

В нём будет всё, и объявление провайдеров, и ресурсы, и data sources, и outputs: 

terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   }   required_version = ">= 0.13"    backend "s3" {     endpoint   = "storage.yandexcloud.net"     bucket     = "BAKET_NAME"     region     = "ru-central1"     key        = "FILE_NAME"     shared_credentials_file = "storage.key"      skip_region_validation      = true     skip_credentials_validation = true   } }  provider "yandex" {   service_account_key_file = "key.json"   cloud_id                 = "CLOUD_ID"   folder_id                = "FOLDER_ID"   zone                     = "ru-central1-a" }  resource "yandex_vpc_network" "test-vpc" {   name = "nixys" }  resource "yandex_vpc_subnet" "test-subnet" {   v4_cidr_blocks = ["10.2.0.0/16"]   network_id     = yandex_vpc_network.test-vpc.id }  resource "yandex_vpc_security_group" "test-sg" {   name        = "My security group"   description = "description for my security group"   network_id  = yandex_vpc_network.test-vpc.id    labels = {     my-label = "my-label-value"   }    dynamic "ingress" {     for_each = ["80", "8080"]     content {       protocol       = "TCP"       description    = "rule1 description"       v4_cidr_blocks = ["0.0.0.0/0"]       from_port      = ingress.value       to_port        = ingress.value     }   }    egress {     protocol       = "ANY"     description    = "rule2 description"     v4_cidr_blocks = ["0.0.0.0/0"]     from_port      = 0     to_port        = 65535   } }  resource "yandex_vpc_address" "test-ip" {   name = "exampleAddress"    external_ipv4_address {     zone_id = "ru-central1-a"   } }  resource "yandex_compute_instance" "nixys" {   name        = "nixys"   platform_id = "standard-v1"    resources {     cores  = 2     memory = 2   }    boot_disk {     initialize_params {       image_id = "fd83h3kff4ja27ejq0d9"     }   }    network_interface {     subnet_id      = yandex_vpc_subnet.test-subnet.id     nat            = true     nat_ip_address = yandex_vpc_address.test-ip.external_ipv4_address.0.address   }    metadata = {     ssh-keys  = "debian:${file("/root/.ssh/id_rsa.pub")}"     user-data = "${file("init.sh")}"   } }  output "external_ip" {   value = yandex_vpc_address.test-ip.external_ipv4_address.0.address }  output "external_ip-2" {   value = yandex_compute_instance.nixys.network_interface.0.nat_ip_address }

Terraform это «прожуёт» и исполнит код. Всё бы ничего, если вы описали один ресурс, но их может быть два, а может и пару десятков. В таком файле будет уже сложнее разбираться и вносить изменения в инфраструктуру.

Хорошо, мы можем под каждый ресурс создавать отдельный файл, например gitlab.tf, mongodb.tf, rebbitmq.tf и т. д.:

my_project/ ├── k8s └── network

Уже лучше, теперь мы знаем в каком файле необходимо изменить значение или дописать параметр. Но возникает ещё одна проблема — Terraform воспринимает все файлы в рабочем каталоге как один, а это значит, что и при запуске plan или apply он пойдёт перепроверять всю вашу инфраструктуру. Это долго и никому не нужно. Нет необходимости перепроверять ресурсы для кластера k8s, если изменения вносились в подсеть. Какой тогда выход? Всё просто, мы создадим отдельный рабочий каталог под каждый ресурс или связанную логически группу ресурсов:

my_project/ ├── k8s │   └── main.tf └── network     └── main.tf

Теперь у нас имеется несколько манифестов Terraform, каждый из которых описывает свою часть общей инфраструктуры, но всё же у нас остаётся проблема с тем, что всё свалено в одну кучу. Описание того же кластера k8s достаточно большое, и работать в одном файле совсем не удобно. Решение следующее — мы разбиваем конфигурационные файлы по типу описываемых компонентов, т. е. есть главный файл main.tf, где мы объявим провайдера и пропишем доступы к облаку. Далее, мы логически распределим код в отдельные файлы. Выглядит это следующим образом:

my_project/ ├── k8s │   ├── cluster.tf │   ├── main.tf │   ├── node_group.tf │   ├── service_account.tf └── network     ├── external_ip.tf     ├── main.tf     ├── security_group.tf     └── vpc.tf

Таким образом, мы и разделяем конфигурацию логически и с легкостью разберемся в инфраструктуре, если видим её впервые.

Конечно, можно придумывать ещё много разных усовершенствований, но иногда стоит просто остановиться и работать дальше)

Поднимаем кластер

Итак, теперь мы примерно понимаем, что к чему, и как нам организовать нашу конфигурацию. Давайте теперь перейдём к делу и начнём создание межрегионального кластера kubernetes с автоскейлингом worker-нод с помощью Terraform в Yandex Cloud. В этом кластере мы поднимем мониторинг посредством kube-prometheus-stack.

Части про установку Terraform и yc (консольной утилиты Yandex Cloud) я снова пропускаю, вы можете самостоятельно ознакомиться с этой информацией по приведённым ссылкам. 

Также я не буду останавливаться на создании сервисного аккаунта и организации доступа к облаку, об этом я рассказывал в демонстрационном ролике – DevOps с Nixys | Знакомство с Terraform — Tutorial для начинающих #1.

Начнём мы, как это водится, с документации. Согласно ей, код по созданию межрегионального кластера будет выглядеть так:

main.tf
locals {   cloud_id    = "b1gq90dgh25bebiu75o"   folder_id   = "b1gia87mbaomkfvsleds"   k8s_version = "1.22"   sa_name     = "myaccount" }  terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   } }  provider "yandex" {   folder_id = local.folder_id }  resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = yandex_vpc_network.mynet.id   master {     version = local.k8s_version     regional {       region = "ru-central1"       location {         zone      = yandex_vpc_subnet.mysubnet-a.zone         subnet_id = yandex_vpc_subnet.mysubnet-a.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-b.zone         subnet_id = yandex_vpc_subnet.mysubnet-b.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-c.zone         subnet_id = yandex_vpc_subnet.mysubnet-c.id       }     }     security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]   }   service_account_id      = yandex_iam_service_account.myaccount.id   node_service_account_id = yandex_iam_service_account.myaccount.id   depends_on = [     yandex_resourcemanager_folder_iam_binding.k8s-clusters-agent,     yandex_resourcemanager_folder_iam_binding.vpc-public-admin,     yandex_resourcemanager_folder_iam_binding.images-puller   ]   kms_provider {     key_id = yandex_kms_symmetric_key.kms-key.id   } }  resource "yandex_vpc_network" "mynet" {   name = "mynet" }  resource "yandex_vpc_subnet" "mysubnet-a" {   v4_cidr_blocks = ["10.5.0.0/16"]   zone           = "ru-central1-a"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_vpc_subnet" "mysubnet-b" {   v4_cidr_blocks = ["10.6.0.0/16"]   zone           = "ru-central1-b"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_vpc_subnet" "mysubnet-c" {   v4_cidr_blocks = ["10.7.0.0/16"]   zone           = "ru-central1-c"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_iam_service_account" "myaccount" {   name        = local.sa_name   description = "K8S regional service account" }  resource "yandex_resourcemanager_folder_iam_binding" "k8s-clusters-agent" {   # Сервисному аккаунту назначается роль "k8s.clusters.agent".   folder_id = local.folder_id   role      = "k8s.clusters.agent"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "vpc-public-admin" {   # Сервисному аккаунту назначается роль "vpc.publicAdmin".   folder_id = local.folder_id   role      = "vpc.publicAdmin"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "images-puller" {   # Сервисному аккаунту назначается роль "container-registry.images.puller".   folder_id = local.folder_id   role      = "container-registry.images.puller"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_kms_symmetric_key" "kms-key" {   # Ключ для шифрования важной информации, такой как пароли, OAuth-токены и SSH-ключи.   name              = "kms-key"   default_algorithm = "AES_128"   rotation_period   = "8760h" # 1 год. }  resource "yandex_kms_symmetric_key_iam_binding" "viewer" {   symmetric_key_id = yandex_kms_symmetric_key.kms-key.id   role             = "viewer"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}",   ] }  resource "yandex_vpc_security_group" "k8s-main-sg" {   name        = "k8s-main-sg"   description = "Правила группы обеспечивают базовую работоспособность кластера. Примените ее к кластеру и группам узлов."   network_id  = yandex_vpc_network.mynet.id   ingress {     protocol          = "TCP"     description       = "Правило разрешает проверки доступности с диапазона адресов балансировщика нагрузки. Нужно для работы отказоустойчивого кластера и сервисов балансировщика."     predefined_target = "loadbalancer_healthchecks"     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ANY"     description       = "Правило разрешает взаимодействие мастер-узел и узел-узел внутри группы безопасности."     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ANY"     description       = "Правило разрешает взаимодействие под-под и сервис-сервис. Укажите подсети вашего кластера и сервисов."     v4_cidr_blocks    = concat(yandex_vpc_subnet.mysubnet-a.v4_cidr_blocks, yandex_vpc_subnet.mysubnet-b.v4_cidr_blocks, yandex_vpc_subnet.mysubnet-c.v4_cidr_blocks)     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ICMP"     description       = "Правило разрешает отладочные ICMP-пакеты из внутренних подсетей."     v4_cidr_blocks    = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]   }   ingress {     protocol          = "TCP"     description       = "Правило разрешает входящий трафик из интернета на диапазон портов NodePort. Добавьте или измените порты на нужные вам."     v4_cidr_blocks    = ["0.0.0.0/0"]     from_port         = 30000     to_port           = 32767   }   egress {     protocol          = "ANY"     description       = "Правило разрешает весь исходящий трафик. Узлы могут связаться с Yandex Container Registry, Yandex Object Storage, Docker Hub и т. д."     v4_cidr_blocks    = ["0.0.0.0/0"]     from_port         = 0     to_port           = 65535   } }

А чтобы развернуть ещё и группы узлов, понадобится новый ресурс:

main.tf
resource "yandex_kubernetes_node_group" "my_node_group_a" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-a"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-a-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-a.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-a"     }   } }

Это согласно инструкции Яндекса + документации Terraform.

Если хотим больше групп — давайте сделаем по одной в каждой зоне и в конечном итоге, с остальными ресурсами мы получим такую картину:

main.tf
locals {   cloud_id    = "b1gq90dgh25bebiu75o"   folder_id   = "b1gia87mbaomkfvsleds"   k8s_version = "1.22"   sa_name     = "myaccount" }  terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   } }  provider "yandex" {   folder_id = local.folder_id }  resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = yandex_vpc_network.mynet.id   master {     version = local.k8s_version     regional {       region = "ru-central1"       location {         zone      = yandex_vpc_subnet.mysubnet-a.zone         subnet_id = yandex_vpc_subnet.mysubnet-a.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-b.zone         subnet_id = yandex_vpc_subnet.mysubnet-b.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-c.zone         subnet_id = yandex_vpc_subnet.mysubnet-c.id       }     }     security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]   }   service_account_id      = yandex_iam_service_account.myaccount.id   node_service_account_id = yandex_iam_service_account.myaccount.id   depends_on = [     yandex_resourcemanager_folder_iam_binding.k8s-clusters-agent,     yandex_resourcemanager_folder_iam_binding.vpc-public-admin,     yandex_resourcemanager_folder_iam_binding.images-puller   ]   kms_provider {     key_id = yandex_kms_symmetric_key.kms-key.id   } }  resource "yandex_vpc_network" "mynet" {   name = "mynet" }  resource "yandex_vpc_subnet" "mysubnet-a" {   v4_cidr_blocks = ["10.5.0.0/16"]   zone           = "ru-central1-a"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_vpc_subnet" "mysubnet-b" {   v4_cidr_blocks = ["10.6.0.0/16"]   zone           = "ru-central1-b"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_vpc_subnet" "mysubnet-c" {   v4_cidr_blocks = ["10.7.0.0/16"]   zone           = "ru-central1-c"   network_id     = yandex_vpc_network.mynet.id }  resource "yandex_iam_service_account" "myaccount" {   name        = local.sa_name   description = "K8S regional service account" }  resource "yandex_resourcemanager_folder_iam_binding" "k8s-clusters-agent" {   # Сервисному аккаунту назначается роль "k8s.clusters.agent".   folder_id = local.folder_id   role      = "k8s.clusters.agent"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "vpc-public-admin" {   # Сервисному аккаунту назначается роль "vpc.publicAdmin".   folder_id = local.folder_id   role      = "vpc.publicAdmin"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "images-puller" {   # Сервисному аккаунту назначается роль "container-registry.images.puller".   folder_id = local.folder_id   role      = "container-registry.images.puller"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_kms_symmetric_key" "kms-key" {   # Ключ для шифрования важной информации, такой как пароли, OAuth-токены и SSH-ключи.   name              = "kms-key"   default_algorithm = "AES_128"   rotation_period   = "8760h" # 1 год. }  resource "yandex_kms_symmetric_key_iam_binding" "viewer" {   symmetric_key_id = yandex_kms_symmetric_key.kms-key.id   role             = "viewer"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}",   ] }  resource "yandex_vpc_security_group" "k8s-main-sg" {   name        = "k8s-main-sg"   description = "Правила группы обеспечивают базовую работоспособность кластера. Примените ее к кластеру и группам узлов."   network_id  = yandex_vpc_network.mynet.id   ingress {     protocol          = "TCP"     description       = "Правило разрешает проверки доступности с диапазона адресов балансировщика нагрузки. Нужно для работы отказоустойчивого кластера и сервисов балансировщика."     predefined_target = "loadbalancer_healthchecks"     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ANY"     description       = "Правило разрешает взаимодействие мастер-узел и узел-узел внутри группы безопасности."     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ANY"     description       = "Правило разрешает взаимодействие под-под и сервис-сервис. Укажите подсети вашего кластера и сервисов."     v4_cidr_blocks    = concat(yandex_vpc_subnet.mysubnet-a.v4_cidr_blocks, yandex_vpc_subnet.mysubnet-b.v4_cidr_blocks, yandex_vpc_subnet.mysubnet-c.v4_cidr_blocks)     from_port         = 0     to_port           = 65535   }   ingress {     protocol          = "ICMP"     description       = "Правило разрешает отладочные ICMP-пакеты из внутренних подсетей."     v4_cidr_blocks    = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]   }   ingress {     protocol          = "TCP"     description       = "Правило разрешает входящий трафик из интернета на диапазон портов NodePort. Добавьте или измените порты на нужные вам."     v4_cidr_blocks    = ["0.0.0.0/0"]     from_port         = 30000     to_port           = 32767   }   egress {     protocol          = "ANY"     description       = "Правило разрешает весь исходящий трафик. Узлы могут связаться с Yandex Container Registry, Yandex Object Storage, Docker Hub и т. д."     v4_cidr_blocks    = ["0.0.0.0/0"]     from_port         = 0     to_port           = 65535   } }  resource "yandex_kubernetes_node_group" "my_node_group_a" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-a"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-a-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-a.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-a"     }   } }  resource "yandex_kubernetes_node_group" "my_node_group_b" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-b"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-b-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-b.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-b"     }   } }  resource "yandex_kubernetes_node_group" "my_node_group_c" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-c"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-c-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-c.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-c"     }   } }

В принципе, если не париться, можно запускать и так. Всё заведётся и будет работать. Мы получим отказоустойчивый кластер k8s с рабочими узлами в разных зонах доступности, да ещё и с автоскейлингом. Но сможем ли мы оставить всё как есть? Конечно же нет, давайте сделаем из этого франкенштейна аккуратные, красивые, а главное функциональные манифесты, с которыми в дальнейшем можно будет жить с чистой совестью.

Погнали)

Работать мы будем в отдельной директории my_project

mkdir ~/my_project

Для того, чтобы создать кластер k8s нам потребуется сеть, подсеть и сервисный аккаунт. Поэтому, в соответствии с упомянутыми ранее рекомендациями, мы начнём создавать инфраструктуру с сети и её компонентов в отдельном рабочем каталоге network:

mkdir ~/my_project/network cd ~/my_project/network

Далее мы создадим файл main.tf, в нем пропишем провайдера и доступ к облаку:

my_project/network/main.tf
terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   }   required_version = ">= 0.13"    backend "s3" {     endpoint   = "storage.yandexcloud.net"     bucket     = "test-tfstate"     region     = "ru-central1"     key        = "my-project-network.tfstate"     shared_credentials_file = "storage.key"      skip_region_validation      = true     skip_credentials_validation = true   } }  provider "yandex" {   service_account_key_file = "key.json"   cloud_id                 = "b1gq90dgh25bebiu75o"   folder_id                = "b1gia87mbaomkfvsleds"   zone                     = "ru-central1-a" }

Всё, что здесь описано, должно быть знакомо по предыдущему ролику, поэтому не будем останавливаться на этом и перейдем к описанию сети и подсети, их мы опишем в файле vpc.tf:

my_project/network/vpc.tf
resource "yandex_vpc_network" "network-main" {   name = "mynet" }  resource "yandex_vpc_subnet" "mysubnet-a" {   v4_cidr_blocks = ["10.5.0.0/16"]   zone           = "ru-central1-a"   network_id     = yandex_vpc_network.network-main.id }  resource "yandex_vpc_subnet" "mysubnet-b" {   v4_cidr_blocks = ["10.6.0.0/16"]   zone           = "ru-central1-b"   network_id     = yandex_vpc_network.network-main.id }  resource "yandex_vpc_subnet" "mysubnet-c" {   v4_cidr_blocks = ["10.7.0.0/16"]   zone           = "ru-central1-c"   network_id     = yandex_vpc_network.network-main.id }

Это согласно инструкции Яндекса. Давайте на примере кода в main.tf и vpc.tf проведём ряд усовершенствований. Для начала вынесем имя сети и ещё пару параметров в переменные. Создадим файлы terraform.tfvars и variables.tf. Теперь наш рабочий каталог выглядит следующим образом:

my_project/ └── network     ├── main.tf     ├── terraform.tfvars     ├── variables.tf     └── vpc.tf

В файле variables.tf укажем, что хотим создать четыре переменные:

my_project/network/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== network ============== variable "network_name" {   description = "The name of main network"   type        = string }

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

Далее, в файле terraform.tfvars мы присвоим значение этим переменным:

my_project/network/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== network ============== network_name = "mynet"

И в конце концов перепишем в файлах main.tf и vpc.tf соответствующие параметры:

my_project/network/main.tf
terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   }   required_version = ">= 0.13"    backend "s3" {     endpoint   = "storage.yandexcloud.net"     bucket     = "test-tfstate"     region     = "ru-central1"     key        = "my-project-network.tfstate"     shared_credentials_file = "storage.key"      skip_region_validation      = true     skip_credentials_validation = true   } }  provider "yandex" {   service_account_key_file = "key.json"   cloud_id                 = var.cloud_id   folder_id                = var.folder_id   zone                     = var.default_zone }

my_project/network/vpc.tf
resource "yandex_vpc_network" "network-main" {   name = var.network_name }  resource "yandex_vpc_subnet" "mysubnet-a" {   v4_cidr_blocks = ["10.5.0.0/16"]   zone           = "ru-central1-a"   network_id     = yandex_vpc_network.network-main.id }  resource "yandex_vpc_subnet" "mysubnet-b" {   v4_cidr_blocks = ["10.6.0.0/16"]   zone           = "ru-central1-b"   network_id     = yandex_vpc_network.network-main.id }  resource "yandex_vpc_subnet" "mysubnet-c" {   v4_cidr_blocks = ["10.7.0.0/16"]   zone           = "ru-central1-c"   network_id     = yandex_vpc_network.network-main.id }

Отлично, с переменными разобрались, теперь перейдём к циклу — зачем нам описывать три подсети, по сути одинаковых ресурса, если можно обойтись одним блоком кода, верно? Добавим в variables.tf описание переменной subnets, только здесь мы обозначим не один параметр, а их набор. У Terraform есть тип map, который нам отлично подходит под задачу (ещё бы, для этого и создавался):

my_project/network/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== network ============== variable "network_name" {   description = "The name of main network"   type        = string }  #=========== subnet ============== variable "subnets" {   description = "Subnets for k8s"    type = map(list(object(     {       name = string,       zone = string,       cidr = list(string)     }))   )    validation {     condition     = alltrue([for i in keys(var.subnets) : alltrue([for j in lookup(var.subnets, i) : contains(["ru-central1-a", "ru-central1-b", "ru-central1-c"], j.zone)])])     error_message = "Error! Zones not supported!"   } }

Тут же, исключительно ради примера, делаю проверку на корректность указываемых значений с помощью блока validation, где в condition пишу условие, а в параметре error_message текст ошибки. Можно делать проверки на всё подряд, но это отдельная и трудоемкая задача. Остановимся на том, что просто упомянули о такой возможности.

Далее, в файле terraform.tfvars мы просто укажем значения:

my_project/network/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== network ============== network_name = "mynet"  #=========== subnet ============== subnets = {   "k8s_masters" = [     {       name = "k8s_master_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.1.0/28"]     },     {       name = "k8s_master_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.2.0/28"]     },     {       name = "k8s_master_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.3.0/28"]     }   ],   "k8s_workers" = [     {       name = "k8s_worker_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.4.0/28"]     },     {       name = "k8s_worker_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.5.0/28"]     },     {       name = "k8s_worker_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.6.0/28"]     }   ], }

Как вы можете видеть, я добавил ещё 3 подсети для воркер-нод, чтобы всё разграничить.

А теперь создадим файл local.tf, со следующим содержимым:

my_project/network/locals.tf
locals {   subnet_array = flatten([for k, v in var.subnets : [for j in v : {     name = j.name     zone = j.zone     cidr = j.cidr     }   ]]) }

Выражение перебирает входную переменную var.subnets и с помощью встроенной функции flatten сглаживает полученный вложенный список в единый одномерный список объектов. Каждый объект в результирующем списке имеет три атрибута: name, zone и cidr. При этом, оставляя значение для ключа cidr в списке, что очень важно. Ведь далее в ресурсе нам нужно указывать именно список.

Таким образом, мы можем переписать наш код в vpc.tf:

my_project/network/vpc.tf
resource "yandex_vpc_network" "network-main" {   name = var.network_name }  resource "yandex_vpc_subnet" "subnet-main" {   for_each = {     for k, v in local.subnet_array : "${v.name}" => v   }   network_id = yandex_vpc_network.network-main.id   v4_cidr_blocks = each.value.cidr   zone           = each.value.zone   name           = each.value.name }

Здесь мы используем цикл for_each, в котором сопоставляем уникальный идентификатор с каждым элементом списка local.subnet_array. В данном случае, уникальным идентификатором является “name”.

Таким образом, сейчас для Terraform это выглядит как 6 ресурсов, и он создаст отдельные подсети с уникальным именем.

Всё, у нас есть 6 подсетей ?

Что ещё. А, ну давайте создадим статические IP-адреса, сколько бы их ни было. Действуем по тому же сценарию, в variables.tf добавляем новую переменную:

my_project/network/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== network ============== variable "network_name" {   description = "The name of main network"   type        = string }  #=========== subnet ============== variable "subnets" {   description = "Subnets for k8s"    type = map(list(object(     {       name = string,       zone = string,       cidr = list(string)     }))   )    validation {     condition     = alltrue([for i in keys(var.subnets) : alltrue([for j in lookup(var.subnets, i) : contains(["ru-central1-a", "ru-central1-b", "ru-central1-c"], j.zone)])])     error_message = "Error! Zones not supported!"   } }  #=========== external_ip ============== variable "external_static_ips" {   description = "static ips"    type = map(list(object(     {       name = string,       zone = string     }))   ) }

После этого в terraform.tfvars добавляем значения. Сделаю один, но мы сразу закладываем возможность создания нескольких:

my_project/network/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== network ============== network_name = "mynet"  #=========== subnet ============== subnets = {   "k8s_masters" = [     {       name = "k8s_master_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.1.0/28"]     },     {       name = "k8s_master_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.2.0/28"]     },     {       name = "k8s_master_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.3.0/28"]     }   ],   "k8s_workers" = [     {       name = "k8s_worker_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.4.0/28"]     },     {       name = "k8s_worker_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.5.0/28"]     },     {       name = "k8s_worker_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.6.0/28"]     }   ], }  #=========== external_ip ============== external_static_ips = {   ingress_lb = [     {       name = "ingress_lb_zone_ru_central1_a"       zone = "ru-central1-a"     }   ] }

Создаём локальную переменную external_ips_array в locals.tf:

my_project/network/locals.tf
locals {   subnet_array = flatten([for k, v in var.subnets : [for j in v : {     name = j.name     zone = j.zone     cidr = j.cidr     }   ]])   external_ips_array = flatten([for k, v in var.external_static_ips : [for j in v : {     name = j.name     zone = j.zone     }   ]]) }

Ну и, наконец, создадим файл static-ip.tf:

my_project/network/static-ip.tf
resource "yandex_vpc_address" "public_addr" {   for_each = {     for v in local.external_ips_array : "${v.name}" => v   }   name = each.value.name   external_ipv4_address {     zone_id = each.value.zone   } }

Всё как и с подсетями.

Что ещё нам потребуется? Конечно же связность подсетей и безопасность, а значит пора заняться описанием security group.

По уже сложившейся традиции, создадим файл security-group.tf, где опишем группы безопасности для мастеров и воркеров:

my_project/network/security-group.tf
resource "yandex_vpc_security_group" "internal" {   name        = "internal"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "yc_internal"   }   ingress {     protocol          = "ANY"     description       = "self"     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   }   egress {     protocol          = "ANY"     description       = "self"     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   } }  resource "yandex_vpc_security_group" "k8s_master" {   name        = "k8s-master"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "k8s-master"   }   ingress {     protocol       = "TCP"     description    = "access to api k8s"     v4_cidr_blocks = var.white_ips_for_master     port           = 443   }   ingress {     protocol          = "TCP"     description       = "access to api k8s from Yandex lb"     predefined_target = "loadbalancer_healthchecks"     from_port         = 0     to_port           = 65535   } }  resource "yandex_vpc_security_group" "k8s_worker" {   name        = "k8s-worker"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "k8s-worker"   }   ingress {     protocol       = "ANY"     description    = "any connections"     v4_cidr_blocks = ["0.0.0.0/0"]     from_port      = 0     to_port        = 65535   }   egress {     protocol       = "ANY"     description    = "any connections"     v4_cidr_blocks = ["0.0.0.0/0"]     from_port      = 0     to_port        = 65535   } }

Лично я считаю такую организацию групп безопасности более верной для кластера:

  •  internal, для связности мастеров и воркеров, здесь разрешено всё в рамках этой самой группы. 

  •  k8s_master исключительно под мастеров где разрешён доступ к 443 порту по переменной, которой мы должны передать вайтлист и служебное правило для балансировщиков.

  •  k8s_worker группа безопасности для воркеров, где мы разрешим вообще всё.

 Создадим переменную для вайтлиста и пропишем значение для неё.

my_project/network/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== network ============== variable "network_name" {   description = "The name of main network"   type        = string }  #=========== subnet ============== variable "subnets" {   description = "Subnets for k8s"    type = map(list(object(     {       name = string,       zone = string,       cidr = list(string)     }))   )    validation {     condition     = alltrue([for i in keys(var.subnets) : alltrue([for j in lookup(var.subnets, i) : contains(["ru-central1-a", "ru-central1-b", "ru-central1-c"], j.zone)])])     error_message = "Error! Zones not supported!"   } }  #=========== external_ip ============== variable "external_static_ips" {   description = "static ips"    type = map(list(object(     {       name = string,       zone = string     }))   ) }  #=========== security_group ============== variable "white_ips_for_master" {   type = list(string) }

my_project/network/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== network ============== network_name = "mynet"  #=========== subnet ============== subnets = {   "k8s_masters" = [     {       name = "k8s_master_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.1.0/28"]     },     {       name = "k8s_master_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.2.0/28"]     },     {       name = "k8s_master_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.3.0/28"]     }   ],   "k8s_workers" = [     {       name = "k8s_worker_zone_a"       zone = "ru-central1-a"       cidr = ["10.0.4.0/28"]     },     {       name = "k8s_worker_zone_b"       zone = "ru-central1-b"       cidr = ["10.0.5.0/28"]     },     {       name = "k8s_worker_zone_c"       zone = "ru-central1-c"       cidr = ["10.0.6.0/28"]     }   ], }  #=========== external_ip ============== external_static_ips = {   ingress_lb = [     {       name = "ingress_lb_zone_ru_central1_a"       zone = "ru-central1-a"     }   ] }  #=========== security_group ============== white_ips_for_master = ["0.0.0.0/0"]    # Add your IP ["$YOUR_IP/32"]

С network закончили, переносим сюда ключи от сервисного аккаунта и от бакета. Рабочий каталог, после всех наших манипуляций, должен выглядеть следующим образом:

my_project/ └── network     ├── external_ip.tf     ├── key.json     ├── locals.tf     ├── main.tf     ├── outputs.tf     ├── security_group.tf     ├── storage.key     ├── terraform.tfvars     ├── variables.tf     └── vpc.tf

Можно запускать terraform init, затем terraform apply.

Теперь переходим к каталогу k8s

mkdir ~/my_project/k8s cd ~/my_project/k8s

Всё по аналогии: создадим файлы main.tf, variables.tf и terraform.tfvars, а также перенесём наши ключи. Кроме того, создадим ещё три конфигурационных файла cluster.tf, node_group.tf и service_account.tf:

my_project/ ├── k8s │   ├── cluster.tf │   ├── key.json │   ├── main.tf │   ├── node_group.tf │   ├── service_account.tf │   ├── storage.key │   ├── terraform.tfvars │   └── variables.tf └── network     ├── external_ip.tf     ├── key.json     ├── locals.tf     ├── main.tf     ├── outputs.tf     ├── security_group.tf     ├── storage.key     ├── terraform.tfvars     ├── variables.tf     └── vpc.tf

Переносим ресурсы для групп узлов в node_group.tf, описание кластера в cluster.tf, и всё, что относится к сервисным аккаунтам в service_account.tf, сюда же положим и ключ шифрования.

my_project/k8s/cluster.tf
resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = yandex_vpc_network.mynet.id   master {     version = local.k8s_version     regional {       region = "ru-central1"       location {         zone      = yandex_vpc_subnet.mysubnet-a.zone         subnet_id = yandex_vpc_subnet.mysubnet-a.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-b.zone         subnet_id = yandex_vpc_subnet.mysubnet-b.id       }       location {         zone      = yandex_vpc_subnet.mysubnet-c.zone         subnet_id = yandex_vpc_subnet.mysubnet-c.id       }     }     security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]   }   service_account_id      = yandex_iam_service_account.myaccount.id   node_service_account_id = yandex_iam_service_account.myaccount.id   depends_on = [     yandex_resourcemanager_folder_iam_binding.k8s-clusters-agent,     yandex_resourcemanager_folder_iam_binding.vpc-public-admin,     yandex_resourcemanager_folder_iam_binding.images-puller   ]   kms_provider {     key_id = yandex_kms_symmetric_key.kms-key.id   } }

my_project/k8s/service_account.tf
resource "yandex_iam_service_account" "myaccount" {   name        = local.sa_name   description = "K8S regional service account" }  resource "yandex_resourcemanager_folder_iam_binding" "k8s-clusters-agent" {   # Сервисному аккаунту назначается роль "k8s.clusters.agent".   folder_id = local.folder_id   role      = "k8s.clusters.agent"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "vpc-public-admin" {   # Сервисному аккаунту назначается роль "vpc.publicAdmin".   folder_id = local.folder_id   role      = "vpc.publicAdmin"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "images-puller" {   # Сервисному аккаунту назначается роль "container-registry.images.puller".   folder_id = local.folder_id   role      = "container-registry.images.puller"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_kms_symmetric_key" "kms-key" {   # Ключ для шифрования важной информации, такой как пароли, OAuth-токены и SSH-ключи.   name              = "kms-key"   default_algorithm = "AES_128"   rotation_period   = "8760h" # 1 год. }  resource "yandex_kms_symmetric_key_iam_binding" "viewer" {   symmetric_key_id = yandex_kms_symmetric_key.kms-key.id   role             = "viewer"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}",   ] }

my_project/k8s/node_group.tf
resource "yandex_kubernetes_node_group" "my_node_group_a" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-a"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-a-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-a.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-a"     }   } }  resource "yandex_kubernetes_node_group" "my_node_group_b" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-b"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-b-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-b.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-b"     }   } }  resource "yandex_kubernetes_node_group" "my_node_group_c" {   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = "worker-c"   description = "description"   version     = local.k8s_version   labels = {     "key" = "value"   }   instance_template {     platform_id = "standard-v1"     name        = "worker-c-{instance.short_id}"     network_interface {       nat                = true       subnet_ids         = [yandex_vpc_subnet.mysubnet-c.id]       security_group_ids = [yandex_vpc_security_group.k8s-main-sg.id]     }     resources {       memory = 2       cores  = 2     }     boot_disk {       type = "network-hdd"       size = 32     }     scheduling_policy {       preemptible = false     }   }   scale_policy {     auto_scale {       min     = 1       max     = 3       initial = 1     }   }   allocation_policy {     location {       zone = "ru-central1-c"     }   } }

Так же, как и в network, создадим переменные:

my_project/k8s/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }

Присвоим им значения:

my_project/k8s/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"

И перепишем main.tf

my_project/k8s/main.tf
terraform {   required_providers {     yandex = {       source = "yandex-cloud/yandex"     }   }   backend "s3" {     endpoint                = "storage.yandexcloud.net"     bucket                  = "test-tfstate"     region                  = "ru-central1"     key                     = "my-project-k8s.tfstate"     shared_credentials_file = "storage.key"      skip_region_validation      = true     skip_credentials_validation = true   } }  provider "yandex" {   service_account_key_file = "key.json"   cloud_id                 = var.cloud_id   folder_id                = var.folder_id   zone                     = var.default_zone }

Не забываем сменить имя файла состояния в параметре key.

Итак, давайте переписывать. Начинаем с service_account.tf, у нас имя сервисного аккаунта было задано через locals, мы это переделаем. Нам нужно имя сервисного аккаунта и ключа шифрования задать через переменные, для этого создаём их:

my_project/k8s/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== service_account ============== variable "service_account_name" {   description = "Name of service account"   type = string   default = null }  variable "kms_provider_key_name" {   description = "KMS key name."   type = string   default = null }

И определяем:

my_project/k8s/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== service_account ============== service_account_name  = "myaccount" kms_provider_key_name = "kms-key"

Переписываем service_account.tf:

my_project/k8s/service_account.tf
resource "yandex_iam_service_account" "myaccount" {   name        = var.service_account_name   description = "K8S regional service account" }  resource "yandex_resourcemanager_folder_iam_binding" "editor" {   # Сервисному аккаунту назначается роль "editor".   folder_id = var.folder_id   role      = "editor"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_resourcemanager_folder_iam_binding" "images-puller" {   # Сервисному аккаунту назначается роль "container-registry.images.puller".   folder_id = var.folder_id   role      = "container-registry.images.puller"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}"   ] }  resource "yandex_kms_symmetric_key" "kms-key" {   # Ключ для шифрования важной информации, такой как пароли, OAuth-токены и SSH-ключи.   name              = var.kms_provider_key_name   default_algorithm = "AES_128"   rotation_period   = "8760h" # 1 год. }  resource "yandex_kms_symmetric_key_iam_binding" "viewer" {   symmetric_key_id = yandex_kms_symmetric_key.kms-key.id   role             = "viewer"   members = [     "serviceAccount:${yandex_iam_service_account.myaccount.id}",   ] }

Что ж, вот мы и можем, наконец, приступить к оптимизации кода кластера. Давайте подставим наши значения в cluster, попутно прописав переменные для некоторых из них:

my_project/k8s/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== service_account ============== variable "service_account_name" {   description = "Name of service account"   type = string   default = null }  variable "kms_provider_key_name" {   description = "KMS key name."   type = string   default = null }   #=========== cluster ============== variable "cluster_name" {   description = "Name of a specific Kubernetes cluster."   type        = string   default     = null }  variable "network_policy_provider" {   description = "Network policy provider for the cluster. Possible values: CALICO."   type = string   default = "CALICO" }  variable "master_version" {   description = "Version of Kubernetes that will be used for master."   type = string   default = null }  variable "master_public_ip" {   description = "Boolean flag. When true, Kubernetes master will have a visible ipv4 address."   type = bool   default = true }  variable "master_region" {   description = "Name of region where cluster will be created. Required for regional cluster, not used for zonal cluster."   type = string   default = null }

Как вы можете видеть, у двух переменных, а именно master_public_ip и network_policy_provider, уже задано дефолтное значение. Нам нужно в terraform.tfvars указать остальные:

my_project/k8s/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== service_account ============== service_account_name  = "myaccount" kms_provider_key_name = "kms-key"  #=========== cluster ============== cluster_name   = "cluster" master_version = "1.22" master_region  = "ru-central1"

Вот мы и дошли до важнейшего момента — нам необходимо указать id сети, но её мы поднимали другим манифестом Terraform’а, а значит, из этого манифеста Terraform не может увидеть созданные там ресурсы. как же быть?

Для того, чтобы получить какие-либо данные извне, есть data sources. С помощью data мы можем указать терраформ источник, откуда ему следует взять данные. 

Создадим файл data.tf:

my_project/k8s/data.tf
data "yandex_vpc_network" "network" {   name = "mynet" }

Источник данных указывается очень просто:  data, и прописываем откуда брать данные, а нужна нам сеть с именем mynet.

Теперь нам достаточно сослаться на этот источник в описании ресурса, обязательно указав что именно нам нужно оттуда забрать — data.yandex_vpc_network.mynet.id:

resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = data.yandex_vpc_network.mynet.id [...] }

Можем оставить так, но что, если имя сети поменяется? Переписали мы конфигурацию сети, нам же придётся и в каталоге k8s в data.tf поменять параметр, а ведь мы переписываем код для того, чтобы не лазить больше в конфигурационные файлы. Мы можем вынести этот параметр в переменные и менять его там — скажете вы. А что, если вообще не заморачиваться над тем, как назван ресурс? Что, если мы укажем id сети так, что нам нигде не придётся ничего переписывать при любых изменениях в манифесте network? Для этого нужно ответить на один вопрос — а где ещё хранятся данные о созданных ресурсах любого манифеста? Верно, в файле состояния. Вот мы и укажем Terraform’у сходить и прочитать другой tfstate файл.

Создадим другой источник данных и в нем укажем путь и доступы к чужому файлу состояния. 

my_project/k8s/data.tf
data "terraform_remote_state" "network" {   backend = "s3"   config = {     endpoint                = "storage.yandexcloud.net"     bucket                  = var.network_backet_name     region                  = "ru-central1"     key                     = var.network_state_key     shared_credentials_file = "storage.key"      skip_region_validation      = true     skip_credentials_validation = true   }

Как видите, здесь используются переменные, давайте их создадим. И присвоим значения — их мы возьмём из main.tf в каталоге network. Расположил я их после переменных для main.

my_project/k8s/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== terraform_remote_state ============== variable "network_state_key" {   description = "The key of state for the network."   type        = string   default     = null }  variable "network_bucket_name" {   description = "The name of bucket for the network."   type        = string   default     = null }  #=========== service_account ============== variable "service_account_name" {   description = "Name of service account"   type = string   default = null }  variable "kms_provider_key_name" {   description = "KMS key name."   type = string   default = null }   #=========== cluster ============== variable "cluster_name" {   description = "Name of a specific Kubernetes cluster."   type        = string   default     = null }  variable "network_policy_provider" {   description = "Network policy provider for the cluster. Possible values: CALICO."   type = string   default = "CALICO" }  variable "master_version" {   description = "Version of Kubernetes that will be used for master."   type = string   default = null }  variable "master_public_ip" {   description = "Boolean flag. When true, Kubernetes master will have a visible ipv4 address."   type = bool   default = true }  variable "master_region" {   description = "Name of region where cluster will be created. Required for regional cluster, not used for zonal cluster."   type = string   default = null }

my_project/k8s/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== terraform_remote_state ============== network_state_key = "my-project-network.tfstate" network_bucket_name = "test-tfstate"  #=========== service_account ============== service_account_name  = "myaccount" kms_provider_key_name = "kms-key"  #=========== cluster ============== cluster_name   = "cluster" master_version = "1.22" master_region  = "ru-central1"

Теперь мы можем читать другой файл состояния и считывать оттуда значения, однако, их там пока нет. Для того, чтобы они появились, нам нужно вернуться в каталог network и оформить outputs. Итак, создаём outputs.tf и описываем то, что хотим вывести:

my_project/network/outputs.tf
output "network_id" {   value = yandex_vpc_network.mynet.id }

А нужно нам id сети. Вот и всё. Обязательно запустите terraform apply, чтобы данные записались в файл состояния.

Теперь, для того, чтобы нам сослаться на какой-либо ресурс из network, потребуется постоянно указывать конструкцию data.terraform_remote_state.network.outputs. и имя того output’а который нам нужен.

Например:

resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = data.terraform_remote_state.network.outputs.network_id [...] }

Давайте используем locals, чтобы упростить себе жизнь. Итак, создаём файл locals.tf тут мы создадим переменную:

my_project/k8s/locals.tf
locals {   network_output = data.terraform_remote_state.network.outputs }

Теперь можем всегда указывать просто:

resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id = local.network_output.network_id [...] }

Думаю так гораздо удобнее.

Давайте для групп безопасности тоже выведем id, т.к. они нам также понадобятся:

my_project/network/outputs.tf
output "network_id" {   value = yandex_vpc_network.mynet.id }  output "sg_internal" {   value = yandex_vpc_security_group.internal.id }  output "sg_k8s_master" {   value = yandex_vpc_security_group.k8s_master.id }  output "sg_k8s_worker" {   value = yandex_vpc_security_group.k8s_worker.id }

Но также нам нужны зоны и id подсетей. Здесь уже сложнее, ведь мы не можем просто указать эти значения, так же, как и для других, поскольку подсети у нас создаются циклом в network и писать output под каждую подсеть мы не можем. 

Для нашей задачи мы действительно используем output, но для этого напишем небольшое условие:

my_project/network/outputs.tf
output "network_id" {   value = yandex_vpc_network.mynet.id }  output "sg_internal" {   value = yandex_vpc_security_group.internal.id }  output "sg_k8s_master" {   value = yandex_vpc_security_group.k8s_master.id }  output "sg_k8s_worker" {   value = yandex_vpc_security_group.k8s_worker.id }  output "k8s_masters_subnet_info" {   value = [for k, v in var.subnets["k8s_masters"] : zipmap(["subnet_id", "zone"], [yandex_vpc_subnet.subnet-main[v.name].id, yandex_vpc_subnet.subnet-main[v.name].zone])] }  output "k8s_workers_subnet_info" {   value = [for k, v in var.subnets["k8s_workers"] : zipmap(["subnet_id", "zone"], [yandex_vpc_subnet.subnet-main[v.name].id, yandex_vpc_subnet.subnet-main[v.name].zone])] }

Как вы помните, мы создали 6 подсетей, три под мастеров и три под воркеров, вот я и хочу получить их id и zone разными output»ами: первый — под наши требования для кластера, а второй — заранее сделаем для воркеров.

Здесь мы фильтруем значение переменной subnets, чтобы бралась информация только по необходимым подсетям(в данном случае для мастеров). Далее с помощью функции zipmap мы создаём список, где присваиваем ключам subnet_id и zone соответствующие атрибуты из переменной, и так для каждой подсети в k8s_masters.

Аналогично и для подсетей в k8s_workers.

Перезапускаем terraform apply.

Окей, теперь переписываем конфигурацию на переменные, а ID групп безопасности указываем две, internal и k8s_master из output»ов предыдущего манифеста:

my_project/k8s/cluster.tf
resource "yandex_kubernetes_cluster" "k8s-regional" {   network_id              = local.network_output.network_id   network_policy_provider = var.network_policy_provider   service_account_id      = yandex_iam_service_account.myaccount.id   node_service_account_id = yandex_iam_service_account.myaccount.id   master {     version   = var.master_version     public_ip = var.master_public_ip     regional {       region = var.master_region       dynamic "location" {         for_each = local.network_output.k8s_masters_subnet_info         content {           zone      = location.value["zone"]           subnet_id = location.value["subnet_id"]         }       }     }     security_group_ids = [local.network_output.sg_internal, local.network_output.sg_k8s_master]   }     kms_provider {     key_id = yandex_kms_symmetric_key.kms-key.id   }   depends_on = [     yandex_resourcemanager_folder_iam_binding.editor,     yandex_resourcemanager_folder_iam_binding.images-puller   ] }

Для того, чтобы указать подсети и зоны, мы используем dynamic блок с циклом for_each, указываем, откуда будем брать значения (а берем мы их из нашего нового output»a). Далее мы описываем content, и сюда пишем те параметры, которые нужны этому блоку, а нужны зона и ID, теперь указываем откуда брать значения, а их мы берем из того цикла, который мы передали, т. е. location.value и указываем какой элемент нужен.

Запускаем terraform init и terraform apply в каталоге k8s.

Ну вот, у нас получилось поднять инфраструктуру с помощью двух разных манифестов Terraform. Бежим в Яндекс и проверяем — кластер на месте. Нам осталось решить, как описать группы узлов. Они у нас так же, как и подсети до этого, указаны тремя ресурсами. Давайте исправлять. Как вы наверное уже догадались, делать мы это будем через тот же for_each. Но сначала, создадим переменную:

my_project/k8s/variables.tf
#=========== main ============== variable "cloud_id" {   description = "The cloud ID"   type        = string } variable "folder_id" {   description = "The folder ID"   type        = string } variable "default_zone" {   description = "The default zone"   type        = string   default     = "ru-cenral1-a" }  #=========== terraform_remote_state ============== variable "network_state_key" {   description = "The key of state for the network."   type        = string   default     = null }  variable "network_bucket_name" {   description = "The name of bucket for the network."   type        = string   default     = null }  #=========== service_account ============== variable "service_account_name" {   description = "Name of service account"   type = string   default = null }  variable "kms_provider_key_name" {   description = "KMS key name."   type = string   default = null }  #=========== cluster ============== variable "cluster_name" {   description = "Name of a specific Kubernetes cluster."   type        = string   default     = null }  variable "network_policy_provider" {   description = "Network policy provider for the cluster. Possible values: CALICO."   type = string   default = "CALICO" }  variable "master_version" {   description = "Version of Kubernetes that will be used for master."   type = string   default = null }  variable "master_public_ip" {   description = "Boolean flag. When true, Kubernetes master will have a visible ipv4 address."   type = bool   default = true }  variable "master_region" {   description = "Name of region where cluster will be created. Required for regional cluster, not used for zonal cluster."   type = string   default = null }  #=========== node_groups ============== variable "node_groups" {   description = "Parameters of Kubernetes node groups."   default = {} }

И присвоим значение этой переменной в terraform.tfvars:

my_project/k8s/terraform.tfvars
#=========== main ============== cloud_id  = "b1gq90dgh25bebiu75o" folder_id = "b1gia87mbaomkfvsleds"  #=========== terraform_remote_state ============== network_state_key = "my-project-network.tfstate" network_bucket_name = "test-tfstate"  #=========== service_account ============== service_account_name  = "myaccount" kms_provider_key_name = "kms-key"  #=========== cluster ============== cluster_name   = "cluster" master_version = "1.22" master_region  = "ru-central1"  #=========== node_groups ============== node_groups = {   node-group-a = {     platform_id    = "standard-v1",     name           = "worker-a-{instance.short_id}",     cores          = 2,     memory         = 2,     boot_disk_type = "network-ssd",     boot_disk_size = 32,     zone           = "ru-central1-a",     auto_scale = {       min     = 1,       max     = 3,       initial = 1     }   }   node-group-b = {     platform_id    = "standard-v1",     name           = "worker-b-{instance.short_id}",     cores          = 2,     memory         = 2,     boot_disk_type = "network-ssd",     boot_disk_size = 32,     zone           = "ru-central1-b",     fixed_scale = {       size     = 1     }   }   node-group-c = {     platform_id    = "standard-v1",     name           = "worker-c-{instance.short_id}",     cores          = 2,     memory         = 2,     boot_disk_type = "network-ssd",     boot_disk_size = 32,     zone           = "ru-central1-c",     fixed_scale = {       size     = 1     }   } }

Тут мы указываем всё то, что хотим видеть в параметрах ресурса, главное потом это правильно присвоить. Как вы могли заметить, блоки отличаются, а именно для node‑group‑a указан auto_scale, а для остальных групп fixed_scale. Таким образом, я хочу добавить возможность менять тип групп через переменные.

Также, в yandex_kubernetes_node_group нам потребуется указать id подсетей. Вспоминаем о нашем ранее созданном output»е — k8s_workers_subnet_info, но там передаётся не только id, но и зона. В чем проблема? — спросите вы. Ведь можно переписать output, чтобы он выдавал только id. Но нет, в таком случае у нас пропадёт привязка к зоне, ведь мы просто получим набор id, и понять какой из них принадлежит определённой зоне не получится. Да, они у нас сейчас по порядку, первый id будет относится к зоне ru‑cenral1-a, второй — к b и третий — к c. Но это мы в манифесте network нашей переменной так значения по порядку указали, а вот если их поменять местами, получим путаницу с таким подходом, и поднимется у нас, к примеру, группа с именем node‑group‑a в зоне b. Поэтому output мы не трогаем, а сделаем преобразование данных через locals:

my_project/k8s/locals.tf
locals {   network_output = data.terraform_remote_state.network.outputs   worker_subnet_list = zipmap([for subnet in local.network_output.k8s_workers_subnet_info : subnet.zone], [for subnet in local.network_output.k8s_workers_subnet_info : subnet.subnet_id]) }

Создаём переменную worker_subnet_list, тут мы создадим список, где каждой зоне присвоится id.

Возвращаемся к нашему ресурсу в файл node_group.tf:

my_project/k8s/node_group.tf
resource "yandex_kubernetes_node_group" "my_node_groups" {   for_each    = var.node_groups   cluster_id  = yandex_kubernetes_cluster.k8s-regional.id   name        = each.key   description = lookup(each.value, "description", null)   version     = lookup(each.value, "version", var.master_version)   labels = lookup(each.value, "labels", null)   instance_template {     platform_id = lookup(each.value, "platform_id", null)     name        = lookup(each.value, "name", null)     network_interface {       nat                = lookup(each.value, "nat", true)       subnet_ids         = [lookup(local.worker_subnet_list, each.value["zone"])]       security_group_ids = [local.network_output.sg_internal, local.network_output.sg_k8s_worker]     }     resources {       memory = lookup(each.value, "memory", 2)       cores  = lookup(each.value, "cores", 2)     }     boot_disk {       type = lookup(each.value, "boot_disk_type", "network-hdd")       size = lookup(each.value, "boot_disk_size", 64)     }     scheduling_policy {       preemptible = lookup(each.value, "preemptible", false)     }   }   scale_policy {     dynamic "fixed_scale" {       for_each = flatten([lookup(each.value, "fixed_scale", can(each.value["auto_scale"]) ? [] : [{ size = 1 }])])        content {         size = fixed_scale.value.size       }     }     dynamic "auto_scale" {       for_each = flatten([lookup(each.value, "auto_scale", [])])       content {         min     = auto_scale.value.min         max     = auto_scale.value.max         initial = auto_scale.value.initial       }     }   }   allocation_policy {     location {       zone = each.value["zone"]     }   } }

  • Конечно же, меняем имя ресурса в Terraform’е.

  • Объявляем, что хотим использовать for_each и указываем откуда циклу брать значения (а лежат они в переменной node_groups).

  • name, берет значение из each.key, это у нас имя объекта: node‑group‑a, node‑group‑b и node‑group‑c.

  • Далее присваиваем значения остальным параметрам, но делаем мы это по условию, для этого используем функция lookup, синтаксис у неё очень прост, мы указываем где искать, что искать, и что присвоить если ничего не нашла (значение по умолчанию).

  • subnet_ids ставим источник local.worker_subnet_list, указываем что брать, а забирать мы будем id той зоны, которая указана в переменной node_groups, поэтому ставим each.value[«zone»]. Дефолтное значение убираем, т.к. сюда мы по умолчанию ничего не сможем поставить.

  • security_group_ids — группы безопасности мы указываем без всяких условий, просто указываем нужные output»ы.

  • scale_policy — как я уже говорил, нужна возможность выбора, либо фиксированная группа узлов, либо с автоскейлингом — для этого добавляем два соответствующих dynamic блока, в них описываем цикл, но, просто указать источник значений не получится, у нас в переменной вложенный список для этих блоков, чтобы вытащить необходимые значения, нужно этот список вытащить из другого, для этого воспользуемся функцией flatten, которая выравнивает вложенные списки. А далее можем использовать lookup — отличие лишь в том, что для блока fixed_scale на месте дефолтного значения мы ставим условие, которое проверяет наличие параметра auto_scale и если он есть, оставляет этот блок fixed_scale без значения, а если нет, то выставляет один фиксированный узел.

  • В content для этих блоков указываем использовать значения из соответствующего источника: для auto_scale — auto_scale.value.что_берем, для fixed_scale — fixed_scale.value.size

  • allocation policy: без всяких lookup»ов ссылаемся на нужное значение.

Получившаяся конфигурация должна поднимать для нас региональный кластер Kubernetes, с автоскейлингом нод в зоне ru‑central1-a, а в остальных зонах с 1 фиксированный нодой.

Однако, в процессе подготовки тестового стенда я столкнулся с проблемой, что инфраструктура поднималась и работала с этим кодом, а уже на следующий день — нет. Все необходимые узлы создавались, но не имели связи с мастерами. После проверки, я понял, что правила для группы безопасности не отрабатывают так, как это задумано. Пришлось переписать группу internal:

my_project/network/security-group.tf
resource "yandex_vpc_security_group" "internal" {   name        = "internal"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "yc_internal"   }   ingress {     protocol          = "ANY"     description       = "self"     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   }   ingress {     protocol       = "ANY"     description    = "Правило разрешает взаимодействие под-под и сервис-сервис. Укажите подсети вашего кластера и сервисов. P.s. Правило избыточно и добавлено только потому, что политика self_security_group не функционирует как положено между машинами в разных регионах."     v4_cidr_blocks = flatten([for v in concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) : v.cidr])     from_port      = 0     to_port        = 65535   }    egress {     protocol          = "ANY"     description       = "self"     predefined_target = "self_security_group"     from_port         = 0     to_port           = 65535   } }  resource "yandex_vpc_security_group" "k8s_master" {   name        = "k8s-master"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "k8s-master"   }   ingress {     protocol       = "TCP"     description    = "access to api k8s"     v4_cidr_blocks = var.white_ips_for_master     port           = 443   }   ingress {     protocol          = "TCP"     description       = "access to api k8s from Yandex lb"     predefined_target = "loadbalancer_healthchecks"     from_port         = 0     to_port           = 65535   } }  resource "yandex_vpc_security_group" "k8s_worker" {   name        = "k8s-worker"   description = "Managed by terraform"   network_id  = yandex_vpc_network.mynet.id   labels = {     firewall = "k8s-worker"   }   ingress {     protocol       = "ANY"     description    = "any connections"     v4_cidr_blocks = ["0.0.0.0/0"]     from_port      = 0     to_port        = 65535   }   egress {     protocol       = "ANY"     description    = "any connections"     v4_cidr_blocks = ["0.0.0.0/0"]     from_port      = 0     to_port        = 65535   } } 

Поясню свою мысль. Правило, где мы выставляем predefined_target = «self_security_group», должно разрешать всё в рамках этой самой группы, т. е. хосты, к которым привязана группа internal (а в нашем случае это все мастеры и воркеры), должны свободно общаться друг с другом. Но этого не происходит — по информации от техподдержки, они перерабатывают механизм работы этого правила, и пока оно может «работать неожидаемым образом, когда эндпоинты находятся в разных зонах». Поэтому, мне пришлось добавить другое, которое открывает доступ всем узлам, находящимся в перечисленных в ней подсетях.

Как работает правило: 

  1. Нам нужны cidr наших подсетей, они у нас в переменной subnets. Однако, нам не нужно собирать этот параметр со всех подсетей, мы же можем прописать создание и других, не только для кластера и воркеров, как сейчас, — у нас могут появиться другие подсети, и мы не можем позволить трафику с других хостов, которые не относятся к кластеру, ходить к нему. Поэтому отфильтруем только то, что относится к кластеру, — это мастеры и воркеры:

[(var.subnets["k8s_masters"], var.subnets["k8s_workers"])] 
  1. Но, таким образом, мы получили два списка. Не проблема, вызовем функцию concat, которая объединяет несколько списков в один: 

concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) 
  1. Теперь, нужно вытащить только cidr, а у нас в переменной помимо этого ключа ещё name и zone, поэтому напишем условие, что в список мы собираем только значения ключа cidr:

[for v in concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) : v.cidr]
  1. Мы получили нужные параметры, но, cidr у нас представлен в переменной, как вложенный список, с помощью flatten мы выравниваем этот список:

flatten([for v in concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) : v.cidr])

Вот теперь, можем запускаем terraform apply и любуемся на нашу инфраструктуру. ?

Если есть необходимость в настройке дополнительных параметров, как доступ по ssh к воркер‑нодам, вы это сможете сделать уже самостоятельно, почитав документацию.

Заключение

Можно было ещё много параметров описать, но цель статьи в другом — показать разные методы по написанию кода инфраструктуры и научить способам работы с компонентами Terraform»а. Надеюсь вам это помогло.

Самые внимательные, наверное, заметили, что я говорил о том, что не нужно дублировать ресурсы, если их можно описать одним блоком, но при этом, сам создал подряд три группы безопасности. Так вот, предлагаю вам написать в комментариях, как бы вы описали эти ресурсы.

Спасибо, что дочитали до конца. Постараемся выложить следующую часть, как можно скорее. Благодарю всех за внимание, до скорого!)


ссылка на оригинал статьи https://habr.com/ru/articles/721404/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *