менять ВМ через новый образ, а не правки на живом сервере
Неизменяемая инфраструктура с 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.
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 меняют ёмкость.
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]
}
}
}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 для отката одной командой.
#!/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.
