Всем привет! Меня зовут Виктор, я 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 использует несколько основных элементов для описания инфраструктуры:
Providers: провайдеры — это интерфейсы для взаимодействия с ресурсами, такими, как AWS, Google Cloud, Microsoft Azure и т. д.
Resources: ресурсы — это объекты, которые вы хотите создать, удалить или изменить. Например, виртуальная машина, балансировщик нагрузки, база данных.
Variables: переменные — это значения, которые можно передать в Terraform для конфигурирования вашей инфраструктуры.
Data Sources: источники данных — это инструменты, которые позволяют Terraform получать данные из внешних источников, таких как API, базы данных или другие службы.
Outputs: выводы — это результаты, которые Terraform выдает после выполнения конфигурации, например, IP‑адреса, идентификаторы ресурсов и т. д.
Дополнительные элементы Terraform, которые стоит упомянуть:
Модули. Позволяют повторно использовать код и упрощают управление сложными инфраструктурами.
Варианты окружения. Terraform позволяет управлять различными версиями инфраструктуры в зависимости от окружения.
Выражения. Terraform использует выражения для вычисления значений и проверки условий.
Итак, мы понимаем, что с помощью Terraform можно описать инфраструктуру в коде, этот код содержится в файлах, что логично????, так давайте поговорим с вами о них.
Основные типы файлов, которые вы можете использовать в Terraform, включают в себя:
Файлы конфигурации (.tf файлы) — это основные файлы Terraform, которые содержат описание ресурсов, которые вы хотите создать или управлять.
Файлы переменных (.tfvars файлы) — это файлы, которые содержат значения переменных, использующихся в файлах конфигурации.
Файл состояния (.tfstate файл): это файл, который хранит текущее состояние вашей инфраструктуры. Он обновляется каждый раз, когда вы запускаете apply (конечно, если вы что‑то изменили в коде).
Также, стоит отдельно рассказать про файл состояния ‑.tfstate. Terraform сохраняет информацию о текущем состоянии вашей инфраструктуры в файле состояния. Этот файл содержит информацию о ресурсах, которые уже были созданы в вашей инфраструктуре, и их текущих свойствах.
Terraform использует этот файл, чтобы определить, какие ресурсы необходимо изменить, какие создать, а какие необходимо удалить, когда вы запускаете команду apply. Грубо говоря, он помогает Terraform определять, какие изменения необходимо внести в инфраструктуру, чтобы ее состояние соответствовало описанному в конфигурационных файлах.
Файл состояния может храниться локально или удаленно, например, в объектном хранилище. Это позволяет вам хранить общую копию файла состояния и управлять им с нескольких хостов.
Важно отметить, что файл состояния является важным аспектом управления вашей инфраструктурой, поэтому необходимо обеспечить его блокировку на время работы с ним.
Также, нужно понимать, что файл состояния является конфиденциальным, и его необходимо хранить в безопасном месте, так как он содержит важную информацию о всей вашей инфраструктуре.
Кроме всего этого, хочу упомянуть, что если вам пришлось вручную залезть в tfstate‑файл, то, как говорит мой коллега: «Вы где‑то что‑то сделали не так в этой жизни» ????
Теперь давайте поговорим с вами о том, на какие best practices стоит обратить внимание:
Не храните всю инфраструктуру в одном месте, разбейте её на части;
Изолируйте несвязанные компоненты друг от друга. Разделяйте логически свою инфраструктуру, сеть, сервисы, мониторинг и т. д.;
Не храните файл состояния локально;
Блокируйте файл состояния при работе;
Пишите код так, чтобы потом его можно было использовать в другом месте, используйте модули.
Пользуйтесь переменными — удобнее использовать переменные для управления конфигурацией инфраструктуры.
Пишите документацию, она всегда пригодится.
Чуть подробнее о том, что мы здесь перечислили
Когда вы пишите код своей инфраструктуры, достаточно написать всё в одном файле, к примеру в файле 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 (а в нашем случае это все мастеры и воркеры), должны свободно общаться друг с другом. Но этого не происходит — по информации от техподдержки, они перерабатывают механизм работы этого правила, и пока оно может «работать неожидаемым образом, когда эндпоинты находятся в разных зонах». Поэтому, мне пришлось добавить другое, которое открывает доступ всем узлам, находящимся в перечисленных в ней подсетях.
Как работает правило:
Нам нужны cidr наших подсетей, они у нас в переменной subnets. Однако, нам не нужно собирать этот параметр со всех подсетей, мы же можем прописать создание и других, не только для кластера и воркеров, как сейчас, — у нас могут появиться другие подсети, и мы не можем позволить трафику с других хостов, которые не относятся к кластеру, ходить к нему. Поэтому отфильтруем только то, что относится к кластеру, — это мастеры и воркеры:
[(var.subnets["k8s_masters"], var.subnets["k8s_workers"])]
Но, таким образом, мы получили два списка. Не проблема, вызовем функцию concat, которая объединяет несколько списков в один:
concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"])
Теперь, нужно вытащить только cidr, а у нас в переменной помимо этого ключа ещё name и zone, поэтому напишем условие, что в список мы собираем только значения ключа cidr:
[for v in concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) : v.cidr]
Мы получили нужные параметры, но, cidr у нас представлен в переменной, как вложенный список, с помощью flatten мы выравниваем этот список:
flatten([for v in concat(var.subnets["k8s_masters"], var.subnets["k8s_workers"]) : v.cidr])
Вот теперь, можем запускаем terraform apply и любуемся на нашу инфраструктуру. ????
Если есть необходимость в настройке дополнительных параметров, как доступ по ssh к воркер‑нодам, вы это сможете сделать уже самостоятельно, почитав документацию.
Заключение
Можно было ещё много параметров описать, но цель статьи в другом — показать разные методы по написанию кода инфраструктуры и научить способам работы с компонентами Terraform»а. Надеюсь вам это помогло.
Самые внимательные, наверное, заметили, что я говорил о том, что не нужно дублировать ресурсы, если их можно описать одним блоком, но при этом, сам создал подряд три группы безопасности. Так вот, предлагаю вам написать в комментариях, как бы вы описали эти ресурсы.
Спасибо, что дочитали до конца. Постараемся выложить следующую часть, как можно скорее. Благодарю всех за внимание, до скорого!)
Ru6aKa
При применении Terragrunt часть этих самых best practices просто поменяется или пропадет и появятся новые рекомендации и практики.
Vikontrol Автор
Абсолютно согласен, но это уже совсем другая история)
Ru6aKa
Я бы с этой другой истории и начинал бы, потому что terragrunt решает кучу проблем которые не решить никакими best practices в terraform. Да и мало того с terraform не так просто перейти на terragrunt, там логика на другая и на уровне выше и это все можно было бы описать в статье, хотя бы вскользь с какими-то простыми примерами или ремарками.
Vikontrol Автор
Всё впереди, и до terragrunt доберёмся)