Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров603

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

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

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

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

Теги:
Хабы:
0
Комментарии1

Публикации

Ближайшие события