Гостевой пост: управляем «Виртуальным приватным облаком» с помощью Terraform

Terraform VPC

Сегодня мы публикуем гостевой пост, написанный нашим клиентом Алексеем Ваховым. Алексей — технический директор компании Учи.Ру. Компания занимается разработкой одноимённой образовательной платформы, а также проводит интерактивные олимпиады для школьников. Вся инфраструктура Учи.Ру построена на базе нашего сервиса «Виртуальное приватное облако».
Алексей Вахов подробно рассказывает о том, как он и его коллеги используют утилиту Terraform для автоматизации настройки и поддержки виртуальной инфраструктуры. Надеемся, что его опыт будет интересен и другим пользователям нашего облака.

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

Наш флагманский продукт — это сайт https://uchi.ru . Помимо работы над сайтом мы проводим онлайн-олимпиады по математике, предпринимательству и русскому языку. Продакшены олимпиад, как и любых мероприятий с четкими датами проведения, отличаются сильно неравномерным трафиком. Во время основного тура сайт нагружен сильно, и на нас лежит высокая ответственность и важно, чтобы технически произошло все гладко. А после награждения и подведения итогов продакшен олимпиады работает в спокойном режиме.

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

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

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

Скажем из 8 серверов, которые должны быть одинаковые, один может иметь другую версию операционной системы или чуть-чуть другое количество оперативной памяти. Задача клонирования или поднятия нового продакшена с нуля также подразумевала много ручной работы. Поэтому мы решили автоматизировать работу по настройке и поддержке нашей инфраструктуры с помощью утилиты Terraform..

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

Что такое Terraform

Terraform — это замечательная утилита от компании HashiCorp (создатели Vagrant, Consul и некоторых других широко известных в узких кругах инструментов). С её помощью можно моделировать, хранить и изменять облачную инфраструктуру в виде простых шаблонов на языке HCL (HashiCorp Configuration Language). Хотя создание своего языка — обычно очень плохая идея, HCL мне нравится. Он представляет собой надстройку над JSON, которую легко и приятно читать.

Приведу упрощенный пример (в реальности нужно указать чуть больше атрибутов) для создания сервера в облаке «Селектела», практически полностью объясняющий все тонкости синтаксиса, которые нам пригодятся:

resource "openstack_blockstorage_volume_v1" "disk" {
  name        = "disk"
  region      = "ru-1"
  size        = 10
}


resource "openstack_compute_instance_v2" "server" {
  name        = "server"
  flavor_name = "flavor-1024-1"
  region      = "ru-1"


  block_device {
    uuid   = "${openstack_blockstorage_volume_v1.disk.id}"
  }
}

Если сохранить приведённый выше код в файл с расширением .tf, настроить необходимые токены для доступа и вызвать в консоли команду `terraform apply`, то произойдет ровно то, что ожидается: Terraform создаст диск и только после этого создаст сервер, на основе этого свежего диска.

Если запустить `terraform apply` еще раз, то утилита увидит, что такой диск и сервер уже есть, и ничего делать не будет. Также легко добавить памяти на сервер, поменяв `flavor_name` на `flavor-2048-1` и снова вызвав `terraform apply`. Terraform предсказуемо обновит только память, а диск трогать не будет. Команда `terraform destroy` удалит все созданные ресурсы (и можно забыть про висящие в пустоту DNS-записи тестовых серверов, забытые диски и другие артефакты, которые раздражают любого перфекциониста).

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

Рассмотрим сценарии работы с Terraform более подробно.

Подготовка

Для начала необходимо настроить окружение. Я расскажу, как это делаем мы, но, думаю, если вы уже скриптуете свою инфраструктуру, то наверняка знаете какие моменты требуют автоматизации. Рекомендую изучить полный список поддерживаемых провайдеров, там есть очень неожиданные представители — Grafana, PostgreSQL, Heroku и многие другие. Может быть, что-то пригодится при моделировании вашей инфраструктуры.
Мы пока управляем только серверами и DNS-записями.

Для работы нам понадобятся:

  • пользователь с доступом к данному проекту;
  • консольные утилиты OpenStack;
  • идентификатор образа операционной системы;
  • подготовленный набор переменных окружения.

Новый проект, пользователя и квоты настройте через GUI (хотя это тоже можно скриптовать, но уже не через Terraform). Сохраните ID проекта: он будет использоваться при настройке доступа OpenStack-провайдера. Зайдите в проект и создайте локальную сеть, если она еще не создана (в облаке «Селектеле» локальная сеть создается автоматически, если вы закажете плавающий IP-адрес).

Далее установите консольные утилиты для получения некоторых внутренних идентификаторов, которые недоступны через GUI. В статье «Работа с API виртуального приватного облака: консольные клиенты» подробно написано, как их установить и использовать. Для удобства мы завернули их в контейнер и используем следующим образом:

docker run --rm \
  -e OS_AUTH_URL=https://api.selvpc.ru/identity/v3 \
  -e OS_PROJECT_ID=#{...} \
  -e OS_USER_DOMAIN_NAME=#{...} \
  -e OS_USERNAME=#{...} \
  -e OS_PASSWORD=#{...} \
  -e OS_REGION_NAME=#{...} \
  uchiru/ostack:v2 <команда, которую необходимо запустить>

Где OS_USER_DOMAIN_NAME — логин в панели «Селектела» (номер договора), PROJECT_ID — идентификатор проекта, OS_REGION_NAME — регион (ru-1 для Санкт-Петербурга, ru-2 — Москва), OS_USERNAME/OS_PASSWORD — логин/пароль пользователя (не забывайте, что у него должен быть доступ в проект).

Чтобы получить идентификатор образа, запустите команду glance image-list и найдите нужный вам образ:

root@2118c4e58238:/# glance image-list |grep 16.04
eecd3d0f-6968-40ea-bed6-4c2949bbac3d | Ubuntu-16.04 LTS 32-bit
ce532860-acef-40cd-b3c7-699c22b4dfd6 | Ubuntu-16.04 LTS 64-bit

Теперь осталось создать нужные флаворы. Обычно OpenStack-провайдеры предоставляют фиксированный набор конфигураций, однако «Селектел» в этом плане более гибок, так как позволяет собрать произвольную конфигурацию для каждого сервера (при работе в UI флавор создается автоматически на каждый новый сервер). Мы же будем создавать флаворы с помощью команды nova. Сразу же примите схему именования, которой будете следовать. Разумно выбрать что-то типа flavor-1024-2 —двухядерный сервер с гигабайтом памяти.
Формат команды:

nova --is-public False flavor-create <flavor-name> auto <mem-in-mb> 0 <cpu-n>.

Наконец, для корректной работы Terraform понадобится экспортировать несколько переменных окружения:

export TF_VAR_SELECTEL_ACCOUNT=112233 # ваш логин от аккаунта Селектела
export TF_VAR_PROJ_ID=5b1b496..       # идентификатор проекта
export TF_VAR_USER=...                # логин пользователя
export TF_VAR_PASSWORD=...            # пароль этого пользователя

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

Приступаем к работе

Создайте файл provider.tf (имя файла, естественно, может быть любым) с доступом к облаку:

provider "openstack" {
  domain_name = "${var.SELECTEL_ACCOUNT}"
  auth_url  = "https://api.selvpc.ru/identity/v3"
  tenant_name = "${var.PROJ_ID}"
  tenant_id = "${var.PROJ_ID}"
  user_name  = "${var.USER}"
  password  = "${var.PASSWORD}"
}

Добавьте файл variables.tf:

variable "image_list" {
  type = "map"
  default = {
    "ubuntu-x64-1604" = "2de94623-a2a2-49e3-984d-3e6ca85e2b84"
  }
}


# переменные необходимо объявить
variable "PROJ_ID" {}
variable "SELECTEL_ACCOUNT" {}
variable "USER" {}
variable "PASSWORD" {}


# плавающий IP и идентификатор сети можно скопировать из UI
variable "box01-floating-ip" { default = "..." }
variable "network-id"        { default = "..." }
variable "box01-ip"          { default = "192.168.0.4" }

И, наконец, файл box01.tf с описанием сервера:

resource "openstack_blockstorage_volume_v1" "disk-for-box01" {
  name        = "disk-for-box01"
  region      = "ru-1"
  size        = 10
  image_id    = "#${var.image_list["ubuntu-x64-1604"]}"
  volume_type = "basic.ru-1a"
}


resource "openstack_compute_instance_v2" "box01" {
  name        = "box01"
  flavor_name = "flavor-1024-1"
  region      = "ru-1"


  network {
    uuid        = "${var.network-id}"
    fixed_ip_v4 = "${var.box01-ip}"
    floating_ip = "${var.box01-floating-ip}"
  }


  metadata = {
    "x_sel_server_default_addr"  = "{\"ipv4\":\"\"}"
  }


  block_device {
    uuid   = "${openstack_blockstorage_volume_v1.disk-for-box01.id}"
    source_type      = "volume"
    boot_index       = 0
    destination_type = "volume"
  }
}

Затем выполните команду `terraform apply` — и через несколько секунд ваш первый сервер готов! Мы раздаем IP-адреса вручную для большей контролируемости, плюс при изменении ресурсов OpenStack выдаст новый адрес, если он не указан явно, что не всегда удобно.

Синтаксис Terraform достаточно самодокументированный. Вы из кирпичиков-ресурсов складываете те конструкции, которые вам нужны. Утилита сама просчитывает зависимости и, к примеру, создаст сервер только после того, как создан диск. Однако то, что можно сделать параллельно — будет делать параллельно.

Конечно, один-единственный сервер гораздо быстрее и удобнее создать через GUI. Я думаю, что реальная польза от Terraform начинается от пары десятков серверов, особенно если также скриптовать DNS-записи, бакеты S3 и другие сервисы.

Изменение конфигурации

Добавить или убрать память (или диски) очень просто: измените флавор и вызовите команду `terraform apply`. Обратите внимание: в результате выполнения этой команды сервер перезагрузится!

Размер диска следует менять аккуратно, так как Terraform пересоздаст его с нуля, если делать через скрипты. Но выход есть: увеличьте размер через UI и в соответствующем tf-скрипте. Terraform вас поймет правильно и трогать диск не будет.

Добавить сервер проще всего, скопировав файл box01.tf и обновив соответсвующие переменные. Для наших проектов в 10-20 серверов мы храним tf-файлы плоско (не формируя иерархию из файлов по какому-либо признаку): сколько серверов — столько файлов.

Сценарии использования Terraform

Повторюсь ещё раз: по уровню бед, которые можно натворить на живом продакшене (причем, очень, очень быстро) Terraform оставит далеко позади даже `truncate table`. Пожалуй, это самый опасный инструмент, с которым я когда-либо работал. Поэтому очень важно хорошо понимать принципы его работы.

С технической точки зрения Terraform представляет собой один-единственный бинарный файл на Go, его можно скомпилировать, разобраться, отладить. Вся магия реализована через файл состояния и многообразие провайдеров для работы с разными сервисами.

Типичный сценарий выглядит так. Вы задаете желаемую конфигурацию в tf-скриптах. Файл состояния в этот момент пустой. Набираете команду `terraform plan`.Это самая приятная и безопасная команда: она ничего не меняет, просто пишет, какие изменения утилита внесет, если запустить команду `terraform apply`.
Далее вы запускаете `terraform apply` и только тогда будут созданы реальные серверы, диски, записи в DNS и так далее. Информация об этих ресурсах будет сохранена в файл состояния (JSON-файл с довольно простым синтаксисом). При дальнейшей работе утилита будет использовать файл состояния, также собирать информацию из реального мира и сравнивать ее с вашими скриптами.

Причиной всех конфузов и недопониманий является то, что Terraform работает с тремя объектами одновременно:

  • tf-скрипты — это то, что вы хотите видеть;
  • файл состояния — это то, как выглядит инфраструктура по мнению Terraform;
  • реальное состояние дел — как на самом деле выглядит инфраструктура.

В момент планирования запроса (а apply команда отличается от plan только тем, что изменения реально применяются), Terraform считывает файл состояния, обновляет свойства каждого объекта по его ID (в любом сервисе у каждого ресурса есть уникальный идентификатор, именно его использует Terraform для идентификации) и сравнивает полученные атрибуты.

Грубо говоря, можно считать, что состояние — это перечень идентификаторов ресурсов, которыми управляет данный проект. Если вы добавите сервер или DNS-запись руками, то Terraform про нее не узнает. Необходимо будет поместить ее в состояние явно. Если удалите сервер через GUI, то Terraformу также нужно будет обновить состояние с помощью команды `terraform state rm <удаленные ресурсы>`.

В предыдущем разделе я рекомендовал увеличивать диск через UI и в tf-скрипте. Как это работает? Допустим, изначально в tf-скрипте указан диск 10ГБ. Terraform создал его и записал id свежего диска, а также его размер в файл состояния. Вы поменяли размер через UI на 20ГБ. Если запустить `terraform plan` сейчас, то Terraform узнает у OpenStack, что размер диска 20ГБ, а в tf-файле — 10ГБ, и предложит его пересоздать. Если вы поменяете размер диска на 20ГБ, то Terraform убедится, что диск реально 20ГБ и в скриптах — 20ГБ и ничего делать не будет. Может показаться, что хранить в состоянии факт, что когда-то диск был 10ГБ и не нужно, так как Terraform не использует этот атрибут. На самом деле, это так и есть, но особенность реализации некоторых провайдеров требует сохранение некоторых дополнительных атрибутов (например, регион для всех OpenStack-ресурсов), подозреваю, что именно поэтому авторы решили хранить в состоянии последние значения всех свойств каждого ресурса.

Чтобы Terraform работал корректно, необходимо хранить файл состояния централизованно. Мы храним прямо в репозитории, также можно хранить на S3, в коммерческом сервисе Atlas, в Consul и некоторых других хранилищах, полный список которых также есть в официальной документации.

Заключение

В этой статье мы рассмотрели особенности работы с Terraform очень кратко. К сожалению, за рамками нашего рассмотрения осталась интереснейшая тема по импорту уже существующей инфраструктуры. Скажу кратко. Можно воспользоваться командой `terraform import`, которая, правда, появилась недавно, реализована далеко не для всех провайдеров и пока откровенно сыровата. Рабочий вариант на сегодня — добавить ресурс с правильным типом и идентификатором прямо в файл состояния и вызвать `terraform refresh`.

Итак, если вы активно пользуетесь облачными сервисами, часто создаете и удаляете разные сущности через GUI, рекомендую попробовать. Здесь вы найдете полный комплект скриптов из статьи, достаточный для заведения своего сервера с помощью Terraform. Хотя утилита молодая, с достаточно высоким порогом входа, после внедрения ощущение порядка и контроля сильно возрастет. Буду рад ответить на любые вопросы и замечания в комментариях и по почте vakhov@gmail.com.

Благодарю команду «Селектелa» за предложение написать гостевой пост. Также приглашаю всех читателей посетить мой личный блог. Каждый рабочий день публикую маленькую историю про разработку, мысли, инструменты и все такое.

Спасибо за внимание!