менять ВМ через новый образ, а не правки на живом сервере

Неизменяемая инфраструктура с Packer и Terraform: выкатка ВМ без простоя в масштабе

14 минут

Правки по SSH и дрейф конфигурации делают мутабельные ВМ ненадёжными. Запекайте состояние ОС и приложения в образ через Packer, поднимайте инфраструктуру Terraform и меняйте инстансы через rolling refresh Auto Scaling — без «любимых» серверов.

Почему мутабельные ВМ расходятся и где нужна неизменяемость

Классический мутабельный сервер заканчивается «археологией»: SSH, пакет, правка конфига, перезапуск сервиса. Через неделю то же делает другой инженер. Одинаковые машины расходятся, staging перестаёт совпадать с продакшеном, патчи безопасности становятся лотереей. Контейнеры задают неизменяемость на уровне процесса, но не каждая нагрузка туда ложится: legacy с глубокой завязкой на ОС, GPU и чувствительность к задержке, базы на локальных дисках, регулируемые образы с фиксированным baseline ОС — всё ещё на ВМ. Цель — дисциплина как у контейнеров: пересобрать, а не допатчить. Изменилось что-то важное — новый AMI или облачный образ и замена инстансов; продакшен вручную не трогают.

Сначала сборка образа, затем развёртывание: разные циклы обновлений

Первый этап — Packer: образ с пакетами ОС, артефактами приложения, агентами мониторинга и hardening-скриптами. После сборки артефакт не меняют — обновление значит новый ID образа. Второй этап — Terraform: сеть, Auto Scaling Group, балансировщики и IAM поверх этого образа. Релизы приложения меняют конвейер образа; ёмкость, подсети и security groups — конвейер инфраструктуры. Раздельные циклы делают каждое изменение небольшим и проверяемым. Цепочка: коммит, CI собирает артефакты, Packer печёт образ, manifest отдаёт AMI ID, Terraform обновляет launch template, instance refresh крутит замену за балансировщиком.

Шаблон Packer: провижининг, проверка, manifest

Зафиксируйте плагин Amazon, параметризуйте app_version и base_ami, помечайте образы тегами происхождения сборки и гоняйте self-test до финализации. Копируйте tarballs из CI, ставьте агенты с зафиксированными версиями пакетов, пишите packer-manifest.json для Terraform. user_data в Terraform держите минимальным — секреты и средо-специфичные значения в SSM Parameter Store или ролях инстанса, не в golden image.

HCL · шаблон Packer для web server AMI
packer {
  required_plugins {
    amazon = {
      source  = "github.com/hashicorp/amazon"
      version = "~> 1.3"
    }
  }
}

variable "app_version" { type = string }
variable "base_ami" { type = string }

source "amazon-ebs" "webserver" {
  ami_name      = "webserver-${var.app_version}-{{timestamp}}"
  instance_type = "t3.medium"
  region        = "us-east-1"
  source_ami    = var.base_ami
  ssh_username  = "ec2-user"

  tags = {
    Name       = "webserver-${var.app_version}"
    AppVersion = var.app_version
    BuildTime  = "{{timestamp}}"
    ManagedBy  = "packer"
  }
}

build {
  sources = ["source.amazon-ebs.webserver"]

  provisioner "shell" {
    inline = [
      "sudo dnf update -y",
      "sudo dnf install -y amazon-cloudwatch-agent awscli jq",
      "sudo systemctl enable amazon-cloudwatch-agent",
    ]
  }

  provisioner "file" {
    source      = "build/app-${var.app_version}.tar.gz"
    destination = "/tmp/app.tar.gz"
  }

  provisioner "shell" {
    inline = [
      "sudo mkdir -p /opt/app",
      "sudo tar -xzf /tmp/app.tar.gz -C /opt/app",
      "sudo chown -R appuser:appuser /opt/app",
      "sudo systemctl enable app-server",
    ]
  }

  provisioner "shell" {
    inline = ["sudo /opt/app/bin/healthcheck --self-test"]
  }

  post-processor "manifest" {
    output     = "packer-manifest.json"
    strip_path = true
  }
}

Terraform: launch template, Auto Scaling Group и rolling instance refresh

Передавайте baked ami_id в launch template с create_before_destroy. Подключите Auto Scaling Group к target group Application Load Balancer с проверками ELB. После смены launch template запускайте aws_autoscaling_instance_refresh с min_healthy_percentage и instance_warmup. ignore_changes на desired_capacity, если отдельные политики autoscaling меняют ёмкость.

HCL · launch template и Auto Scaling Group
resource "aws_launch_template" "webserver" {
  name_prefix   = "webserver-"
  image_id      = var.ami_id
  instance_type = "t3.medium"

  iam_instance_profile {
    name = aws_iam_instance_profile.webserver.name
  }

  network_interfaces {
    security_groups             = [aws_security_group.webserver.id]
    associate_public_ip_address = false
  }

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name       = "webserver"
      AppVersion = var.app_version
      ManagedBy  = "terraform"
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "webserver" {
  name                = "webserver-asg"
  desired_capacity    = var.desired_capacity
  min_size            = var.desired_capacity
  max_size            = var.desired_capacity + 2
  vpc_zone_identifier = var.private_subnet_ids

  launch_template {
    id      = aws_launch_template.webserver.id
    version = "$Latest"
  }

  target_group_arns         = [aws_lb_target_group.webserver.arn]
  health_check_type         = "ELB"
  health_check_grace_period = 120

  lifecycle {
    ignore_changes = [desired_capacity]
  }
}

resource "aws_autoscaling_instance_refresh" "webserver" {
  autoscaling_group_name = aws_autoscaling_group.webserver.name
  strategy               = "Rolling"

  preferences {
    min_healthy_percentage = 75
    instance_warmup        = 120
  }

  triggers {
    launch_template {
      versions = [aws_launch_template.webserver.latest_version]
    }
  }
}
HCL · health check target group ALB
resource "aws_lb_target_group" "webserver" {
  name     = "webserver-tg"
  port     = 8080
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path                = "/healthz"
    interval            = 15
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

CI: сборка образа, apply, ожидание refresh

Свяжите Packer build, извлечение AMI ID из manifest, Terraform apply с новыми переменными и опрос статуса instance refresh до Successful. Команды aws autoscaling wait instance-refresh не существует — используйте describe-instance-refreshes в цикле. Храните предыдущие AMI ID в SSM или реестре manifest для отката одной командой.

Bash · скрипт деплоя с опросом refresh
#!/usr/bin/env bash
set -euo pipefail

APP_VERSION="${1:?Usage: deploy.sh <version>}"

packer init packer/
packer build -var "app_version=${APP_VERSION}" packer/web-server.pkr.hcl

AMI_ID=$(jq -r '.builds[-1].artifact_id' packer/packer-manifest.json | cut -d: -f2)

cd infrastructure/
terraform init -input=false
terraform apply -auto-approve \
  -var "app_version=${APP_VERSION}" \
  -var "ami_id=${AMI_ID}"

REFRESH_ID=$(aws autoscaling describe-instance-refreshes \
  --auto-scaling-group-name webserver-asg \
  --query 'InstanceRefreshes[0].InstanceRefreshId' --output text)

until [[ "$(aws autoscaling describe-instance-refreshes \
  --auto-scaling-group-name webserver-asg \
  --instance-refresh-ids "$REFRESH_ID" \
  --query 'InstanceRefreshes[0].Status' --output text)" == "Successful" ]]; do
  sleep 15
done

echo "Deployed ${APP_VERSION} on AMI ${AMI_ID}"

Практика: происхождение сборки, запрет SSH, компактные образы и откат

Помечайте каждый образ и инстанс SHA коммита, ID сборки и версией шаблона. Закройте SSH в продакшен через security groups; ловите дрейф конфигурации агентами. Фиксируйте версии пакетов в provisioner Packer. Разделите ежеквартальный hardened base и частый слой приложения. Гоняйте интеграционные тесты на временном инстансе до промоушена AMI. Держите три–пять последних образов для отката повторным apply с предыдущим ami_id. Цель по времени сборки приложения — меньше десяти минут за счёт кэша и меньших артефактов. Весь путь в CI: от коммита до instance refresh без ручных шагов — для аудита и для сна дежурного. Неизменяемые ВМ дают воспроизводимость и безопасный откат там, где ещё нужна модель машины, без привычки «подправить на живом сервере».

Перед продакшеном проверяйте Terraform-модули по подходу из гайда по тестированию инфраструктуры как кода с Terraform и Kitchen-Terraform.

Даже при неизменяемых образах нужен контроль дрейфа оркестрации — см. гайд по обнаружению и исправлению дрейфа инфраструктуры с Terraform.