
Привет, Хабр! Я Алексей Волков, менеджер продукта компании VK Cloud Solutions. Хочу рассказать о подходе IaC (Infrastructure as Code, инфраструктура как код), который позволяет управлять сетями, виртуальными машинами, подсистемами балансировки нагрузки и другими элементами инфраструктуры как кодом с помощью описательной модели. Поговорим о современных принципах управления инфраструктурой, инструментах IaC в облаке и вариантах построения CI/CD-пайплайна.
Традиционный процесс администрирования vs IaC
Чтобы лучше понять суть IaC, удобнее всего сравнить его с традиционным подходом к администрированию инфраструктуры.
При традиционном подходе:
- Устанавливают операционную систему (ОС).
 - Добавляют пользователей.
 - Ставят нужные пакеты.
 - Вписывают конфигурации.
 - Запускают приложение.
 
Если нужно расширить функциональность образа или перенастроить его, повторяют пункты 3–5. Такой подход еще называют Configuration Drain. Недостаток в том, что при неоднократном обновлении инстанса практически невозможно понять, какая версия пакетов установлена и какие настройки активны. Это нередко приводит к тому, что последующее обновление может не запуститься или сломать всю инфраструктуру.
При IaC-подходе алгоритм отличается:
- Готовят шаблон сервера со всеми настройками и пакетами.
 - Описывают в тексте инфраструктуру с указанием сетей, серверов, прав доступа и других параметров.
 - Применяют изменения.
 
В итоге получается «золотой образ», готовый к использованию. Как правило, перед запуском в продакшене его проверяют на стейдже — если он корректно работает во время теста, то и в реальных условиях проблем не будет.
«Золотой образ» можно использовать и в качестве шаблона — например, если нужно внести дополнительные изменения поверх уже активных настроек.
IaC-подход к администрированию повышает предсказуемость системы, ее воспроизводимость и контролируемость. Кроме того, он позволяет быстро исправлять возникающие ошибки: в любой момент можно перезапустить инстанс с корректными настройками из чистого образа. Одновременно с этим можно управлять инфраструктурой как кодом: версионировать, использовать возможности Git для контроля изменений, указывать этапы выкатки и другие нюансы.
На практике для реализации IaC-подхода к администрированию используют разные инструменты. Рассмотрим Packer и Terraform от компании HashiCorp.
Packer
Packer — инструмент для создания одинаковых образов ОС для различных платформ из одного описания. Образ, создаваемый Packer, включает в себя настроенную операционную систему (ОС) и набор программного обеспечения (ПО). Packer умеет создавать образы AMI для EC2, VMDK/VMX-файлы для VMware, OVF для VirtualBox и другие.
Packer не зависит от облачной инфраструктуры — он поддерживает несколько бэкендов и может работать с разными провайдерами «из коробки».
Алгоритм работы с Packer следующий:
- Создаем и описываем файл конфигурации базовой виртуальной машины.
 - Запускаем Packer, который создает виртуальную машину.
 - Обращаемся в провижинер, например Ansible, для провижинга виртуальной машины.
 - После провижинга приводим виртуальную машину к нужному состоянию, со всеми настройками и наборами ролей.
 - Packer идет в облачное хранилище в бэкенде и делает снапшот виртуальной машины, который потом загружает в облако в виде образа.
 

Схема работы Packer
В результате получается базовый образ, из которого можно развернуть любое количество виртуальных машин. Логика работы Packer похожа на логику Docker, когда из Docker-файла с описанием создают образ виртуальной машины.
Пример использования Packer
Покажу на примере. Возьмем директорию Nginx, она вложена в директорию Packer на GitHub:
# ls 
README.md	
	
packer 	
terraform
# cd packer/nginx
Далее соберем базовый образ с Nginx. Для этого:
- Берем стандартный образ из облака.
 - С помощью Ansible ставим в образ Nginx.
 - Изменяем конфигурацию заменой конфигурационного файла.
 - Запускаем и включаем Nginx.
 
Nginx взят исключительно для примера, по аналогии можно сконфигурировать любое приложение, которое запускается на виртуальных машинах.
Приступаем к сборке образа:
1. Для примера я создал на GitHub директорию Packer. В ней лежит директория Nginx и файл nginx.pkr.hcl.
# ls		
nginx		
nginx.pkr.hcl 	
playbook.yml
# vim nginx.pkr.hcl	
2. Файл nginx.pkr.hcl содержит конфигурацию для Packer, эта информация нужна, чтобы Packer понимал, в каком бэкенде и как нужно запустить виртуальную машину.
variable "image_tag" {
  type = string
}
source "openstack" "nginx" {
  source_image_filter {
    filters {
      name = "Centos-7.9-202107"
    }
    most_recent = true
  }
  flavor                  = "Basic-1-1-10"
  ssh_username            = "centos"
  security_groups         = ["all"]
  volume_size             = 10
  config_drive            = "true"
  use_blockstorage_volume = "true"
  networks                = ["298117ae-3fa4-4109-9e08-8be5602be5a2"]
  image_name = "nginx-${var.image_tag}"
}
build {
  sources = ["source.openstack.nginx"]
  provisioner "ansible" {
    playbook_file = "playbook.yml"
  }
}
3. В конфиге через
source "openstack" и source_image_filter указываем, какой базовый образ использовать для создания нового. В данном случае — Centos-7.9-202107, который находится в публичном доступе в облаке VK Cloud Solutions.4. Через
flavor указываем размер виртуальной машины, например: 1 ядро, 1 ГБ оперативной памяти, 10 ГБ диска. Эти параметры касаются только базовой виртуальной машины, с которой снимается образ. Из нового образа можно будет запускать ВМ любого размера.5. Здесь же указываем параметры подключения к ВМ по SSH и другие настройки. Доступ по SSH нужен, чтобы Packer мог запустить в облаке ВМ и получить доступ к Ansible, который подключается к ВМ и выполняет нужные действия. Только после этого создается снапшот виртуальной машины.
6. В переменных указываем название создаваемых образов. Здесь
nginx— постоянный префикс,
$ — переменная версии. Например,  nginx 0.0.1. Для этого в начале описываем, что есть переменная image_tag типа string. При старте сборки образа мы запускаем Packer и указываем image_tag. Алгоритм такой же, как при сборке Docker-образов, то есть у нас будет образ nginx 0.0.1, 0.0.2, 0.0.3, 1.0.0 и так далее — сразу версионируем образы.7. В блоке
build описываем, как запускать провижинер и какой. В данном случае Ansible. Тут же указываем файл, который нужен для запуска, —  playbook.yml. 8. В файле playbook.yml указываем, что на всех хостах нужно запустить роль
 Nginx.---
- hosts: all
  become: true
  
  roles:
    - nginx
9. В файле роли
 Nginx все довольно просто: указываем минимальную установку Nginx, прикрепляем набор конфигураций, добавляем репозиторий с исходным Nginx и устанавливаем его. После этого копируем конфигурацию в виртуальную машину, стартуем и энейблим сервис с Nginx. ---
- name: Add nginx repo | Centos
  yum_repository:
    baseurl: http://nginx.org/packages/mainline/rhel/7/$basearch/
    enabled: true
    gpgcheck: false
    description: Nginx repo
    name: nginx
  when: ansible_facts['os_family'] == "RedHat"
- name: Install Nginx | Centos
  yum:
    name: nginx
    state: present
  when: ansible_facts['os_family'] == "RedHat"
- name: Add Nginx config
  template:
    src: default.conf.j2
    dest: /etc/nginx/conf.d/default.conf
    mode: 0644
- name: Start and enable Nginx
  service:
    name: nginx
    enabled: true
    state: started
То есть после того, как из этого образа ВМ мы создадим всю инфраструктуру нескольких виртуальных машин, там изначально будет стоять Nginx, сконфигурированный, запущенный и заэнейбленый. После запуска этой виртуальной машины включится Nginx и будет готов обслуживать пользователей.
10. В файле конфига default.conf просим Nginx при обращении к корню возвращать «200» и свой hostname. Это поможет понять, как работают инстансы и выполняется балансировка.
server {
  listen       80 default_server;
  server_name  _;
  default_type text/plain;
  location / {
    return 200 '$hostname\n';
  }
}
Это вся конфигурация, которая нужна для запуска Packer. Перед этим важно соблюсти два условия:
- Передать в Packer ключи для взаимодействия с API облака, то есть логин и пароль учетной записи.
 - Локально установить и настроить Ansible. 
 
Запустим Packer.
# packer build -var ‘image_tag=1.0.1' nginx.pkr.hcl 
openstack: output will be in this color
==> openstack: Loading flavor: Basic-1-1-10
       openstack: Verified flavor. ID: df3c499a-044f-41d2-8612-d303adc613cc
==> openstack: Creating temporary keypair: packer_621795d4-d237-1627-ef62-57d0952dl304 ...
==> openstack: Created temporary keypair: packer_621795d4-d237-1627-ef62-57d0952dl304
       openstack: Found Image ID: 44709803-5ec2-496b-88b7-85a5250e51c4
==> openstack: Creating volume...
==> openstack: Waiting for volume packer_621795d4-d40a-0146-b30c-92f00elae3b4 (volume id: 9102e41e-15b7-4ef6-8c27-7a0de 49ce922) to become available...
      openstack: Volume ID: 9102e41e-15b7-4ef6-8c27-7a0de49ce922
=> openstack: launching server...
==> openstack: Launching server... openstack: Server ID: f44e40eb-a907-49e3-8d7e-89b20eaa8df1
==> openstack: Waiting for server to become ready...
- Выполняем команду 
packer build - var, указываем переменнуюimage_tagс индексом 1.0.1 и передаем конфигурационный файл nginx.pkr.hcl, который мы использовали раньше.
 - После выполнения команды Packer идет в интерфейс облака и запускает виртуальную машину.
 - При этом в личном кабинете облака начинает создаваться виртуальная машина с указанным типом. На этом этапе Packer ждет запуск ВМ и последующий запуск SSH. После этого на ВМ запускается Ansible, который снимает с ВМ образ.
 - После завершения обработки в интерфейсе облака будет создан образ, из которого можно запустить любое количество виртуальных машин.
 

Вручную добавлять инстансы, конфигурировать и настраивать каждую ВМ в облаке опасно: даже из-за незначительной ошибки могут возникать глобальные сбои в инфраструктуре. Автоматизировать эти процессы и исключить ошибки позволяет Terraform.
Terraform
Terraform — инструмент от компании Hashicorp, который позволяет декларативно управлять инстансами, сетями, группами безопасности и другими компонентами инфраструктуры с помощью файлов конфигураций. Благодаря Terraform можно привести инфраструктуру к нужному состоянию декларативно, сразу указав нужные параметры системы.
Еще в Terraform предусмотрена функция «План». Благодаря ей инструмент сравнивает текущее состояние системы с будущим и отображает пользователю, что именно будет удалено, запущено или изменено в соответствии с новым конфигурационным файлом. Такая проверка помогает исключить ошибки при создании рабочей инфраструктуры.
Конфигурация Terraform
Конфигурация Terraform интереснее, чем у Packer. Для его работы файлы конфигурации должны находиться в директории, из который запускают инструмент. При этом оформление не имеет значения: конфигурацию можно описать как в одном файле, так и в нескольких, разделив на смысловые блоки.
# ls	
nginx		nginx.pkr.hcl 	playbook.yml
# cd ../../nginx/terraform				
# ls
keys.tf			network.tf		terraform.tfstate.backup
loadbalancer.tf		providers.tf		variables.tf
main.tf			terraform.tfstate	vars.tfvars
Как правило, описание конфигурации содержит:
1. Файл с описанием провайдеров. Terraform может работать с разными облаками, поэтому в файле описываем параметры провайдера — название и версию. Подробнее про настройку Terraform-провайдера для VK Cloud Solutions можно посмотреть здесь.
terraform {
    required_providers {
        vkcs = {
            source = "vk-cs/vkcs"
        }
    }
}
2. Файл с данными для подключения к облаку. Инструменту надо передать API endpoints, ключи и другие персональные идентификаторы.
3. Файл с описанием конфигурации виртуальных машин. Terraform работает с двумя типами объектов: Data и Resource. Data — то, что можно получить из облака. Например, конфигурация дефолтной сети облака. А Resource — то, что создаем сами.
resource "vkcs_compute_instance" "instance" {
  count = var.node_count
  name = "node-${count.index}"
  image_name = "${var.image_name}-${var.image_tag}"
  flavor_name = var.flavor_name
  key_pair = vkcs_compute_keypair.ssh.name
  config_drive = true
  security_groups = [
    vkcs_networking_secgroup.secgroup.name
  ]
  network {
    name = vkcs_networking_network.example_routed_private_network.name
  }
  lifecycle {
    create_before_destroy = true
  }
}
В описании Resource указываем тип ресурса:
vkcs_compute_instance. Также указываем внутреннее имя ресурса, которое Terraform будет использовать для автоматического построения зависимостей. С помощью таких имен можно обращаться к свойствам других объектов.В этом файле конфигурации также указываем переменную с названием ВМ — с префиксом node и переменной
count.index. Прописываем еще две переменные: image_name и image_tag, которые будут указывать на название и тег образа, созданного с помощью Packer. Также описываем и другие параметры.
4. Файл с описанием сетей. В файле с сетями используется другой тип объектов — Data. Он нужен для запроса данных из облака в переменную
ext-net. Также в файле прописываем приватную сеть и подсеть, создаем роутер, конфигурируем Security-группы и настраиваем остальные сетевые параметры.data "vkcs_networking_network" "extnet" {
  name = "ext-net"
}
resource "vkcs_networking_network" "example_routed_private_network" {
  name = "example_routed_private_network"
}
resource "vkcs_networking_subnet" "example_routed_private_subnet" {
  name        = "example_routed_private_subnet"
  network_id  = vkcs_networking_network.example_routed_private_network.id
  cidr        = "10.0.2.0/24"
  ip_version  = 4
  enable_dhcp = true
}
resource "vkcs_networking_router" "example_router" {
  name                = "example_router"
  external_network_id = data.vkcs_networking_network.extnet.id
}
resource "vkcs_networking_router_interface" "example_router_interface" {
  router_id = vkcs_networking_router.example_router.id
  subnet_id = vkcs_networking_subnet.example_routed_private_subnet.id
}
resource "vkcs_networking_secgroup" "secgroup" {
  name        = "terraform__security_group"
  description = "security group for terraform instance"
}
resource "vkcs_networking_secgroup_rule" "secgroup_rule22" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = "${vkcs_networking_secgroup.secgroup.id}"
}
resource "vkcs_networking_secgroup_rule" "secgroup_rule80" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 80
  port_range_max    = 80
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = "${vkcs_networking_secgroup.secgroup.id}"
}
resource "vkcs_networking_secgroup_rule" "secgroup_rule-1" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "icmp"
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = "${vkcs_networking_secgroup.secgroup.id}"
}
5. Файл с ключами доступа. В нем прописываем, что Terraform сам автоматически генерирует SSH-ключ: приватную часть после запуска сохранит локально, а публичную часть положит в облако. Впоследствии публичный ключ будет доставлен во все новые ВМ.
resource "tls_private_key" "ssh" {
  algorithm = "RSA"
}
resource "vkcs_compute_keypair" "ssh" {
  name       = "terraform_ssh_key"
  public_key = tls_private_key.ssh.public_key_openssh
}
output "ssh" {
  value = tls_private_key.ssh.private_key_pem
  sensitive = true
}
Ресурс
tls_private_key.ssh.private_key_pem содержит приватный ключ. В выводе он помечен как sensitive = true и будет маскирован. Используем следующую команду для сохранения приватного ключа:terraform.exe output ssh
6. Файл с конфигурацией load balancer. Балансировщик нагрузки нужен, если планируется запускать несколько виртуальных машин. Для подготовки в файле создаем Load Balancer и внешний Listener, добавляем в балансировщик виртуальные машины и определяем правила проверки. При правильной настройке балансировщика Terraform обеспечивает Zero Downtime Deployment с плавной для пользователей сменой версии приложения.
resource "vkcs_networking_floatingip" "example_floating_ip" {
  pool    = "ext-net"
  port_id = vkcs_lb_loadbalancer.example_http_balancer.vip_port_id
}
resource "vkcs_lb_loadbalancer" "example_http_balancer" {
  name          = "example_http_balancer"
  description   = "An HTTP load balancer in a private network with 2 backends"
  vip_subnet_id = vkcs_networking_subnet.example_routed_private_subnet.id
}
resource "vkcs_lb_listener" "example_http_listener" {
  name            = "example_http_listener"
  description     = "A load balancer frontend that listens on 80 prot for client traffic"
  protocol        = "HTTP"
  protocol_port   = 80
  loadbalancer_id = vkcs_lb_loadbalancer.example_http_balancer.id
}
resource "vkcs_lb_pool" "example_http_pool" {
  name        = "example_http_pool"
  description = "A load balancer pool of backends with Round-Robin algorithm to distribute traffic to pool's members"
  protocol    = "HTTP"
  lb_method   = "ROUND_ROBIN"
  listener_id = vkcs_lb_listener.example_http_listener.id
}
resource "vkcs_lb_monitor" example_http_monitor {
  name           = "example_http_monitor"
  delay          = 5
  max_retries    = 3
  timeout        = 5
  type           = "HTTP"
  url_path       = "/"
  http_method    = "GET"
  expected_codes = "200"
  pool_id        = vkcs_lb_pool.example_http_pool.id
}
resource "vkcs_lb_member" "example_http_member" {
  count         = var.node_count
  name          = "example_http_member-${count.index}"
  address       = vkcs_compute_instance.instance.*.access_ip_v4[count.index]
  protocol_port = 80
  weight        = 10
  pool_id       = vkcs_lb_pool.example_http_pool.id
  subnet_id     = vkcs_networking_subnet.example_routed_private_subnet.id
  lifecycle {
    create_before_destroy = true
  }
}
output "example_http_balancer_vip_address" {
  value = vkcs_networking_floatingip.example_floating_ip.address
}
Для запуска Terraform и начала создания указанной инфраструктуры нужно выполнить
terraform apply. В ответ на это Terraform выводит весь план создаваемой конфигурации, запрашивает подтверждение и начинает создавать в облаке всю заданную инфраструктуру с сетями, подсетями, роутерами, балансировщиками и другими компонентами.В итоге IaC-инструменты сводят всю подготовку инфраструктуры к простым действиям:
- Описание образа.
 - Описание состояний виртуальных машин с помощью Ansible.
 - Установка и настройка Nginx.
 - Запуск виртуальных машин с помощью Terraform.
 
При таком подходе не нужно будет катать Ansible по всем создаваемым виртуальным машинам, что упрощает внесение любых изменений.
Обновленный CI/CD-пайплайн
C помощью Packer и Terraform можно реализовать все принципы Docker с неизменяемой инфраструктурой и Kubernetes с выкатыванием новых версий через перезапуск.
Связка инструментов отлично работает с инфраструктурой на виртуальных машинах в облаке и сохраняет привычный алгоритм:
- Разработчик выпускает новую версию приложения.
 - С помощью Packer делаем новый образ ВМ на основе новой версии приложения.
 - С помощью Terraform автоматизировано повторно разворачиваем всю инфраструктуру без даунтайма с Health Check от балансировщиков.
 

На нашей платформе VK Cloud Solutions можно протестировать Terraform. Для этого при регистрации мы начисляем пользователям 3000 бонусных рублей — приходите, пробуйте и оставляйте обратную связь.
          
 
amarao
Путаница какая-то. Golden artifact и baked image - два совершенно разных явления. Golden artifact - это такой анти-паттерн. Волшебное чудо, собранное один раз человеком. Мы знаем, что этот образ работает, но второй раз сделать точно такой же но с апдейтами не можем. Golden artifact используется, но не воспроизводится (кроме как копированием).
А baked image - вполне себе разумный паттерн, который кому-то ощутимо экономит время деплоя. Его можно воспроизвести, протестировать и переделать если что-то чуть-чуть не так.