Terraform и Ansible: полное руководство по Infrastructure as Code (IaC) для DevOps | Примеры для AWS и Yandex Cloud | AdminWiki
Timeweb Cloud — сервера, Kubernetes, S3, Terraform. Лучшие цены IaaS.
Попробовать

Terraform и Ansible: полное руководство по Infrastructure as Code (IaC) для DevOps | Примеры для AWS и Yandex Cloud

16 апреля 2026 11 мин. чтения
Содержание статьи

Полная автоматизация жизненного цикла инфраструктуры — от создания облачных ресурсов до настройки ПО на них — стала стандартом для эффективных DevOps-команд. Связка Terraform для декларативного развертывания и Ansible для идемпотентной конфигурации образует мощный, проверенный на практике дуэт. Это руководство предоставляет готовые модули Terraform для AWS и Yandex Cloud, роли Ansible для Nginx и PostgreSQL, а также детальные инструкции по их безопасной интеграции в командный CI/CD workflow, позволяя вам заменить ручные операции на надежный, воспроизводимый код.

От Infrastructure as Code к работающему сервису: философия связки Terraform + Ansible

Ключевой принцип современной инфраструктуры — идемпотентность: повторное применение конфигурации должно приводить к одинаковому, предсказуемому результату. Terraform и Ansible реализуют этот принцип на разных этапах жизненного цикла, образуя четкое разделение ответственности. Terraform выступает как архитектор и строитель, декларативно описывая и создавая «железо» и базовые облачные сервисы. Ansible же выполняет роль отделочника и наладчика, обеспечивая нужное состояние программного обеспечения внутри уже созданных ресурсов. Такой подход устраняет хаос ручных изменений, обеспечивает полную воспроизводимость сред (от dev до prod) и создает прозрачный аудит всех изменений через систему контроля версий.

Почему не только Terraform или не только Ansible? Границы ответственности

Использование одного инструмента для всех задач ведет к усложнению и нарушению best practices. Terraform идеально подходит для создания и управления внешними ресурсами облачных провайдеров: виртуальными сетями (VPC), подсетями, группами безопасности (Security Groups), виртуальными машинами (EC2, Compute Cloud), балансировщиками нагрузки и базами данных как сервис (RDS). Его декларативная модель гарантирует, что реальное состояние инфраструктуры всегда соответствует коду. Однако для тонкой настройки операционной системы (установка пакетов, конфигурационные файлы, управление системными сервисами) встроенные provisioners (local-exec, remote-exec) считаются крайней мерой (last resort), так как они усложняют отладку и нарушают чистоту модели.

Ansible, будучи императивным инструментом управления конфигурацией, создан именно для задач внутри ОС. Он предоставляет сотни модулей для атомарных операций (apt, yum, template, service), которые гарантируют идемпотентность. Хотя у Ansible есть модули для облачных провайдеров (ec2_instance, vpc), их использование для масштабного provisioning менее удобно и декларативно по сравнению с Terraform. Таким образом, граница проходит четко: Terraform управляет «внешней» инфраструктурой, Ansible — «внутренней» конфигурацией.

Целевой workflow: от `terraform apply` до `ansible-playbook run`

Идеальный автоматизированный пайплайн выглядит как последовательная цепочка:

  1. Код Terraform: Описание необходимых облачных ресурсов в модулях (main.tf).
  2. План и применение: Запуск terraform plan для проверки и terraform apply для создания ресурсов.
  3. Извлечение выходных данных: Terraform экспортирует ключевые атрибуты созданных ресурсов (IP-адреса, имена хостов, ID) через outputs.
  4. Динамический инвентарь Ansible: Специальный скрипт (dynamic inventory) автоматически преобразует outputs Terraform (например, из JSON-файла) в список хостов для Ansible.
  5. Запуск плейбуков: Выполнение ansible-playbook применяет роли для установки и настройки ПО (Nginx, PostgreSQL) на созданных серверах.
  6. Готовый сервис: В результате получается полностью настроенная и работоспособная среда.

Автоматическая передача данных (например, IP-адресов) между этапами исключает ошибки ручного копирования и делает процесс воспроизводимым.

Практикум: развертывание инфраструктуры с помощью модулей Terraform

Рассмотрим создание базовых модулей для двух облачных провайдеров. Структура каждого модуля следует best practices: main.tf (основные ресурсы), variables.tf (входные переменные), outputs.tf (выходные данные). Все примеры проверены с актуальными версиями провайдеров: AWS Provider (~> 5.0) и Yandex Cloud Provider (~> 0.100).

Модуль для AWS: создаем безопасную основу для веб-сервера

Модуль создает минимальную, но безопасную инфраструктуру в AWS для размещения веб-сервера: VPC с включенным DNS, публичную подсеть, security group и EC2-инстанс.

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = {
    Name = "web-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  availability_zone       = "${var.aws_region}a"
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Allow HTTP and SSH"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_access_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = var.key_pair_name

  tags = {
    Name = "web-server"
    Role = "web"
  }
}

Важно: Укажите свои значения для переменных aws_access_key, aws_secret_key (через переменные окружения или backend) и key_pair_name.

Модуль для Yandex Cloud: аналогичная логика в другом облаке

Адаптация подхода для Yandex Cloud демонстрирует принципы cloud-agnostic дизайна. Основные отличия — в названиях ресурсов и способе аутентификации (через service account key).

# main.tf
terraform {
  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
      version = "~> 0.100"
    }
  }
}

provider "yandex" {
  service_account_key_file = var.service_account_key_file
  cloud_id                 = var.cloud_id
  folder_id                = var.folder_id
  zone                     = var.zone
}

resource "yandex_vpc_network" "network" {
  name = "web-network"
}

resource "yandex_vpc_subnet" "subnet" {
  name           = "web-subnet"
  zone           = var.zone
  network_id     = yandex_vpc_network.network.id
  v4_cidr_blocks = ["192.168.10.0/24"]
}

resource "yandex_compute_instance" "web" {
  name        = "web-server"
  platform_id = "standard-v3"
  zone        = var.zone

  resources {
    cores  = 2
    memory = 2
  }

  boot_disk {
    initialize_params {
      image_id = "fd8vmcue7aajpmeo39kk" # Ubuntu 22.04 LTS
    }
  }

  network_interface {
    subnet_id = yandex_vpc_subnet.subnet.id
    nat       = true # Важно для публичного IP
  }

  metadata = {
    ssh-keys = "ubuntu:${file(var.public_key_path)}"
  }

  labels = {
    role = "web"
  }
}

Ключевое отличие — в Yandex Cloud публичный IP (NAT) назначается на уровне сетевого интерфейса (network_interface { nat = true }), а не инстанса.

Ключевые outputs: готовим данные для Ansible

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

# outputs.tf (для AWS)
output "web_instance_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "web_instance_id" {
  description = "ID of the web server"
  value       = aws_instance.web.id
}

output "security_group_id" {
  description = "ID of the security group"
  value       = aws_security_group.web.id
}

После применения конфигурации сохраните outputs в формате JSON, который сможет прочитать Ansible Dynamic Inventory:

terraform output -json > terraform_output.json

Этот файл станет источником данных для автоматического построения инвентаря.

Настройка созданных серверов: от ролей Ansible до готовых сервисов

После создания инфраструктуры переходим к ее конфигурации. Ansible работает с инвентарем хостов. Вместо статического файла с IP-адресами используем Dynamic Inventory, который автоматически получает данные из outputs Terraform.

Dynamic Inventory: автоматически находим хосты из Terraform

Простой Python-скрипт может преобразовать JSON от Terraform в инвентарь Ansible. Создайте файл inventory_terraform.py:

#!/usr/bin/env python3
import json
import sys

with open('terraform_output.json') as f:
    tf_output = json.load(f)

inventory = {
    'web': {
        'hosts': [tf_output['web_instance_public_ip']['value']],
        'vars': {
            'ansible_user': 'ubuntu',
            'ansible_ssh_private_key_file': '~/.ssh/id_rsa'
        }
    }
}

if sys.argv[1] == '--list':
    print(json.dumps(inventory))
elif sys.argv[1] == '--host':
    print(json.dumps({}))

Сделайте скрипт исполняемым (chmod +x inventory_terraform.py) и настройте ansible.cfg или указывайте его напрямую: ansible-playbook -i inventory_terraform.py site.yml.

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

Роль `nginx`: установка, конфигурация и деплой статики

Роль Ansible — это переиспользуемый компонент для решения конкретной задачи. Структура роли nginx:

roles/nginx/
├── tasks/
│   └── main.yml
├── handlers/
│   └── main.yml
├── templates/
│   └── nginx.conf.j2
└── defaults/
    └── main.yml

tasks/main.yml:

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

- name: Deploy Nginx configuration template
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/default
  notify: restart nginx

- name: Deploy index.html
  copy:
    src: files/index.html
    dest: /var/www/html/index.html
    mode: '0644'

handlers/main.yml:

- name: restart nginx
  service:
    name: nginx
    state: restarted

defaults/main.yml (переменные по умолчанию):

nginx_port: 80
server_name: "localhost"

Эта роль гарантирует, что Nginx будет установлен, сконфигурирован и запущен, а при изменении шаблона конфигурации — автоматически перезагружен.

Роль `postgresql`: от установки до настройки репликации

Роль для PostgreSQL демонстрирует управление более сложным stateful-сервисом. Она может быть адаптирована под кластерную конфигурацию.

# tasks/main.yml (фрагмент)
- name: Install PostgreSQL
  apt:
    name: "postgresql-{{ pg_version }}"
    state: present

- name: Configure pg_hba.conf
  template:
    src: pg_hba.conf.j2
    dest: /etc/postgresql/{{ pg_version }}/main/pg_hba.conf
  notify: restart postgresql

- name: Configure postgresql.conf
  template:
    src: postgresql.conf.j2
    dest: /etc/postgresql/{{ pg_version }}/main/postgresql.conf
  notify: restart postgresql

- name: Create database
  postgresql_db:
    name: "{{ db_name }}"

- name: Create user
  postgresql_user:
    name: "{{ db_user }}"
    password: "{{ db_password }}"
    db: "{{ db_name }}"
    priv: "ALL"

Для организации репликации можно использовать inventory-переменные для определения роли сервера:

# В инвентаре для хоста-мастера
db_role: master
# Для хоста-реплики
db_role: replica

Задачи в роли могут содержать условные операторы (when: db_role == 'master') для выполнения специфичных действий. Важно: Данная роль — базовый пример. Для production необходимо детально настроить параметры производительности (shared_buffers, work_mem), безопасность (SSL) и мониторинг.

Сводный плейбук: запускаем всю конфигурацию

Плейбук верхнего уровня site.yml определяет, какие роли и к каким хостам применять.

---
- name: Configure web servers
  hosts: web
  become: yes
  roles:
    - nginx

- name: Configure database servers
  hosts: db
  become: yes
  roles:
    - postgresql

Запуск всей конфигурации после создания инфраструктуры:

ansible-playbook -i inventory_terraform.py site.yml

Если ваш dynamic inventory корректно поместил хосты в группы web и db на основе тегов Terraform (например, tag_Role_Web), плейбук автоматически применит нужные роли к нужным серверам.

Безопасность и командная работа: управление State, блокировки и CI/CD

Использование Terraform и Ansible в одиночку на локальной машине — лишь первый шаг. Для работы в команде и в продакшн-средах критически важны безопасное хранение state, блокировка от параллельного применения и интеграция в CI/CD.

Remote Backend: храним state файл безопасно и доступно

Локальный файл terraform.tfstate — это точка отказа. Его потеря делает управление инфраструктурой невозможным. Remote backend решает эту проблему.

Для AWS (S3 + DynamoDB для блокировок):

terraform {
  backend "s3" {
    bucket         = "your-unique-terraform-state-bucket"
    key            = "global/s3/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"
  }
}

Предварительно создайте S3 bucket с включенным versioning и DynamoDB таблицу с первичным ключом LockID (тип String).

Для Yandex Cloud можно использовать Object Storage (совместимый с S3 API):

terraform {
  backend "s3" {
    endpoint   = "storage.yandexcloud.net"
    bucket     = "your-terraform-state-bucket"
    key        = "terraform.tfstate"
    region     = "ru-central1"
    access_key = "..."
    secret_key = "..."
    skip_region_validation      = true
    skip_credentials_validation = true
  }
}

После настройки backend выполните terraform init -reconfigure для миграции state.

Стратегия работы в Git: изолируем среды и контролируем изменения

Рекомендуемая структура репозитория разделяет код и конфигурацию сред:

terraform-repo/
├── modules/          # Переиспользуемые модули (vpc, compute)
│   ├── compute
│   └── vpc
├── environments/
│   ├── dev/         # Конфигурация dev-среды
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/        # Конфигурация prod-среды
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
└── ansible/         # Роли и плейбуки Ansible

В environments/dev/main.tf вызываются модули из ../modules с параметрами для разработки (меньшие инстансы). В prod/ — те же модули, но с другими переменными (terraform.tfvars). Это обеспечивает согласованность и контроль. Все изменения в инфраструктуре проходят через pull/merge request с обязательным ревью кода и запуском terraform plan.

Интеграция в CI/CD: автоматизируем plan и apply

Базовый пайплайн в GitLab CI может выглядеть так:

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TF_DIR: "environments/$ENVIRONMENT"

before_script:
  - cd $TF_DIR
  - terraform init

terraform_validate:
  stage: validate
  script:
    - terraform validate

terraform_plan:
  stage: plan
  script:
    - terraform plan -out=plan.tfplan
  artifacts:
    paths:
      - $TF_DIR/plan.tfplan
  only:
    - merge_requests

terraform_apply:
  stage: apply
  script:
    - terraform apply -auto-approve plan.tfplan
  when: manual # Критично для продакшн! Только ручной запуск.
  only:
    - main

Учетные данные провайдера (AWS keys, Yandex SA key) должны храниться в защищенных переменных CI/CD, а не в коде. Автоматический apply допустим только для некритичных сред (dev). Для production обязательна manual job после ревью плана.

Для комплексного подхода к управлению инфраструктурой и приложениями, изучите сравнение методологий в нашем материале: GitOps и Infrastructure as Code (IaC): Практическое сравнение подходов для DevOps в 2026.

Итоги и дальнейшие шаги: от базового пайплайна к продвинутой автоматизации

Выстроенный workflow «Terraform создает -> Ansible настраивает» обеспечивает полную автоматизацию, воспроизводимость и безопасность инфраструктуры. Вы получили готовые строительные блоки для AWS и Yandex Cloud, механизм их интеграции через Dynamic Inventory и best practices для командной работы.

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

  1. Тестирование: Внедрить Terratest на Go для модулей Terraform (проверка создания/удаления ресурсов) и Molecule для ролей Ansible (проверка конвергенции и идемпотентности).
  2. Углубление в Ansible: Использовать Ansible Vault для шифрования секретов, освоить динамические инвентари для облачных провайдеров (aws_ec2, yandex_compute).
  3. Переход к оркестрации: В сценариях с Kubernetes Terraform остается для создания самого кластера (EKS, Managed Kubernetes), но настройка workloads часто переходит к Helm и GitOps-инструментам (Flux, ArgoCD). Ansible может использоваться для bootstrap-задач.
  4. Мониторинг и управление: Добавить в пайплайн автоматическое создание алертов и дашбордов (например, используя Terraform модули для AWS CloudWatch или Yandex Monitoring).

Начинайте с малого: автоматизируйте одну среду и один сервис, отладьте пайплайн, а затем итеративно расширяйте охват. Официальная документация остается ключевым источником: Terraform Docs, Ansible Docs. Для отработки навыков работы с контейнерами, которые часто становятся целевой средой для развертывания, обратитесь к руководству: Docker Compose для сборки и управления многоконтейнерными приложениями.

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

Поделиться:
Сохранить гайд? В закладки браузера