Опыт переезда в облако VKCS (IaC, IaC и в продакшн)

в 4:12, , рубрики: Ansible, devops, gitlab, infrastructure as code, openstack, terraform, vkcs, Облачные вычисления, Серверное администрирование
© Viktoria Popova
© Viktoria Popova

Дисклаймер: Я написал такую статью, какую сам хотел бы прочитать полгода-год назад, когда мы стартовали миграцию в облако. Мне бы она здорово помогла сэкономить силы, время и нервы – надеюсь, теперь поможет кому-то ещё. Здесь нет исчерпывающей экспертизы, только немного моего опыта для конкретных условий и наработанные мною решения и инструменты.

Критика и дополнения приветствуются.

Краткие вводные

К концу 2021 года наша серверная инфраструктура состояла из почти сотни виртуалок на паре десятков физических серверов, арендованных в одном из московских ЦОДов и была настоящей болью для эксплуатации. По причинам, столь банальным, что их не стоит обсуждать, мы погрязли в проблемах:

  1. Сервера-снежинки в наихудшей их форме (хуже, чем вы представили)

  2. Разносортица гипервизоров, платформ, конфигураций, версий ОС и ПО

  3. Полное отсутствие решений по отказоустойчивости чего бы то ни было

  4. Мучительные масштабирование, обслуживание и контроль

После нескольких чувствительных аппаратных сбоев было принято решение о масштабной модернизации. Перво-наперво нужно было выбрать стратегическое направление: строим мы свою инфраструктуру on-prem с СХД, VmWare и MLAG-ом, или, по-модному подвернув штаны, катим камень в гору IaaS.

Почему облако

Мы, конечно, слышали о том, что on-prem всегда дешевле, но настоящие инженеры не руководствуются слухами, предрассудками и шестым чувством. Поэтому, собрав в таблицу суммарное наше потребление vCPU, vRAM, дискового пространства и IOPs, пожелания по аренде лицензий и объёму системы резервного копирования, мы отправились получать цифры. Работа с дюжиной поставщиков, интеграторов и облачных провайдеров не была ни простой, ни быстрой, но всё же, спустя месяц-другой мы получили пухлую сводную таблицу: количество и модели серверов, СХД, коммутаторов, лицензии, СРК и прочее с заветной строчкой ИТОГО в самом низу.

Вопреки бытующему мнению, на наши не самые маленькие объёмы ресурсов в сумме 3 лет аренда IaaS у нескольких облачных провайдеров оказалась немного дешевле, чем построение on-prem инфраструктуры. В разрезе 5 лет - немного дороже, но при том, что с облаком уменьшаются накладные трудозатраты на обслуживание, а масштабироваться легче, чем сходить за кофе, выбор в пользу IaaS был несложным.

Теперь необходимо было выбрать между облачными провайдерами из шорт-листа. Мы составили список из нескольких десятков параметров для оценки: от SLA и синтетических тестов IOPs в разных размерах дисков до удобства работы с техподдержкой. Тесты, тесты, тесты, потом митинги с архитекторами и менеджерами облачных провайдеров и снова тесты - субъективно самая долгая и утомительная часть проекта на тот момент.

По совокупности критериев мы остановили выбор на VKCS (тогда ещё Mail.ru Cloud Solutions).

Почему Infrastructure As Code

Облако само по себе решало проблемы #2-4, но lift-and-shift миграция не решила бы проблему #1 с хаосом серверов-снежинок. Мы хорошо представляли, что только одновременная переработка сервисов в концепции Infrastructure As Code даст прозрачность, повторяемость, стандартизацию и прочие преимущества. Даже для тех сервисов, полную конфигурацию которых сложно (привет, платформа 1С, как там твой 2007й год?) или невозможно автоматизировать, мы решили применять IaC для создания всех облачных объектов и базовой конфигурации ОС.

Но понимать, что паровоз сильнее лошади – это одно, а управлять паровозом и, тем более, его создать – совсем другое. Десятки просмотренных видео с конференций могут дать понимание того, какие проблемы решает IaC и какие есть инструменты на рынке, а документация к инструментам – как ими пользоваться, но это не равно экспертизе.

По мере подготовки проекта миграции перед нами встали следующие вопросы, большую часть которых мы решали, не имея ещё опыта, полного видения желаемого результата и в условиях жесткого дедлайна:

А теперь ответы, которые мы для себя нашли, грабли, по которым мы прошли и инструменты, которые мы для себя создали.

Тулинг

В качестве SCM мы выбрали Ansible – потому что минимальные знания по Ansible в команде уже были, а Chef или Puppet пришлось бы учить вообще всем. Кроме того, Ansible лидирует на рынке, что должно коррелировать с размером сообщества и скоростью доработки.

В качестве системы управления инфраструктурой выбрали Terraform – потому что Cloudformation слабо применим к Openstack-облаку, а Pulumi требует хорошего знания общего языка программирования. Возможно, стоило рассмотреть Terragrunt, потому что управление окружениями с ванильным Terraform потребовало отдельной проработки и сложных шаблонов и пайплайнов.

Для CI/CD-платформы решили использовать имеющийся self-hosted инстанс Gitlab, так как именно его мы в компании используем для всех прочих проектов и, когда придёт время и инфраструктурный код сольётся с продуктовым, не нужны будут дополнительные усилия по миграции.

В качестве IDE выбрали Visual Studio Code – множество дополнений для IaC (Ansible, Terraform, Gitlab), приятный интерфейс, встроенный GUI для гита, чтобы упростить вход для начинающих и отличная интеграция с WSL.

Как поделить ресурсы на проекты

В терминологии VKCS «проектом» называется сущность, аналогичная VPC в AWS – набор облачных ресурсов под единым управлением группы пользователей, отделённый от других подобных наборов. Например, виртуальные машины в проекте могут быть напрямую объединены в одну сеть или подсеть, а между проектами связь возможна только через VPN. Список пользователей, рулсетов файрволла, образов и т.п. также различны в разных проектах. Коротко говоря – это независимое отдельное виртуальное частное облако.

Нужно ли вообще делить ваши ресурсы на проекты?

Плюсы деления:

  • Различные скоупы управления – можно наделить администратора или разработчика полными правами для экспериментов и не бояться, что он случайно уничтожит продакшн

  • Можно разворачивать полные копии объектов в тестовых окружениях, не опасаясь что-то сломать на проде

Минусы деления:

  • Нужно обеспечивать сетевую связность между проектами своими силами

  • В Terraform-коде нельзя ссылаться напрямую на объекты из другого проекта, например, на адрес инстанса для правила группы файрволла, только через remote state

  • Дополнительный административный оверхед на управление квотами и доступами

Для себя мы решили вопрос, выделив 3 отдельных типа проектов:

Типы проектов
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 2

Playground – песочница для быстрых экспериментов, разработки и отладки инфраструктурного кода. Когда знания команды по Ansible и Terraform изначально недалеки от нуля, а предстоит переработать в IaC несколько десятков сервисов в ограниченный срок, очень важно дать людям возможность быстро и удобно начать учиться и разрабатывать. Любой желающий имеет права на создание и удаление любых объектов в песочнице, как вручную, так и посредством исполняемого локально или через CI/CD-пайплан кода Terraform.

Production – боевой проект с серверами и сервисами, обслуживающими живых пользователей – внешних и внутренних.

DevQA – проект со статически и динамически создаваемыми окружениями для тестировщиков и разработчиков.

В каждом проекте, конечно, есть core-объекты: сети, подсети, гейтвеи, базовые группы файрволла, роутеры для vpn-связности и т.п., доступ к которым должен быть ограничен.

Что, во имя всех CCIE, делать с сетью

Мы понимали, что сеть – основа основ, а ошибки при проектировании могут выйти дорого. Конечно, cloud-native подход предлагает меньше думать о сегментировании, vlan-ах, маршрутизации и прочих старомодных глупостях, а больше сосредоточиться на продукте, максимально используя преимущества облачные PaaS. Но это всё здорово только если вся компания работает на собственный софтверный продукт и продаёт только его. В наших условиях, кроме продуктовых серверов, нам нужно обеспечивать эксплуатацию множества внутренних сервисов сторонних вендоров, без которых компания не сможет продавать свои услуги.

Подсети

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

В случае с облачной сетью во vlan нет такой необходимости, потому что вы можете виртуально поставить управляемый отдельно файрволл перед каждым сетевым интерфейсом каждой виртуальной машины – в терминологии VKCS это называется Security Group. «Группа безопасности» — это набор правил, определяющий, в каком направлении, по какому протоколу и на какие порты могут быть установлены соединения с помощью этого интерфейса. Этот файрволл не управляется из ОС виртуальной машины, в этом он полностью аналогичен аппаратному файрволлу.

Подробнее об использовании SG будет сказано ниже, но разделение на подсети в целях ИБ потеряло смысл, поэтому мы приняли решение использовать одну сеть в каждом проекте, в каждой из которых по одной подсети – это существенно упрощает эксплуатацию и создание новых инстансов, и не несёт ограничений.

Группы безопасности

Группа безопасности default, назначаемая при ручном создании ВМ по-умолчанию, позволяет всем инстансам беспрепятственно обмениваться трафиком с другими ВМ в группе – некий аналог плоской сети. Это удобно для быстрого старта, когда скорость разработки важнее безопасности, но, конечно, нас такой вариант не устраивал.

С помощью SG вы можете относительно легко реализовать микросегментацию – когда каждая отдельная ВМ или группа ВМ (например, в составе кластера) имеет доступ только к тем адресам, портам и протоколам, к которым должна иметь доступ. Это затрудняет lateral movement для злоумышленника или малвари в случае компрометации одного из серверов и, следовательно, положительно влияет на безопасность.

Для себя мы разработали и приняли следующую схему (принципиальный вариант, только облачная часть; не все механизмы безопасности показаны):

Схема работы с Security Groups
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 3
  • i_default/o_default На виртуальную машину почти всегда назначаются группы по-умолчанию, которые разрешают только самое необходимое – входящие подключения с нод управления, мониторинга и пр., и исходящие во внутреннюю сеть, адреса метаданных Openstack и на репозитории пакетных менеджеров

  • i_int_appname_servicename Для всех сетевых доступов внутри сети создаются отдельные группы с перечисленными адресами или подсетями, которые должны иметь доступ к конкретному сервису

  • o_int_appname_servicename Если список облачных хостов неопределён, например, это кластер, или просто слишком велик для ручного управления, то к группам на входящее подключение создаётся комплиметарная группа на исходящее подключение. В этом случае правило входящего подключения привязывается не к конкретному адресу источника, а к имени группы. То есть группа o_svc_appname_servicename здесь служит как некий тег

  • i_ext_appname_servicename Для доступов извне к сервисам в нашем облачном проекте создаются отдельные группы

В-целом схема получилась простая для понимания и использования, и позволяет строить сервисы с достаточной сетевой изоляцией между ВМ. Минус - заметный оверхед на обработку списков адресов.

Site-to-site VPN

К сожалению, VPNaaS от VKCS пока не умеет ни в ECMP, ни в route-based ipsec, ни в протоколы динамической маршрутизации. А у нас достаточно жёсткие требования по устойчивости и качеству связности с сервисами в ЦОДе/облаке – например, для систем колл-центра. Раньше мы обеспечивали выполнение этих условий, используя Fortigate SDWAN на двух ipsec-туннелях через разных провайдеров с каждой площадки, но в случае с VPNaaS такая схема не могла работать.

Пришлось искать другое решение для виртуального роутера. Хорошее со всех сторон решение Fortigate-VM, к сожалению, на тот момент не прошло по бюджету, поэтому мы использовали VyOS 1.3. Среди плюсов VyOS можно отметить бесплатность (если не покупать поддержку и использовать rolling release), готовый модуль для Ansible и удовлетворительную работу OSPF и route-based ipsec. Собрать образ для KVM можно самостоятельно с помощью любезно предоставленных вендором инструментов, установив в него, например, гостевые утилиты и включив поддержку cloud-init.

В-целом этап с настройкой и тестированием VyOS съел слишком много времени. Мой совет: если у вас нет особых требований к резервированию канала в облако, то лучше используйте VPNaaS, а если есть – купите Fortigate или другое виртуализированное решение с поддержкой.

DNS

Мы используем Active Directory для управления пользовательскими ПК и частью серверов под внутренние сервисы, поэтому DNS-записи должны регистрироваться в зонах, которые обслуживаются контроллерами домена. Мы не нашли решения, которое позволило бы интегрировать платформенные DNS-сервера с нашими, поэтому записи мы создаём через Terraform-провайдер, аутентифицируясь на DNS-серверах через GSS-TSIG. Для каждой ВМ описывать отдельно запись в коде, конечно, неудобно, поэтому мы добавили эту функциональность в модуль Terraform, о котором ниже.

Какую структуру репозиториев выбрать

Казалось бы, пустяковое решение, которое, однако повлияло в дальнейшем и на CI/CD и работу со стейтами, а неверный подход привёл нас в итоге к заметному техническому долгу, который здорово тормозил некоторые наши операции.

Стартовать мы решили в варианте, когда под каждый облачный проект VPC у нас отдельный репозиторий Terraform-кода. А под каждый сервис, конфигурируемый через Ansible, ещё свой отдельный репозиторий.

Как это выглядит
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 4

Плюсы такого подхода:

  • Проще настроить CI/CD один раз вручную и вроде как забыть

  • Можно ссылаться на атрибуты из одного модуля в другом

Минусы:

  • Чем больше сервисов заезжает в репозиторий, тем медленнее становятся операции. State-файл растёт, и для каждого объекта Terraform должен опросить API платформы на предмет изменений. Terraform plan может идти несколько минут.

  • Проблема с одним объектом парализует работу со всеми другими. Например, если при ресайзе поломалось какое-то PaaS-файловое хранилище, другие изменения с несвязанными сервисами вы тоже не сможете применить, потому что Terraform каждый раз будет спотыкаться на одном месте. Всем придётся ждать, когда придёт техподдержка и починит злосчастное хранилище.

  • Невозможно раскатывать один и тот же код в разных окружениях автоматизированно – только ручной копипаст

  • Если для отдельного сервиса потребуется отдельный CI/CD-пайплайн, вы не сможете его сделать, пока код хранится в репозитории

  • Хранимый отдельно код Ansible банально сложнее искать

В итоге сейчас мы пришли к концепции «один сервис – один IaC-репозиторий», с подключаемой из общего репозитория конфигурацией CI/CD и очень жалеем, что не начали так делать с самого начала. Неверные решения - генеральный спонсор нашего технического долга.

Схема нового типа репозиториев
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 5

Подробнее о структуре в разделе про CI/CD

Как готовить Terraform

State-файл

.tfstate – основа основ Terraform, это файл, в котором Terraform сохраняет всю полученную от API информацию о каждом описанном в коде объекте и их зависимости друг от друга. Например, при создании облачного диска вы описываете в коде его название, тип и размер, но с точки зрения платформы облачного провайдера у него есть множество других свойств, генерируемых автоматически – уникальный ID, метаданные и пр., которые нужны будут Terraform, чтобы при следующем обращении к API однозначно определить, какой объект в коде соответствует объекту в облаке и какие параметры изменились.

Хранить стейт нужно надёжно, безопасно и, желательно, иметь возможность вернуться к предыдущей версии, если что-то пошло не так. Кроме того, если вы работаете с кодом не в одиночку с одного ПК, а, например, используете CI/CD для командной разработки, state должен быть доступен для всех ваших коллег, а во время выполнения кем-то кода на state должен ставиться признак блокировки.

Terraform поддерживает множество бэкендов – хранилищ для стейта. Часто для бэкенда выбирают S3, но, к сожалению, на момент старта нашей миграции в облако VKCS ещё не очень хорошо умел в ACL, поэтому мы воспользовались Gitlab-managed Terraform state – встроенным в Gitlab бэкендом типа http с шифрованием at rest, версионированием и блокировками. Этот бэкенд отлично оптимизирован под работу с Gitlab CI/CD.

Стандартизация и модули

Очень скоро после начала совместного написания кода стало понятно, что, во-первых, Terraform-провайдер Openstack - весьма "многословный" с результатом в виде портянок трудночитаемого кода, в котором легче лёгкого ошибиться с именами и значениями десятков объектов. Во-вторых, каждый из коллег писал код так, как казалось правильным именно ему, произвольно именуя и разбивая по файлам десятки объектов. Сопровождать такой код сложно, мы ежедневно ошибались в значениях параметров и именах переменных.

Настоящим спасением стало написание своих модулей.

Это позволило перейти от подобной портянки

resource "openstack_networking_secgroup_v2" "i_example" {
    name        = "i_example"
    description = "Group to access some service"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_1" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 80
    port_range_max    = 80
    remote_ip_prefix  = "10.10.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_2" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 80
    port_range_max    = 80
    remote_ip_prefix  = "10.20.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_3" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 80
    port_range_max    = 80
    remote_ip_prefix  = "10.30.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_4" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 80
    port_range_max    = 80
    remote_ip_prefix  = "10.40.10.1/32"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}

resource "openstack_networking_secgroup_rule_v2" "i_example_5" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 443
    port_range_max    = 443
    remote_ip_prefix  = "10.10.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_6" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 443
    port_range_max    = 443
    remote_ip_prefix  = "10.20.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_7" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 443
    port_range_max    = 443
    remote_ip_prefix  = "10.30.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_8" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "tcp"
    port_range_min    = 443
    port_range_max    = 443
    remote_ip_prefix  = "10.40.10.1/32"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}
resource "openstack_networking_secgroup_rule_v2" "i_example_9" {
    direction         = "ingress"
    ethertype         = "IPv4"
    protocol          = "udp"
    port_range_min    = 9000
    port_range_max    = 11000
    remote_ip_prefix  = "192.168.0.0/24"
    security_group_id = "${openstack_networking_secgroup_v2.i_example.id}"
}

data "openstack_compute_flavor_v2" "standard-2-4-50" {
    name = "Standard-2-4-50"
}
data "openstack_images_image_v2" "win2019en" {
    name = "Windows-Server-2019Std-en.202105"
}
locals {
    winrm_cert = {
        winrm-sf-prod-main = {
            admin_cert0 = substr (filebase64("~/.winrm/winrm.der"),0,255)
            admin_cert1 = substr (filebase64("~/.winrm/winrm.der"),255,255)
            admin_cert2 = substr (filebase64("~/.winrm/winrm.der"),510,255)
            admin_cert3 = substr (filebase64("~/.winrm/winrm.der"),765,255)
        }
    }
}
resource "openstack_compute_keypair_v2" "ansible-key" {
    name       = "ansible-key"
    public_key = file("~/.ssh/id_rsa.pub")
}
resource "openstack_blockstorage_volume_v2" "win-example-c" {
name     = "win-example-c"
size     = 60
volume_type  = "ceph-ssd"
availability_zone = "MS1"
image_id = data.openstack_images_image_v2.win2019en.id
}
resource "openstack_blockstorage_volume_v2" "win-example-bases" {
name     = "win-example-bases"
size     = 120
volume_type  = "ceph-ssd"
availability_zone = "MS1"
}
resource "openstack_compute_instance_v2" "win-example" {
    availability_zone = "MS1"
    name            = "win-example"
    flavor_id       = data.openstack_compute_flavor_v2.standard-2-4-50.id
    security_groups = ["i_default", "o_default", openstack_networking_secgroup_v2.i_example.name]
    key_pair = "ansible-key"
    network {
        name = "network-1"
        fixed_ip_v4 = "10.0.0.10"
    }
    block_device {
        uuid                  = "${openstack_blockstorage_volume_v2.win-example-c.id}"
        source_type           = "volume"
        boot_index            = 0
        destination_type      = "volume"
        delete_on_termination = false
    }
    metadata = merge(
        local.winrm_cert.winrm-sf-prod-main,
            {
                os = "windows"
                os_ver = "2019"
                app = "example"
            }
        )
}
resource "openstack_compute_volume_attach_v2" "bases" {
    instance_id = "${openstack_compute_instance_v2.win-example.id}"
    volume_id   = "${openstack_blockstorage_volume_v2.win-example-bases.id}"
}
resource "dns_a_record_set" "dns" {
    zone = "example.com."
    name = openstack_compute_instance_v2.win-example.name
    addresses = [
        openstack_compute_instance_v2.win-example.network[0].fixed_ip_v4
    ]
    ttl = 300
}
К такому лаконичному и удобному коду
module "i_example" {
    source      = "git::https://github.com/realscorp/tf-openstack-vkcs-secgroup.git?ref=v1.0.0"
    name        = "i_example"
    description = "Group to access some service"
    rules = [{
            direction               = "ingress"
            protocol                = "tcp"
            ports                   = ["80", "443"]
            remote_ips = {
                "Office 1"          = "10.10.0.0/24"
                "Office 2"          = "10.20.0.0/24"
                "Office 3"          = "10.30.0.0/24"
                "Server"            = "10.40.10.1"
                }
            },
            {
            direction               = "ingress"
            protocol                = "udp"
            ports                   = ["9000-11000"]
            remote_ips = {
                "Remote access VPN" = "192.168.0.0/24"
                }
            }]
}

module "win-example" {
    source          = "git::https://github.com/realscorp/tf-openstack-vkcs-vm.git?ref=v1.0.0"
    name            = "win-example"
    flavor          = "standard-2-4-50"
    image           = "Windows-Server-2019Std-en.202105"
    ssh_key_name    = "ansible-key"
    winrm_cert_path = "~/.winrm/winrm.der"
    metadata        = {
            os              = "windows"
            os_ver          = "2019"
            app             = "EXAMPLE"
        }
    ports = [
        {
            network         = "network-1"
            subnet          = "subnet-1"
            ip_address      = ""
            dns_record      = true
            dns_zone        = "example.com."
            security_groups = ["i_default", "o_default"]
            security_groups_ids = [module.i_example.id]
        }
    ]
    volumes = {
        root = {
            type            = "ceph-ssd"
            size            = 60
        }
        bases = {
            type            = "ceph-ssd"
            size            = 120
        }
    }
}

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

Модули можно взять на Гитхабе: инстансы и группы безопасности.

Как готовить Ansible

Динамический инвентори

Одна из основ Ansible – это инвентори: список хостов, конфигурацией которых требуется управлять, а также информация о том, как они сгруппированы и, иногда, список переменных для них. Конечно, если мы хотим максимально автоматизировать свои операции с облачными ВМ и всегда владеть актуальной информацией, статический инвентори нам не подходит.

Среди коллекций, устанавливаемых вместе с Ansible, уже есть плагин динамического инвентори Openstack, и остаётся только решить, как его правильно использовать. Что нам было нужно от инвентори:

  • Получать список хостов из облака каждый раз при запуске плейбука

  • Получать список групп также из облака

  • Задавать группы хостов при создании ВМ через код Терраформ

  • Стандартизировать работу с группами

Очевидный способ задавать группы – использование тегов, но на данный момент VKCS ещё не поддерживает назначение тегов на инстансы через Terraform (точнее, нельзя их задать в момент создания). Исходя из этого ограничения и наших пожеланий мы приняли решения использовать атрибут Metadata для хранения key/value-тегов и написали такую конфигурацию для динамического инвентори:

inventory/openstack.yml
# Настройки динамического инвентори Openstack
# Сами значения переменных для аутентификации задаются в переменных окружения с префиксом OS_
############

# Включаем плагин
plugin: "openstack.cloud.openstack"
# Отключаем встроенный алгоритим разбития на группы
legacy_groups: false
# По умолчанию доступ к хостам по внутренним ip
private: true

# Создаём кастомные группы по различным атрибутам с нужным префиксом
keyed_groups:
  - prefix: mtag
    key: openstack.metadata
    parent_group: all
    separator: "_"

# Генерируем некоторые переменные для каждого хоста
compose:
  # Если на машине установлен метатег ssh_external = yes, настраиваем подключение ansible по внешнему адресу
  ansible_host: openstack.public_v4 if 'yes' == openstack.metadata.ssh_external else openstack.private_v4
  ansible_ssh_host: openstack.public_v4 if 'yes' == openstack.metadata.ssh_external else openstack.private_v4

# Полезно для отладки
fail_on_errors: true

Теперь на создаваемые инстансы добавляем в metadata записи по стандарту:

os = linux
os_ver = ubuntu20
app = nginx
service = site1

И эти записи автоматически разворачиваются в группы mtag_os_linux, mtag_os_ver_ubuntu20, mtag_app_nginx и mtag_service_site1. Для групп, описывающих тип ОС и дистрибутив, через переменные в group_vars можно описать особенности подключения, а для групп приложения и сервиса – создавать таски в плейбуках.

WinRM

Большой загадкой для нас была работа Ansible с Windows, ведь если для Linux есть ssh с привычной, простой и безопасной аутентификацией по ключу, то как поступить с Windows?

Протокол WinRM, который на данный момент в Ansible является предпочтительным для удалённого конфигуририрования Windows (ssh поддерживается, но для продакшна не рекомендован), позволяет использовать несколько схем аутентификации:

  • Basic – прост в использовании, но не безопасен к перехвату злоумышленником. При использовании с Ansible ещё и необходимо устанавливать на все ВМ с Windows одинаковый логин-пароль при создании

  • NTLM – устаревший, недостаточно криптографически стойкий протокол, плюс та же проблема с общим паролем, что и у Basic. При всех недостатках это один самых популярных способов

  • CredSSP – аутентификация по логину и паролю для локальной или доменной учётной записи. Секреты шифруются и протокол считается более безопасным, чем NTLM, однако остальные недостатки остаются, и добавляется необходимость предварительной настройки хоста

  • Kerberos – при правильной настройке и использовании безопасный способ, но требует ввода всех виртуальных машин в домен, что в наших условиях не подходит

  • Certificate – аутентификация по паре открытая/закрытая часть сертификата. Очень безопасный способ, но и самый сложный в использовании: требуется включить WinRM, сгенерировать сертификат, установить его в систему и настроить аутентификацию с нужным отпечатком и, внимание, с указанием пароля пользователя, для которого настраивается аутентификация

Поначалу мы откровенно приуныли - выбор был, как казалось, между слишком компромиссными и слишком сложными методами. Несколько дней прошли в экспериментах и написании собственных Powershell-скриптов по настройке Certificate-based аутентификации. К счастью и большому нашему облегчению, после длительного изучения откровенно плохой документации, выяснилось, что самую грязную работу за нас уже заботливо выполнили авторы Cloubase-init – Windows-аналога cloud-init из мира Linux.

Во всех образах Windows, любезно предоставляемых VKCS, уже предустановлен Cloudbase-init, и, если вы в создаваемый инстанс добавите открытую часть сертификата через userdata или, порезав его на куски, в metadata, то при первой загрузке инстанса cloudbase самостоятельно установит сертификат и настроит WinRM. Пароль при этом будет сгенерирован автоматически и будет уникальным для каждого инстанса. Останется только добавить настройки подключения по сертификату в group_vars и можно приступать к написанию плейбуков. Мы можем использовать все преимущества аутентификации по сертификату с минимумом усилий и избежав практически всех недостатков этого способа!

Однако нужно помнить об одной особенности: если вы измените пароль пользователя, к которому привязан сертификат, то аутентификация по сертификату после перезагрузки перестанет работать. Нужно будет либо перепривязывать сертификат через Powershell, либо пересоздавать инстанс.

Полезный совет

Версионируйте! Всё и всегда! Фиксируйте версии пакетов, которые вы устанавливаете в плейбуках. Фиксируйте версии сторонних ролей, которые вы используете. Создавайте теги с версиями для каждой роли, которую вы написали внутри компании (например, с семантической схемой) и указывайте версию в requirements.yml

Без привязки к версиям вы не сможете полагаться на ваш код, а развитие компонентов будет сущим мучением.

То же самое относится и к Terraform – обязательно создавайте теги с версией для ваших модулей и вызывайте конкретную версию. 

CI? CD, я сам открою

Все мы, конечно, понимаем, что CI/CD – это не только про пайплайны и автоматизацию (можно почитать, например, у Фаулера), но в контексте статьи я хочу рассказать именно про них.

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

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

Но, конечно, базовый пайплайн не мог покрыть наши потребности:

  • Максимально простой и быстрый старт в разработке

  • Работа из одного пайплайна со всем инфраструктурным кодом, Terraform, Ansible в будущем Packer и пр.

  • Вспомогательные инструменты вроде списка флейворов/образов в проекте

  • Полное самообслуживание – от создания проекта до настройки CI/CD-пайплана, доступное без изучения Gitlab

  • Перенос в боевое окружение именно того кода, что проверен локально

  • Разворачивание через CI/CD одного параметризированного кода в разные окружения

  • Модульность для самостоятельного выбора инструментов, версий и окружений

  • Инструменты для простой отладки как локально, так и в боевом окружении

  • Возможность использовать в пайплайне привычные опции Ansible --tags и --limit

  • Хотя бы базовая безопасность для хранения секретов

Пришлось хорошенько попотеть над документацией Gitlab и инструментов, за вайтбордом над схемами и провести множество экспериментов, но в результате получилось найти решение для всех поставленных задач.

Страшная схема пайплайна
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 6
Страшная схема воркфлоу
Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 7

Нестрашное описание словами:

  1. IaC-разработчик создаёт репозиторий в Gitlab из шаблона через Import by URL (ограничение бесплатной версии)

  2. Склонировав репозиторий на свою машину, он запускает скрипт для настройки окружения на работу с проектом-песочницей

  3. Раскомментировав примеры из шаблона, уже можно создать и конфигурировать первые инстансы

  4. Шаблон написан так, чтобы инстансы создавались с локальными парами ssh-ключей и winrm-сертификатов. Если winrm-сертификат ещё не создан, поможет второй скрипт.

  5. Чтобы зайти на созданный инстанс Linux, нужно просто набрать ssh user@host, и, так как он был создан с локальной парой ключей, логин пройдёт успешно.

  6. Чтобы зайти на Windows-инстанс по RDP, можно использовать четвёртый скрипт для получения и дешифровки уникального пароля от платформы Openstack. Для этого нужно, чтобы при создании инстанса был указан ssh-ключ, точно также, как и для Linux.

  7. Примеры Ansible в шаблоне также преднастроены на работу с локальными ключами ssh и WinRM – дополнительные действия не требуются, после создания инстанса можно немедленно запускать его конфигурацию.

  8. Те параметры кода, которые должны отличаться между песочницей и боевым проектом, например, флейворы, выносятся в переменные и указываются либо локально, либо в файлах в каталогах environment

  9. После того, как код готов к деплою в боевое окружение или проверке через CI/CD, разработчик отмечает в .gitlab-ci.yml, какие инструменты и окружения ему нужны и делает push в main или merge request. Пайплайн сам создаст только нужные джобы только в нужных окружениях и заботливо подставит боевые ключи и настроит контейнер на исполнение кода. Изменять код как-то отдельно не нужно.

  10. Для траблшутинга (на отладку это не тянет) исполняемого через пайплайн кода предусмотрен вывод в лог переменных окружения и содержимого файлов с переменным в удобном виде.

  11. Если разработчик хочет использовать Ansible Vault для хранения части переменных, ему поможет ещё один скрипт – нужно только перенести сгенерированный пароль в CI/CD-переменные Gitlab, и не забыть удалить файл с паролем с локального диска, когда работа закончена.

  12. Если разработчику нужно посмотреть список флейворов или образов в проекте, он может использовать вспомогательные джобы пайплайна – просто нажимаешь кнопочку в Gitlab, выполняется запрос через Openstack CLI и выводит информацию.

  13. .gitignore предотвращает случайную загрузку в репозиторий на Gitlab ключей и локальных стейтов.

Посмотреть код пайплайнов, вспомогательных файлов, шаблона и скриптов можно на Гитхабе.

О каких стандартах договориться «на земле»

Конечно, нейминг!

Я немного «повёрнут» на стандартах нейминга – эдакий посттравматический синдром после внутреннего аутсорсинга в большом холдинге, когда можно было часами искать в AD нужную группу или политику среди сотен хаотично и бестолково названных, чтобы выполнить пустяковую задачу. Правильный нейминг позволяет легко ориентироваться среди тысяч объектов, и, зная схему, в голове сгенерировать имя и угадать почти всегда.

Для нейминга инстансов можно использовать несложную схему вроде такой:

Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 8

Примеры:

  • v-sql-qa – Microsoft SQL Server для продакшн-приложения в QA-окружении

  • v-iis-app1-1 – Веб-сервер для бизнес-приложения 1, первый из нескольких

  • v-gitlab – Сервер Гитлаб

Для нейминга SG отлично показала себя такая схема

Опыт переезда в облако VKCS (IaC, IaC и в продакшн) - 9

Примеры Security Groups:

  • i_default – группа по-умолчанию для входящих подключений (мониторинг, управление и т.п.)

  • i_int_web_office – доступ к http/https с офисных площадок

  • i_ext_smtp – входящие подключения к smtp извне

  • o_int_saml – исходящие подключения к сервису saml

Все остальные рекомендации по неймингу есть в документации к Terraform, либо просто очевидны (например, префиксы ans-role- к репам с ролями Ansible), но хотелось бы сделать акцент на именовании переменных в коде Ansible. Из-за того, что значения переменных можно задать в 22 местах и они будут переопределены в соответствии с таблицей приоритетов, недопустимо случайно назвать одинаково переменную роли и переменную плея. Для себя мы определили, что все переменные роли должны иметь префикс из 3-4 букв от сокращённого имени роли, например, wcmn_domain_join – переменная из common-роли для Windows, определяющая, нужно ли заводить инстанс в домен.

Как работать с IaC в команде

Инфраструктурный код – это тоже код. Его можно и нужно версионировать, тестировать и применять другие полезные практики из мира разработки. Одна из таких практик – код-ревью, которую мы широко применили на старте проекта миграции. Всё, что должно было выкатываться в бой из ветки main, не могло попасть туда напрямую, требовалось делать мердж-реквест (MR) на техлида. Это была исключительно полезная практика для команды, потому что стало возможно:

  • На живых и важных задачах обучать коллег

  • Контролировать качество кода и вместе переделывать

  • Выявлять узкие места в процессах и инструментах и тут же их дорабатывать

  • Понимать, кто и с какой проблемой застрял и помогать её решить

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

К счастью, к этому времени костяк команды уже неплохо научился писать инфраструктуру, я дописал все модули и инструменты и настало время попытаться перейти к кросс-ревью внутри команды.

На данный момент мы приняли следующие правила игры:

  1. Разработка нового сервиса ведётся локально через песочницу, пока не будет готово

  2. При выкатке в бой делается МР на кого-то из коллег, тот просматривает код, задаёт вопросы, вносятся правки

  3. После того как сервис уже работает на бою, несрочные правки и фиксы вносятся через МР на коллегу

  4. Срочные фиксы и правки пушатся напрямую в main

У такого подхода есть минусы, например, после первой выкатки в бой могут требоваться доработки, если что-то забыли или проявилась разница в окружениях песочницы и боевого проекта. Каждый раз ждать МР может быть неудобно – ведь задачи обычно все срочные, и такие правки идут без ревью. Но мы понимаем, что для совершенно новой концепции работы это нормально, и процессы будут развиваться.

Заключение

Даже практически завершив миграцию мы, конечно, ещё не достигли всех намеченных целей. Да, на уровне виртуализации всё отказоустойчиво, производительность всегда предсказуема, масштабироваться теперь можно за минуты, а не за недели, а новый сервис развернуть за полчаса. Да, 100% виртуальных машин как минимум базово описаны в коде; у нас есть тесты, версионирование, автор любой правки и найти ошибку или массово изменить конфигурацию стало проще на порядки.

Но есть и технический долг из-за неверно принятых решений, есть и незакрытая ещё потребность в immutable infrastructure, есть и планы по переработке продакшн-сервисов в Cloud Native. Но даже пройденные этапы потребовали значительных затрат времени и сил.

Поэтому надеюсь, что кому-то, кто только задумался о миграции в облако или рефакторингу в IaC, наш опыт и наработки помогут принять правильные решения и провести свои проекты быстрее, эффективнее и без нервного истощения :)

Автор: Сергей Краснов

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js