← Назад к вопросам

Написание Terraform модуля для AWS EC2 с EBS

2.0 Middle🔥 201 комментариев
#Облачные технологии

Условие

Напишите Terraform модуль для создания инфраструктуры в AWS:

Требования

  1. EC2 инстанс типа t3.micro
  2. EBS том размером 20GB, подключенный к инстансу
  3. Security Group с открытыми портами 22 (SSH) и 80 (HTTP)
  4. Elastic IP, привязанный к инстансу
  5. Модуль должен принимать переменные: region, instance_type, volume_size
  6. Outputs: public_ip, instance_id

Дополнительные вопросы

  • Как организовать state файл для командной работы?
  • Что такое terraform plan и terraform apply?
  • Как откатить изменения в Terraform?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Структура Terraform модуля

Файловая структура

ec2-module/
├── main.tf           # Основные ресурсы
├── variables.tf      # Объявление переменных
├── outputs.tf        # Выходные значения
├── terraform.tf      # Конфигурация провайдера и backend
└── terraform.tfvars  # Значения переменных (gitignore!)

2. main.tf - Основные ресурсы

# Security Group
resource "aws_security_group" "web_sg" {
  name        = "web-security-group-${var.environment}"
  description = "Security group for web server"
  vpc_id      = var.vpc_id

  tags = {
    Name = "web-sg"
  }
}

# Ingress rules
resource "aws_vpc_security_group_ingress_rule" "ssh" {
  security_group_id = aws_security_group.web_sg.id
  description       = "SSH access"

  from_port   = 22
  to_port     = 22
  ip_protocol = "tcp"
  cidr_ipv4   = var.ssh_cidr  # например "0.0.0.0/0" или "10.0.0.0/8"

  tags = {
    Name = "ssh-rule"
  }
}

resource "aws_vpc_security_group_ingress_rule" "http" {
  security_group_id = aws_security_group.web_sg.id
  description       = "HTTP access"

  from_port   = 80
  to_port     = 80
  ip_protocol = "tcp"
  cidr_ipv4   = "0.0.0.0/0"

  tags = {
    Name = "http-rule"
  }
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.web_sg.id
  description       = "HTTPS access"

  from_port   = 443
  to_port     = 443
  ip_protocol = "tcp"
  cidr_ipv4   = "0.0.0.0/0"

  tags = {
    Name = "https-rule"
  }
}

# Egress rule (разрешить весь исходящий трафик)
resource "aws_vpc_security_group_egress_rule" "all" {
  security_group_id = aws_security_group.web_sg.id
  description       = "Allow all outbound traffic"

  from_port   = 0
  to_port     = 0
  ip_protocol = "-1"  # -1 означает все протоколы
  cidr_ipv4   = "0.0.0.0/0"

  tags = {
    Name = "allow-all-egress"
  }
}

# EC2 Instance
resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.web_sg.id]
  key_name               = var.key_name

  root_block_device {
    volume_type           = "gp3"
    volume_size           = var.root_volume_size
    delete_on_termination = true
    encrypted             = true

    tags = {
      Name = "root-volume"
    }
  }

  user_data = base64encode(var.user_data_script)

  monitoring = true

  tags = {
    Name        = "web-server-${var.environment}"
    Environment = var.environment
  }

  depends_on = [aws_security_group.web_sg]
}

# EBS Volume
resource "aws_ebs_volume" "data_volume" {
  availability_zone = aws_instance.web.availability_zone
  size              = var.data_volume_size
  type              = "gp3"
  iops              = 3000
  throughput        = 125
  encrypted         = true
  kms_key_id        = var.kms_key_id

  tags = {
    Name = "data-volume"
  }
}

# Attach EBS Volume to Instance
resource "aws_volume_attachment" "data_attach" {
  device_name = "/dev/sdf"
  volume_id   = aws_ebs_volume.data_volume.id
  instance_id = aws_instance.web.id
}

# Elastic IP
resource "aws_eip" "web_eip" {
  instance = aws_instance.web.id
  domain   = "vpc"

  depends_on = [aws_instance.web]

  tags = {
    Name = "web-eip"
  }
}

# Data source для получения последнего AMI Amazon Linux 2
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }
}

3. variables.tf - Входные переменные

variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"

  validation {
    condition     = can(regex("^[a-z]{2}-[a-z]+-\\d{1}$", var.region))
    error_message = "Region must be valid AWS region format."
  }
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be one of: dev, staging, prod."
  }
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"

  validation {
    condition     = can(regex("^t[23]\\.micro$", var.instance_type))
    error_message = "Instance type must be t3.micro or t2.micro."
  }
}

variable "root_volume_size" {
  description = "Root volume size in GB"
  type        = number
  default     = 20

  validation {
    condition     = var.root_volume_size >= 20 && var.root_volume_size <= 100
    error_message = "Root volume size must be between 20 and 100 GB."
  }
}

variable "data_volume_size" {
  description = "Data volume size in GB"
  type        = number
  default     = 20

  validation {
    condition     = var.data_volume_size >= 10 && var.data_volume_size <= 1000
    error_message = "Data volume size must be between 10 and 1000 GB."
  }
}

variable "vpc_id" {
  description = "VPC ID where to create resources"
  type        = string
  nullable    = false
}

variable "subnet_id" {
  description = "Subnet ID for EC2 instance"
  type        = string
  nullable    = false
}

variable "key_name" {
  description = "EC2 Key Pair name for SSH access"
  type        = string
  nullable    = false
}

variable "ssh_cidr" {
  description = "CIDR block for SSH access"
  type        = string
  default     = "0.0.0.0/0"

  validation {
    condition     = can(cidrhost(var.ssh_cidr, 0))
    error_message = "SSH CIDR must be valid CIDR notation."
  }
}

variable "user_data_script" {
  description = "User data script for EC2 initialization"
  type        = string
  default     = ""
}

variable "kms_key_id" {
  description = "KMS key ID for EBS encryption"
  type        = string
  default     = null
}

4. outputs.tf - Выходные значения

output "instance_id" {
  description = "EC2 instance ID"
  value       = aws_instance.web.id
}

output "instance_arn" {
  description = "EC2 instance ARN"
  value       = aws_instance.web.arn
}

output "public_ip" {
  description = "Elastic IP address"
  value       = aws_eip.web_eip.public_ip
}

output "private_ip" {
  description = "Private IP address"
  value       = aws_instance.web.private_ip
}

output "security_group_id" {
  description = "Security Group ID"
  value       = aws_security_group.web_sg.id
}

output "data_volume_id" {
  description = "Data volume ID"
  value       = aws_ebs_volume.data_volume.id
}

output "data_volume_attachment_id" {
  description = "Volume attachment ID"
  value       = aws_volume_attachment.data_attach.id
}

output "connection_string" {
  description = "SSH connection string"
  value       = "ssh -i /path/to/key.pem ec2-user@${aws_eip.web_eip.public_ip}"
}

5. terraform.tf - Конфигурация провайдера и Backend

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # Backend для хранения state в S3 (для командной работы)
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/ec2/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

provider "aws" {
  region = var.region

  default_tags {
    tags = {
      Terraform   = "true"
      Project     = "web-server"
      Environment = var.environment
      ManagedBy   = "Terraform"
    }
  }
}

6. terraform.tfvars - Значения переменных

# Создайте файл terraform.tfvars (НЕ коммитьте в git!)
region           = "us-east-1"
environment      = "prod"
instance_type    = "t3.micro"
root_volume_size = 20
data_volume_size = 20
vpc_id           = "vpc-xxxxxxxxx"
subnet_id        = "subnet-xxxxxxxxx"
key_name         = "my-keypair"
ssh_cidr         = "0.0.0.0/0"

7. .gitignore для Terraform

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
*.tfvars.json

# Ignore override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Ignore CLI configuration files
.terraformrc
terraform.rc

# Ignore plan files
*.tfplan

# IDE
.vscode/
.idea/
*.swp

8. State файл для командной работы

Проблема: локальный state

# ❌ ПЛОХО - state на локальной машине
# Если несколько разработчиков работают на одной инфраструктуре:
ls -la terraform.tfstate
# terraform.tfstate находится только на вашей машине!
# Коллега не видит изменения
# Конфликты при одновременной работе

Решение: Remote State в S3

# terraform.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"      # Название S3 bucket
    key            = "prod/ec2/terraform.tfstate"  # Путь к state файлу
    region         = "us-east-1"
    encrypt        = true                       # Шифрование в S3
    dynamodb_table = "terraform-locks"         # Таблица для блокировок
  }
}

Создание S3 bucket и DynamoDB таблицы

# Это нужно создать один раз, отдельно
aws s3api create-bucket \
  --bucket my-terraform-state \
  --region us-east-1

# Включить versioning
aws s3api put-bucket-versioning \
  --bucket my-terraform-state \
  --versioning-configuration Status=Enabled

# Включить шифрование
aws s3api put-bucket-encryption \
  --bucket my-terraform-state \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "AES256"
        }
      }
    ]
  }'

# Заблокировать публичный доступ
aws s3api put-public-access-block \
  --bucket my-terraform-state \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Создать DynamoDB таблицу для блокировок
aws dynamodb create-table \
  --table-name terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5

Инициализация с remote state

# Инициализация (загрузит state из S3)
terraform init

# После инициализации state будет локально и в S3
terraform state list  # Просмотр всех ресурсов

9. Terraform plan и terraform apply

terraform plan - Просмотр изменений

# Показывает, что Terraform собирается делать (без реальных изменений)
terraform plan

# Вывод
# + aws_instance.web               # Создаст новый инстанс
# + aws_ebs_volume.data_volume     # Создаст новый том
# + aws_eip.web_eip               # Создаст Elastic IP
# + aws_security_group.web_sg      # Создаст Security Group

# Сохранение плана в файл
terraform plan -out=tfplan

terraform apply - Применение изменений

# Применяет изменения (интерактивно просит подтверждение)
terraform apply

# Применяет без подтверждения (использовать осторожно!)
terraform apply -auto-approve

# Применяет сохраненный план (безопаснее)
terraform apply tfplan

Процесс plan + apply

# Шаг 1: Проверяем что изменится
terraform plan -out=tfplan

# Шаг 2: Смотрим результат
cat tfplan  # (нечитаемо, это бинарный формат)

# Шаг 3: Применяем если все хорошо
terraform apply tfplan

# Шаг 4: Проверяем результаты
terraform state show aws_instance.web
terraform output public_ip

10. Откат изменений

Вариант 1: terraform destroy

# Удаляет ВСЕ ресурсы, описанные в config
terraform destroy

# Предварительный просмотр
terraform plan -destroy

# Удаление без подтверждения
terraform destroy -auto-approve

Вариант 2: Откат state к предыдущей версии

# Просмотр истории state
aws s3api list-object-versions --bucket my-terraform-state --prefix "prod/ec2/"

# Восстановление версии из S3
aws s3api get-object \
  --bucket my-terraform-state \
  --key "prod/ec2/terraform.tfstate" \
  --version-id "VersionId12345" \
  terraform.tfstate.backup

# Замена текущего state
cp terraform.tfstate.backup terraform.tfstate
terraform apply

Вариант 3: Откат отдельного ресурса

# Удаление конкретного ресурса из state (он остается в AWS, но Terraform его не видит)
terraform state rm aws_ebs_volume.data_volume

# Пересоздание ресурса
terraform apply

Вариант 4: terraform taint (пересоздание ресурса)

# Помечает ресурс как "грязный" - будет пересоздан при apply
terraform taint aws_instance.web

# При следующем apply инстанс будет уничтожен и создан заново
terraform apply

# Отмена пометки
terraform untaint aws_instance.web

11. Лучшие практики

Структура для production

terraform/
├── environments/
│   ├── dev/
│   │   ├── terraform.tfvars
│   │   └── main.tf (вызов модуля)
│   ├── staging/
│   │   └── terraform.tfvars
│   └── prod/
│       └── terraform.tfvars
├── modules/
│   └── ec2/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── terraform.tf
└── shared/
    ├── backend.tf
    └── versions.tf

Команды для CI/CD

#!/bin/bash
set -e

# Инициализация
terraform init

# Валидация
terraform validate

# Форматирование
terraform fmt -check

# План
terraform plan -out=tfplan

# Применение (только после approval)
terraform apply tfplan
Написание Terraform модуля для AWS EC2 с EBS | PrepBro