DevOps
Модуль 4: Infrastructure as Code (IaC)




Модуль 4: Infrastructure as Code (IaC)
Ручная настройка серверов не масштабируется. Инфраструктура лежит в Git, проходит review и тестируется как обычный код. Terraform — индустриальный стандарт для создания ресурсов (Provisioning).

Если вы раньше работали «руками в консоли», этот модуль может сначала показаться самым сложным.

  • нужно одновременно думать про код, облачные ресурсы и зависимости между ними;
  • ошибка в одной строке может поменять много инфраструктуры;
  • появляется новая сущность state, которой нет в обычной backend-разработке.

Зачем терпеть этот порог входа:
  • повторяемость (одинаковая инфраструктура dev/stage/prod);
  • прозрачность изменений через plan и PR review;
  • меньше «ручной магии», которую невозможно поддерживать через полгода.

Полезная ментальная модель:

  • Terraform описывает желаемое состояние (desired state).
  • Terraform сравнивает код со state и реальностью → строит план.
  • После apply Terraform записывает, какие объекты он «держит за руку».
Необходимый инструментарий
  • Terraform CLI.
  • Yandex Cloud / AWS Free Tier аккаунт (или LocalStack для эмуляции AWS локально).

Опционально (но сильно помогает):

  • tflint / terraform validate (база качества)
  • checkov (политики/безопасность IaC)
  • pre-commit хуки (форматирование + линт)
Часть 1. Terraform Basics
4.0 Структура проекта (как не превратить main. tf в свалку)
Минимальный живучий шаблон:
.
├── main.tf        # ресурсы/модули
├── variables.tf   # входные переменные
├── outputs.tf     # выходные значения
├── versions.tf    # required_version + required_providers
├── locals.tf      # локальные вычисления (опционально)
└── env/
  ├── dev.tfvars
  └── prod.tfvars
Базовые команды качества (делайте автоматически перед PR):
terraform fmt -recursive
terraform validate
Если вы работаете в команде: договоритесь о единых правилах (линтер/хуки), иначе diff’ы будут шумными.
4.1 Структура проекта (main.tf)
Пример создания виртуальной машины (EC2) в AWS.
# 1. Провайдер (Плагин для API AWS)
provider "aws" {
  region = "us-east-1"
}

# 2. Находим свежий AMI Ubuntu автоматически (хардкод AMI сломается при смене региона)
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }
}

# 3. Ресурс (То, что мы создаём)
resource "aws_instance" "app_server" {
  ami           = data.aws_ami.ubuntu.id   # Всегда актуальный образ
  instance_type = "t2.micro"                # 1 CPU, 1 GB RAM

  tags = {
    Name = "Production-App"
    Environment = "Prod"
  }
}

# 3. Output (Что вернуть после создания)
output "instance_ip" {
  value = aws_instance.app_server.public_ip
  description = "Публичный IP созданного сервера"
}
4.2 Variables (variables.tf)
Хардкодить значения — плохая практика. Используем переменные.
variable "instance_count" {
  description = "Сколько серверов создать"
  type        = number
  default     = 2
}

resource "aws_instance" "web" {
  count         = var.instance_count # Цикл
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}
Откуда берутся значения переменных (практически важное):
  • -var и -var-file в CLI (удобно для CI)
  • файлы *.tfvars (например, env/prod.tfvars)
  • переменные окружения TF_VAR_name=value
Пример запуска с окружением:
terraform plan -var-file=env/dev.tfvars
terraform apply -var-file=env/prod.tfvars
Если значение секретное — помечайте sensitive = true и не логируйте его в outputs.
Часть 2. Жизненный цикл (Workflow)
  1. Init: terraform init Скачивает плагин aws в папку.terraform.
  2. Plan: terraform plan Этот шаг нельзя пропускать. plan читает текущее состояние облака, сравнивает с кодом и показывает, что собирается сделать.
  • + create (зеленый)
  • — destroy (красный) — Внимание!
  • ~ update (желтый)

3. Apply: terraform apply Применяет изменения. Создает файл terraform.tfstate.
Практичные флаги для жизни:
  • terraform plan -out plan. tfplan → сохранить план как артефакт.
  • terraform apply plan. tfplan → применить именно то, что было проверено.
  • terraform apply -auto-approve — ок только в автоматизации и там, где у вас уже есть review плана.
State File (terraform.tfstate): Это JSON-файл, где Terraform хранит связку: «Ресурс в коде aws_instance.web= ID в облаке i-12 345 678».
  • Правило: Никогда не правь этот файл руками.
  • Teamwork: В команде стейт хранят в S3 (Remote State), чтобы все видели актуальное состояние.

Полезные команды работы со state:
terraform state list
terraform state show aws_instance.app_server
4.3 Remote state и блокировки (почему без этого больно)
Когда вы один — локальный state норм. Когда вас несколько:
  • нужен remote backend (чтобы все работали с одним state)
  • нужна блокировка (чтобы два человека не применили изменения одновременно)
Примерный принцип (без привязки к конкретному облаку):
  • state хранится в объектном хранилище (S3/GCS/YC Object Storage)
  • lock хранится отдельно (DynamoDB/etcd/аналог)
Важно: настройка backend — это «фундамент». Один раз сделаете правильно — сэкономите недели.
Часть 3. Ansible (кратко)
Если Terraform создает сервер, то Ansible настраивает внутренности (Nginx, Users, Configs). Пример Playbook(deploy.yaml):
- name: Configure Web Server
  hosts: webservers # Группа из inventory файла
  become: true      # sudo

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Start Nginx
      service:
        name: nginx
        state: started
        enabled: yes
Ключевые идеи Ansible для DevOps:
  • Idempotency: повторный запуск не должен ломать систему.
  • Inventory: список хостов и групп.
  • Handlers: перезапуски сервисов делать только когда конфиг изменился.
Мини-улучшение: handler.
- name: Configure Web Server
  hosts: webservers
  become: true

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Put nginx config
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: restart nginx

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
Часть 4. Terraform в реальной жизни (модули, рефакторинг, drift)
4.4 Locals и Outputs
  • locals — чтобы не копировать одно и то же в 5 мест.
  • output — чтобы отдавать наружу важные значения (IP, DNS, IDs).

Пример:
locals {
  common_tags = {
    Project     = "demo"
    Environment = "dev"
  }
}

resource "aws_instance" "app_server" {
  # ...
  tags = local.common_tags
}
4.5 Modules (как масштабировать код)
Модуль — это переиспользуемый набор ресурсов. Обычно вы выносите в модуль VPC, базу, сервис.

Принцип:

  • Root module (корень) вызывает child modules.
  • Модули получают входные параметры через variables и отдают outputs.
4.6 Drift: когда реальность не совпадает с кодом
Классика: кто-то «быстро поправил в консоли», а через неделю Terraform «внезапно» хочет всё откатить.

Минимальная привычка:
terraform plan
Регулярный plan в CI по PR — дешёвая страховка.
4.7 Import: как взять под контроль существующий ресурс
Если ресурс уже существует (создан руками/скриптом), Terraform можно научить про него знать:
terraform import aws_instance.app_server i-0123456789abcdef0
После импорта нужно привести код в соответствие с реальными настройками, иначе следующий plan захочет «починить» расхождения.
4.8 Безопасный рефакторинг: moved blocks
Иногда вы переименовываете ресурсы/переносите их в модуль. Чтобы Terraform не «удалил и создал заново», используйте moved:
moved {
  from = aws_instance.web
  to   = aws_instance.app_server
}
Troubleshooting Guide (Terraform)
1. State lock / «ConditionalCheckFailedException» / «already locked» Кто-то уже выполняет apply, или предыдущий запуск упал и оставил lock.
  • Действие: дождитесь завершения; если уверены, что lock «мертвый», используйте terraform force-unlock <lock-id>.
2. Provider version mismatch В команде/CI разные версии провайдера.
  • Действие: фиксируйте версии в required_providers и обновляйте осознанно.
3. Постоянные «replace» вместо «update» Ресурс нельзя изменить на лету (например, некоторые поля у VM/сетей).
  • Действие: смотрите diff в plan и принимайте решение (окно работ/blue-green).
Итоговая задача: "Terraform + Docker"
Задание без расходов на облако. Используем kreuzwerker/docker провайдер для управления локальным Docker через Terraform.

Задача:

  1. Создать main. tf:
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0.1"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:stable-alpine"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}
2. Выполнить terraform init -> terraform apply.
3. Проверить браузером localhost:8000.
4. Поменять внешний порт на 8080 в коде.
5. Сделать terraform apply. Посмотреть, как Terraform удалит старый контейнер и создаст новый (так как порты нельзя менять на лету).