Найти в Дзене
Linux | Network | DevOps

Terraform + Yandex Cloud: минимальный, но рабочий пример

Предыдущая статья была посвящена деплою Angular+Java веб-приложения
на виртуальном сервере Ubuntu Linux с помощью Ansible. В этой статье мы
научимся настраивать для этого деплоя сеть и создавать виртуальный
сервер в облаке с помощью Terraform. Интересно? Тогда прошу под кат. Итак, наша задача - подготовить облачную инфраструктуру к деплою приложения, а именно: В качестве облака мы будем использовать Yandex Cloud. Обратите внимание, что конфигурация Terraform сильно зависит от конкретного облака, поэтому в другом облаке она будет выглядеть иначе. Предварительные условия: Terraform — это инфраструктура как код (IaC) — инструмент с открытым исходным кодом, разработанный компанией HashiCorp, который позволяет управлять и автоматизировать инфраструктуру в облаках и дата-центрах с помощью конфигурационных файлов. Основные особенности Terraform: Terraform
использует HCL (HashiCorp Configuration Language) — простой и читаемый
язык, похожий на JSON. Примеры конфигурации описывают ресурсы,
Оглавление

Предыдущая статья была посвящена деплою Angular+Java веб-приложения
на виртуальном сервере Ubuntu Linux с помощью Ansible. В этой статье мы
научимся настраивать для этого деплоя сеть и создавать виртуальный
сервер в облаке с помощью Terraform.

Интересно? Тогда прошу под кат.

Итак, наша задача - подготовить облачную инфраструктуру к деплою приложения, а именно:

  • создать виртуальный сервер Ubuntu Linux с пользователем ansible
  • получить внешний IP-адрес сервера для подключения CLI ansible

В качестве облака мы будем использовать Yandex Cloud. Обратите внимание, что конфигурация Terraform сильно зависит от конкретного облака, поэтому в другом облаке она будет выглядеть иначе.

Предварительные условия:

Пара слов о Terraform

Terraform — это инфраструктура как код (IaC) — инструмент с открытым исходным кодом, разработанный компанией HashiCorp, который позволяет управлять и автоматизировать инфраструктуру в облаках и дата-центрах с помощью конфигурационных файлов.

Основные особенности Terraform:

  • Язык конфигурации

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

  • Поддержка множества провайдеров

Terraform может работать с разными облачными провайдерами и сервисами:

  • AWS
  • Google Cloud Platform
  • Microsoft Azure
  • Yandex Cloud
  • Kubernetes
  • и др.
  • Управление жизненным циклом инфраструктуры

Terraform позволяет:

  • создавать инфраструктуру (terraform apply)
  • изменять существующую конфигурацию
  • удалять ненужные ресурсы (terraform destroy)
  • Планирование изменений

Команда terraform plan показывает, какие изменения будут внесены в инфраструктуру, прежде чем они будут применены — это снижает риск ошибок.

  • Идемпотентность

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

Дополнительные материалы:

Конфигурация Terraform

В домашней директории (/home/<user>) создаем файл .terraformrc следующего содержания:

provider_installation {
network_mirror {
url = "https://terraform-mirror.yandexcloud.net/"
include = ["registry.terraform.io/*/*"]
}
direct {
exclude = ["registry.terraform.io/*/*"]
}
}

Создаем директорию terraform со следующей структурой:

- main.tf
- provider.tf
- user-data.txt
- variables.tf
- versions.tf

Определяем провайдера в файлах versions.tf и provider.tf.

versions.tf:

terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
version = ">= 0.87.0"
}
}

# Хранение состояния Terraform (terraform.tfstate) в бакете (bucket) Yandex Cloud
# backend "s3" {
# endpoint = "https://storage.yandexcloud.net"
# bucket = "terraform-state"
# region = "ru-central1"
# key = "terraform.tfstate"

# skip_region_validation = true
# skip_credentials_validation = true
# }
}

provider.tf:

provider "yandex" {
cloud_id = var.cloud_id
folder_id = var.folder_id
zone = var.zone
}

Определяем переменные в файле variables.tf:

variable "cloud_id" {
type = string
# Заменить
default = "b1g..."
}

variable "folder_id" {
type = string
# Заменить
default = "b1g..."
}

variable "zone" {
type = string
default = "ru-central1-a"
}

Определяем данные пользователей в файле user-data.txt (он потребуется нам позднее):

#cloud-config
users:
- name: ansible
shell: /bin/bash
sudo: "ALL=(ALL) NOPASSWD:ALL"
ssh_authorized_keys:
# Заменить
- ssh-rsa AAA...

Обратите внимание:

  • комментарий #cloud-config является обязательным
  • содержание этого файла зависит от версии Terraform

Определяем основную конфигурацию Terraform в файле main.tf:

module "instance" {
source = "./modules/tf-yc-instance"
subnet_id = module.network.subnet_ids[var.zone]
}

module "network" {
source = "./modules/tf-yc-network"
}

output "external_ip" {
value = module.instance.external_ip
}

output "internal_ip" {
value = module.instance.internal_ip
}

output "network_id" {
value = module.network.network_id
}

output "subnet_ids" {
value = module.network.subnet_ids
}

Здесь мы видим подключение 2 модулей: instance для виртуальной машины и network для сети. Обратите внимание, что мы передаем ID подсети, соответствующий зоне ru-central1-a (subnet_id), из модуля network в модуль instance.
Также мы указываем Terraform после завершения работы вывести в терминал
внешний и внутренний IP-адреса сервера, ID сети и подсетей. Из этих
значений нам нужен только внешний IP, остальное выводится ради забавы.

Не забудьте добавить файлы состояния Terraform в файл .gitignore:

*.tfstate
*.tfstate.*

Конфигурация модуля сети

Создаем директорию modules/tf-yc-network со следующей структурой:

- main.tf
- outputs.tf
- README.md
- variables.tf
- versions.tf

Файл versions.tf идентичен одноименному файлу в директории terraform (без блока s3).

Определяем ресурсы сети и подсети в файле main.tf:

data "yandex_vpc_network" "default" {
name = "default"
}

data "yandex_vpc_subnet" "default" {
for_each = var.network_zones
name = "default-${each.value}"
}

Определяем переменную в файле variables.tf:

variable "network_zones" {
description = "Список зон для создания подсетей"
type = set(string)
default = ["ru-central1-a", "ru-central1-b", "ru-central1-d"]
}

Определяем возвращаемые значения в файле outputs.tf:

output "network_id" {
description = "ID созданной сети"
value = data.yandex_vpc_network.default.id
}

output "subnet_ids" {
description = "ID созданных подсетей"
value = { for zone, subnet in data.yandex_vpc_subnet.default : zone => subnet.id }
}

Разберем, что такое yandex_vpc_subnet и как будет выглядеть subnet_ids на выходе.

  • data "yandex_vpc_subnet" "default"

Этот блок получает существующие подсети по имени default-<zone> в разных зонах.
Мы используем for_each по списку зон, значит, получаем несколько подсетей, по одной на каждую зону из переменной network_zones.

Terraform создаст следующие ресурсы:

data.yandex_vpc_subnet.default["ru-central1-a"]
data.yandex_vpc_subnet.default["ru-central1-b"]
data.yandex_vpc_subnet.default["ru-central1-d"]

Каждый из них будет искать подсеть с именем:

  • default-ru-central1-a
  • default-ru-central1-b
  • default-ru-central1-d
  • output "subnet_ids"

Этот блок создает словарь (map), где:

  • ключ — это имя зоны (ru-central1-a и т.д.)
  • значение — это subnet.id (идентификатор найденной подсети)

Примерный вывод:

subnet_ids = {
"ru-central1-a" = "e2lu123abcde12345678"
"ru-central1-b" = "e2lu456fghij45678901"
"ru-central1-d" = "e2lu789klmno78901234"
}

Этот вывод используется для передачи идентификатора подсети ru-central1-a в модуль instance.

Обратите внимание на следующее:

  • все указанные подсети должны уже существовать в Yandex Cloud с именами default-ru-central1-a и т.д. Если хотя бы одной не существует — Terraform выдаст ошибку
  • мы работаем с data, а не с resource, то есть ничего не создается, только читается

Конфигурация модуля сервера

Создаем директорию modules/tf-yc-instance со следующей структурой:

- main.tf
- outputs.tf
- README.md
- variables.tf
- versions.tf

Файл versions.tf идентичен одноименному файлу в директории terraform (без блока s3).

Определяем создание виртуального сервера vm-1 в файле main.tf:

resource "yandex_compute_instance" "vm-1" {
name = var.resource_name
platform_id = var.platform_id

resources {
cores = var.cpu_cores
memory = var.memory_size
}

boot_disk {
initialize_params {
image_id = var.image_id
size = var.disk_size
}
}

network_interface {
# Получаем из модуля `network`
subnet_id = var.subnet_id
nat = var.enable_nat
}

scheduling_policy {
preemptible = var.preemptible
}

metadata = {
# Данные пользователей
user-data = "${file(var.user_data_file)}"
# Альтернатива - ssh-keys = "ansible:${file("~/.ssh/id_rsa.pub")}"
}
}

Определяем переменные в файле variables.tf:

variables.tf

variable "resource_name" {
description = "Имя виртуальной машины"
type = string
default = "my-vm"
}

variable "platform_id" {
description = "Платформа виртуальной машины"
type = string
default = "standard-v3"
}

variable "cpu_cores" {
description = "Количество ядер процессора"
type = number
default = 2
}

variable "memory_size" {
description = "Объем оперативной памяти (в ГБ)"
type = number
default = 2
}

variable "image_id" {
description = "ID образа диска"
type = string
default = "fd80qm01ah03dkqb14lc"
}

variable "disk_size" {
description = "Размер диска (в ГБ)"
type = number
default = 50
}

variable "enable_nat" {
description = "Включить NAT для ВМ"
type = bool
default = true
}

variable "preemptible" {
description = "Использовать прерываемую ВМ"
type = bool
default = true
}

variable "user_data_file" {
description = "Путь к файлу с данными пользователей"
type = string
default = "./user-data.txt"
}

variable "subnet_id" {
type = string
}

Обратите внимание на следующее:

  • image_id - образ диска Ubuntu 20.04
  • user_data_file - путь к файлу с данными пользователей (относительно директории terraform)
  • subnet_id - идентификатор подсети, который передается из модуля network

Определяем возвращаемые значения в outputs.tf:

output "external_ip" {
description = "Внешний IP-адрес виртуальной машины"
value = yandex_compute_instance.vm-1.network_interface.0.nat_ip_address
}

output "internal_ip" {
description = "Внутренний IP-адрес виртуальной машины"
value = yandex_compute_instance.vm-1.network_interface.0.ip_address
}

Обратите внимание, что названия outputs в модулях и terraform/main.tf должны совпадать.

Итого

Финальная структура проекта:

terraform
| main.tf
| provider.tf
| user-data.txt
| variables.tf
| versions.tf
| modules
| tf-yc-instance
| main.tf
| outputs.tf
| README.md
| variables.tf
| versions.tf
| tf-yc-network
| main.tf
| outputs.tf
| README.md
| variables.tf
| versions.tf

Команды для запуска Terraform:

# Выполняется в корневой директории (`terraform`).
# Установка провайдера
terraform init
# Опционально, валидация конфигурации
terraform validate
# Генерация плана создания и настройки инфрастуктуры
terraform plan
# Применение конфигурации
terraform apply

Мы рассмотрели далеко не все возможности, предоставляемые Terraform, но
думаю вы получили неплохое представление о том, что и как позволяет
делать этот замечательный инструмент. Наряду с другими популярными
решениями для автоматизации ИТ-процессов (Ansible, Docker, Kubernetes и
т.д.), Terraform на сегодняшний день является важной частью арсенала
DevOps-инженера.