DevOps Tutorials — Terraform: создаем виртуальный сервер в облаке

от автора

Привет, друзья!

В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.

Другие статьи серии:

Предыдущая статья была посвящена деплою 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-инженера.

Happy devopsing!


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