Что такое Terraform
Terraform -- это инструмент от HashiCorp для декларативного управления инфраструктурой. Вы описываете желаемое состояние ресурсов в файлах .tf, а Terraform определяет, какие действия выполнить, чтобы привести реальную инфраструктуру к этому состоянию.
Terraform Workflow:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ .tf files │ │ Terraform │ │ Cloud API │
│ │────►│ Engine │────►│ (AWS, GCP, │
│ HCL Code │ │ │ │ Azure...) │
│ │ │ Plan → Apply│ │ │
└──────────────┘ └──────┬───────┘ └──────────────┘
│
┌──────▼───────┐
│ terraform │
│ .tfstate │
│ │
│ Current state│
│ of resources │
└──────────────┘
HCL синтаксис
HCL (HashiCorp Configuration Language) -- специализированный язык для описания инфраструктуры. Он проще, чем JSON/YAML для человека, но при этом поддерживает вычисления и логику.
Blocks (Блоки)
Основная единица HCL -- блок. Каждый блок имеет тип и (иногда) метки:
# Block structure: type "label1" "label2" { ... }
# Resource block - 2 labels (type and name)
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
}
# Variable block - 1 label (name)
variable "region" {
type = string
default = "eu-central-1"
}
# Provider block - 1 label (name)
provider "aws" {
region = var.region
}
# Terraform block - no labels
terraform {
required_version = ">= 1.9"
}
# Locals block - no labels
locals {
common_tags = {
Project = "my-app"
ManagedBy = "terraform"
}
}
Arguments (Аргументы)
Аргументы -- это key = value пары внутри блоков:
resource "aws_instance" "web" {
# Simple arguments
ami = "ami-0c55b159cbfafe1f0" # string
instance_type = "t3.medium" # string
monitoring = true # bool
count = 3 # number
# Nested block (not an argument!)
root_block_device {
volume_size = 50
volume_type = "gp3"
encrypted = true
}
# Map argument
tags = {
Name = "web-server"
Env = "production"
}
}
Expressions (Выражения)
HCL поддерживает мощные выражения:
locals {
# String interpolation
bucket_name = "app-${var.environment}-${var.region}"
# Conditional expression
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
# List comprehension with for
public_subnet_ids = [for s in aws_subnet.public : s.id]
# Map comprehension
instance_tags = {
for idx, inst in aws_instance.web :
inst.id => inst.private_ip
}
# Splat expression (shorthand for list comprehension)
all_instance_ids = aws_instance.web[*].id
# Function calls
config = jsondecode(file("config.json"))
# Arithmetic
total_storage = var.base_storage * var.instance_count
}
Типы данных
variable "examples" {
# Primitive types
# string = "hello"
# number = 42
# bool = true
# Collection types
# list(string) = ["a", "b", "c"]
# set(string) = ["a", "b", "c"] # no duplicates, unordered
# map(string) = { key1 = "val1", key2 = "val2" }
# Structural types
# object({ name = string, age = number })
# tuple([string, number, bool])
# Special
# any = accepts any type
# null = absence of a value
}
# Object type example
variable "database_config" {
type = object({
engine = string
instance_class = string
storage_gb = number
multi_az = bool
backup_days = number
})
default = {
engine = "postgres"
instance_class = "db.t3.medium"
storage_gb = 100
multi_az = false
backup_days = 7
}
}
Providers
Providers -- это плагины, которые Terraform использует для взаимодействия с API облачных платформ и других сервисов.
┌──────────────────────────────────────────────────────┐
│ Terraform Core │
│ (HCL parser, plan, apply) │
├──────┬───────┬──────┬──────────┬─────────────────────┤
│ │ │ │ │ │
│ ┌───▼──┐ ┌──▼──┐ ┌─▼───┐ ┌───▼────┐ ┌────────────┐│
│ │ AWS │ │ GCP │ │Azure│ │Docker │ │Kubernetes ││
│ │Provid│ │Provi│ │Provi│ │Provider│ │Provider ││
│ │er │ │der │ │der │ │ │ │ ││
│ └───┬──┘ └──┬──┘ └─┬───┘ └───┬────┘ └─────┬──────┘│
│ │ │ │ │ │ │
└──────┼───────┼──────┼──────────┼─────────────┼───────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
AWS API GCP API Azure API Docker API K8s API
Конфигурация провайдеров
terraform {
required_providers {
# AWS provider
aws = {
source = "hashicorp/aws"
version = "~> 5.80" # >= 5.80.0, < 6.0.0
}
# Docker provider (for local development)
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
# Kubernetes provider
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.35"
}
# Random provider (utility)
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
# AWS provider configuration
provider "aws" {
region = "eu-central-1"
# Authentication (best practice: use environment variables)
# AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
# Or IAM role / SSO profile
default_tags {
tags = {
ManagedBy = "terraform"
Project = var.project_name
}
}
}
# Multiple provider configurations (aliases)
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
# Use aliased provider for specific resources
resource "aws_acm_certificate" "cert" {
provider = aws.us_east # CloudFront requires certs in us-east-1
domain_name = "example.com"
}
# Docker provider
provider "docker" {
host = "unix:///var/run/docker.sock"
}
# Kubernetes provider
provider "kubernetes" {
config_path = "~/.kube/config"
context = "my-cluster"
}
Версионирование провайдеров
# Version constraints
version = "5.80.0" # Exact version
version = ">= 5.80" # Minimum version
version = "~> 5.80" # >= 5.80.0, < 6.0.0 (pessimistic)
version = ">= 5.0, < 6.0" # Range
# Lock file: .terraform.lock.hcl
# Generated by terraform init, commit to git!
# Ensures all team members use same provider versions
Resources
Resources -- главные строительные блоки Terraform. Каждый ресурс соответствует объекту в облаке (сервер, сеть, база данных и т.д.).
Жизненный цикл ресурса
Resource Lifecycle:
┌─────────┐
│ Code │
│ Written │
└────┬────┘
│
terraform plan
│
┌────▼────┐
┌───── │ Plan │ ─────┐
│ │ Review │ │
│ └────┬────┘ │
No changes │ Changes needed
│ terraform apply │
│ │ │
│ ┌────▼────┐ │
│ │ │ │
│ │ CREATE │◄─────┘ (new resource)
│ │ UPDATE │◄───── (changed attribute)
│ │ DESTROY │◄───── (removed from code)
│ │ REPLACE │◄───── (force new: immutable attr change)
│ │ │
│ └────┬────┘
│ │
│ ┌────▼────┐
└─────►│ State │
│ Updated │
└─────────┘
Основные аргументы ресурсов
# Basic resource
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
subnet_id = aws_subnet.private[0].id
tags = {
Name = "web-server"
}
}
# depends_on - explicit dependency (use only when implicit isn't enough)
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
# Terraform usually detects dependencies automatically
# Use depends_on only for hidden dependencies
depends_on = [aws_iam_role_policy.app_policy]
}
# lifecycle - control resource behavior
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
lifecycle {
# Create new before destroying old (zero-downtime)
create_before_destroy = true
# Prevent accidental destruction
prevent_destroy = true
# Ignore changes made outside Terraform
ignore_changes = [
tags["LastModified"],
ami, # Don't update AMI automatically
]
# Custom condition
precondition {
condition = var.instance_type != "t3.nano"
error_message = "Instance type t3.nano is too small for production."
}
}
}
# timeouts - how long to wait for operations
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = "db.t3.medium"
timeouts {
create = "60m" # RDS can take a while
update = "90m"
delete = "30m"
}
}
count и for_each
Два способа создать несколько экземпляров ресурса:
# count - for simple numeric iteration
resource "aws_instance" "web" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = aws_subnet.private[count.index].id
tags = {
Name = "web-${count.index + 1}" # web-1, web-2, web-3
}
}
# Reference: aws_instance.web[0], aws_instance.web[1], ...
# All IDs: aws_instance.web[*].id
# Problem with count: removing item from middle shifts indices!
# If count was 3 and you remove web-2, web-3 gets recreated as web-2
# ---
# for_each - for maps and sets (preferred for most cases)
resource "aws_instance" "web" {
for_each = {
api = { type = "t3.large", subnet = "private-1" }
worker = { type = "t3.medium", subnet = "private-2" }
cron = { type = "t3.small", subnet = "private-1" }
}
ami = "ami-0c55b159cbfafe1f0"
instance_type = each.value.type
subnet_id = aws_subnet.main[each.value.subnet].id
tags = {
Name = "app-${each.key}" # app-api, app-worker, app-cron
Role = each.key
}
}
# Reference: aws_instance.web["api"], aws_instance.web["worker"]
# All IDs: values(aws_instance.web)[*].id
# for_each with set of strings
variable "availability_zones" {
type = set(string)
default = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
}
resource "aws_subnet" "private" {
for_each = var.availability_zones
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, index(tolist(var.availability_zones), each.value))
availability_zone = each.value
tags = {
Name = "private-${each.value}"
}
}
Когда использовать count vs for_each:
| Сценарий | count | for_each |
|---|---|---|
| Простое количество (N серверов) | Да | Избыточно |
| Разные параметры для каждого | Нет | Да |
| Условное создание (0 или 1) | count = var.enabled ? 1 : 0 |
Можно, но count проще |
| Удаление из середины списка | Проблема (сдвиг индексов) | Нет проблемы |
Data Sources
Data Sources позволяют читать информацию о существующих ресурсах, которыми Terraform не управляет.
# Read existing VPC by tag
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["production-vpc"]
}
}
# Read latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "state"
values = ["available"]
}
}
# Use data source in resource
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.medium"
subnet_id = data.aws_vpc.existing.id # Use existing VPC
}
# Read current AWS account info
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
# Read IAM policy document (common pattern)
data "aws_iam_policy_document" "app_policy" {
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
]
resources = [
"${aws_s3_bucket.app.arn}/*",
]
}
statement {
effect = "Allow"
actions = [
"secretsmanager:GetSecretValue",
]
resources = [
aws_secretsmanager_secret.db_password.arn,
]
}
}
resource "aws_iam_policy" "app" {
name = "app-policy"
policy = data.aws_iam_policy_document.app_policy.json
}
# Read file
data "local_file" "user_data" {
filename = "${path.module}/scripts/user_data.sh"
}
# Template rendering
data "template_file" "nginx_config" {
template = file("${path.module}/templates/nginx.conf.tpl")
vars = {
server_name = var.domain_name
upstream = aws_lb.app.dns_name
}
}
Variables (Переменные)
Input Variables
# variables.tf
# Simple string variable
variable "environment" {
description = "Deployment environment (dev, staging, production)"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Environment must be dev, staging, or production."
}
}
# Number with validation
variable "instance_count" {
description = "Number of web server instances"
type = number
default = 2
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
# Boolean
variable "enable_monitoring" {
description = "Enable CloudWatch detailed monitoring"
type = bool
default = false
}
# List of strings
variable "allowed_cidrs" {
description = "CIDR blocks allowed to access the application"
type = list(string)
default = ["10.0.0.0/8"]
}
# Map
variable "instance_types" {
description = "Instance types per environment"
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
production = "t3.large"
}
}
# Complex object
variable "database" {
description = "Database configuration"
type = object({
engine = string
engine_version = string
instance_class = string
allocated_storage = number
multi_az = bool
backup_retention = number
})
default = {
engine = "postgres"
engine_version = "18.1"
instance_class = "db.t3.medium"
allocated_storage = 50
multi_az = false
backup_retention = 7
}
}
# Sensitive variable (won't show in plan output)
variable "db_password" {
description = "Database master password"
type = string
sensitive = true
validation {
condition = length(var.db_password) >= 16
error_message = "Database password must be at least 16 characters."
}
}
# Nullable variable
variable "custom_domain" {
description = "Custom domain name (optional)"
type = string
default = null
nullable = true
}
Приоритет задания переменных
Terraform читает значения переменных из разных источников. Приоритет (от низшего к высшему):
Priority (lowest → highest):
┌──────────────────────────────────────────────────┐
│ 1. default в variable block (lowest) │
│ 2. terraform.tfvars файл │
│ 3. *.auto.tfvars файлы │
│ 4. -var-file=custom.tfvars │
│ 5. TF_VAR_name environment variable │
│ 6. -var="name=value" в командной строке (highest)│
└──────────────────────────────────────────────────┘
# Ways to pass variables:
# 1. Default value in variable block
# (already in variables.tf)
# 2. terraform.tfvars (auto-loaded)
echo 'environment = "production"' > terraform.tfvars
# 3. Named .tfvars file
terraform apply -var-file="prod.tfvars"
# 4. Environment variable
export TF_VAR_db_password="super-secret-password"
terraform apply
# 5. Command line
terraform apply -var="environment=production" -var="instance_count=3"
Пример prod.tfvars:
# environments/prod.tfvars
environment = "production"
instance_count = 3
enable_monitoring = true
allowed_cidrs = ["10.0.0.0/8", "172.16.0.0/12"]
database = {
engine = "postgres"
engine_version = "18.1"
instance_class = "db.r6g.large"
allocated_storage = 200
multi_az = true
backup_retention = 30
}
Outputs
Outputs экспортируют значения из Terraform -- ID ресурсов, IP-адреса, DNS-имена. Полезны для передачи данных между модулями и для отображения после apply.
# outputs.tf
# Simple output
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
# Output used by other modules
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
# Sensitive output (hidden in console)
output "db_connection_string" {
description = "Database connection string"
value = "postgresql://${aws_db_instance.main.username}:${var.db_password}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
sensitive = true
}
# Conditional output
output "load_balancer_dns" {
description = "DNS name of the load balancer"
value = var.enable_lb ? aws_lb.main[0].dns_name : null
}
# Complex output
output "instances" {
description = "Map of instance names to their IPs"
value = {
for key, inst in aws_instance.web :
key => {
id = inst.id
private_ip = inst.private_ip
public_ip = inst.public_ip
}
}
}
# View outputs after apply
terraform output
terraform output vpc_id
terraform output -json # Machine-readable format
# Use in scripts
VPC_ID=$(terraform output -raw vpc_id)
Locals
Locals -- вычисляемые значения, которые упрощают конфигурацию и устраняют дублирование.
locals {
# Common tags for all resources
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
Team = "platform"
}
# Computed name prefix
name_prefix = "${var.project_name}-${var.environment}"
# Conditional logic
is_production = var.environment == "production"
instance_type = local.is_production ? "t3.large" : "t3.micro"
# Transform data
azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
# Merge maps
resource_tags = merge(local.common_tags, {
CreatedAt = timestamp()
})
# Flatten nested structures
subnet_configs = flatten([
for az in local.azs : [
{
name = "public-${az}"
az = az
type = "public"
},
{
name = "private-${az}"
az = az
type = "private"
}
]
])
}
# Using locals in resources
resource "aws_instance" "web" {
instance_type = local.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
Role = "web-server"
})
}
Terraform Workflow
Основные команды
Terraform Workflow:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ terraform │ │ terraform │ │ terraform │
│ init │────►│ plan │────►│ apply │
│ │ │ │ │ │
│ Download │ │ Show what │ │ Execute │
│ providers │ │ will change │ │ changes │
│ Init backend│ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ┌─────────────┐ │
│ │ terraform │ │
│ │ destroy │◄───────────┘
│ │ │ (when needed)
│ │ Remove all │
│ │ resources │
│ └─────────────┘
│
▼
┌─────────────┐
│ .terraform/ │ Downloaded providers
│ .terraform │ and modules cached
│ .lock.hcl │ Version lock file
└─────────────┘
terraform init
# Initialize working directory
terraform init
# Output:
# Initializing the backend...
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.80"...
# - Installing hashicorp/aws v5.82.0...
# Terraform has been successfully initialized!
# Upgrade providers to latest matching versions
terraform init -upgrade
# Reconfigure backend
terraform init -reconfigure
# Generated files:
# .terraform/ - downloaded providers and modules
# .terraform.lock.hcl - lock file (COMMIT to git!)
terraform plan
# Show what will change
terraform plan
# Save plan to file (for later apply)
terraform plan -out=tfplan
# Plan with specific variables
terraform plan -var-file="prod.tfvars"
# Plan for destruction
terraform plan -destroy
# Target specific resource
terraform plan -target=aws_instance.web
# Output:
# Terraform will perform the following actions:
#
# # aws_instance.web will be created
# + resource "aws_instance" "web" {
# + ami = "ami-0c55b159cbfafe1f0"
# + instance_type = "t3.medium"
# + id = (known after apply)
# + public_ip = (known after apply)
# }
#
# Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply
# Apply changes (will show plan and ask for confirmation)
terraform apply
# Apply saved plan (no confirmation needed)
terraform apply tfplan
# Apply with auto-approve (CI/CD only!)
terraform apply -auto-approve
# Apply with variables
terraform apply -var-file="prod.tfvars"
# Apply specific resource only
terraform apply -target=aws_instance.web
terraform destroy
# Destroy all resources
terraform destroy
# Destroy specific resource
terraform destroy -target=aws_instance.web
# Destroy with auto-approve (dangerous!)
terraform destroy -auto-approve
Вспомогательные команды
# Format code
terraform fmt
terraform fmt -recursive # All subdirectories
terraform fmt -check # Check only (for CI)
# Validate configuration
terraform validate
# Show current state
terraform show
# List resources in state
terraform state list
# Show specific resource in state
terraform state show aws_instance.web
# Refresh state (sync with real infrastructure)
terraform refresh # Deprecated, use:
terraform apply -refresh-only
# Console for testing expressions
terraform console
> var.environment
"production"
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
Provisioners (и почему их НЕ стоит использовать)
Provisioners выполняют скрипты на созданном ресурсе. Однако HashiCorp рекомендует избегать их в пользу cloud-init, Packer, или Ansible.
# ❌ Anti-pattern: provisioner
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
# DON'T DO THIS in production
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx php8.4-fpm",
"sudo systemctl start nginx",
]
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
# File provisioner
provisioner "file" {
source = "configs/nginx.conf"
destination = "/tmp/nginx.conf"
}
# Local exec (runs on YOUR machine, not the server)
provisioner "local-exec" {
command = "echo ${self.private_ip} >> inventory.txt"
}
}
Почему provisioners -- плохая практика:
| Проблема | Описание |
|---|---|
| Не декларативные | Terraform не знает, что скрипт сделал |
| Не идемпотентные | Повторный запуск может сломать |
| Нет drift detection | Terraform не отследит изменения |
| Зависимость от SSH | Требует открытый порт 22 |
| Медленные | Выполняются последовательно |
Правильные альтернативы:
# ✅ User Data (cloud-init) - runs on first boot
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx php8.4-fpm
systemctl start nginx
EOF
user_data_replace_on_change = true
}
# ✅ Pre-baked AMI with Packer (best practice)
# Build AMI with all software pre-installed
# Then reference it in Terraform
data "aws_ami" "app" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["app-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.app.id
instance_type = "t3.medium"
# No provisioner needed - everything is in the AMI
}
Практический пример: VPC + EC2 для PHP/Go приложения
Полный пример создания базовой инфраструктуры для веб-приложения:
Структура файлов
infrastructure/
├── main.tf # Provider and backend
├── variables.tf # Input variables
├── outputs.tf # Output values
├── networking.tf # VPC, subnets, security groups
├── compute.tf # EC2 instances
├── dev.tfvars # Dev environment values
└── prod.tfvars # Production values
main.tf
# main.tf
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.80"
}
}
backend "s3" {
bucket = "myapp-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
}
}
variables.tf
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "eu-central-1"
}
variable "project_name" {
description = "Project name used for resource naming"
type = string
default = "myapp"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "instance_count" {
description = "Number of EC2 instances"
type = number
default = 1
}
variable "ssh_allowed_cidrs" {
description = "CIDR blocks allowed for SSH access"
type = list(string)
default = []
}
networking.tf
# networking.tf
locals {
azs = ["${var.aws_region}a", "${var.aws_region}b"]
name_prefix = "${var.project_name}-${var.environment}"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${local.name_prefix}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${local.name_prefix}-igw"
}
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${local.name_prefix}-public-${local.azs[count.index]}"
Type = "public"
}
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = local.azs[count.index]
tags = {
Name = "${local.name_prefix}-private-${local.azs[count.index]}"
Type = "private"
}
}
# NAT Gateway (for private subnets internet access)
resource "aws_eip" "nat" {
count = var.environment == "prod" ? length(local.azs) : 1
domain = "vpc"
tags = {
Name = "${local.name_prefix}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = var.environment == "prod" ? length(local.azs) : 1
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${local.name_prefix}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${local.name_prefix}-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(local.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[var.environment == "prod" ? count.index : 0].id
}
tags = {
Name = "${local.name_prefix}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# Security Groups
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
dynamic "ingress" {
for_each = length(var.ssh_allowed_cidrs) > 0 ? [1] : []
content {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.ssh_allowed_cidrs
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.name_prefix}-web-sg"
}
}
compute.tf
# compute.tf
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "state"
values = ["available"]
}
}
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.private[count.index % length(local.azs)].id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
# Install Docker for PHP/Go containers
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker
# Pull and run application
docker pull ${var.project_name}/api:latest
docker run -d -p 80:80 ${var.project_name}/api:latest
EOF
root_block_device {
volume_size = 30
volume_type = "gp3"
encrypted = true
}
tags = {
Name = "${local.name_prefix}-web-${count.index + 1}"
Role = "web-server"
}
}
outputs.tf
# outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
output "web_security_group_id" {
description = "Web security group ID"
value = aws_security_group.web.id
}
output "instance_ids" {
description = "EC2 instance IDs"
value = aws_instance.web[*].id
}
output "instance_private_ips" {
description = "Private IPs of web instances"
value = aws_instance.web[*].private_ip
}
tfvars файлы
# dev.tfvars
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
ssh_allowed_cidrs = ["YOUR_IP/32"]
# prod.tfvars
environment = "prod"
instance_type = "t3.large"
instance_count = 3
vpc_cidr = "10.1.0.0/16"
ssh_allowed_cidrs = [] # No SSH in production
Выполнение
# Initialize
terraform init
# Plan for dev
terraform plan -var-file="dev.tfvars"
# Apply dev
terraform apply -var-file="dev.tfvars"
# Plan for prod
terraform plan -var-file="prod.tfvars"
# Apply prod (with care!)
terraform apply -var-file="prod.tfvars"
Встроенные функции
Terraform предоставляет множество полезных функций:
locals {
# String functions
upper_env = upper(var.environment) # "PRODUCTION"
name_parts = split("-", "my-app-server") # ["my", "app", "server"]
joined = join(", ", var.allowed_cidrs) # "10.0.0.0/8, 172.16.0.0/12"
replaced = replace("hello-world", "-", "_") # "hello_world"
trimmed = trimspace(" hello ") # "hello"
# Numeric functions
max_count = max(var.min_count, 3) # Returns larger
min_count = min(var.max_count, 10) # Returns smaller
ceil_val = ceil(4.3) # 5
# Collection functions
flat_list = flatten([["a", "b"], ["c"]]) # ["a", "b", "c"]
unique = distinct(["a", "b", "a"]) # ["a", "b"]
merged = merge({a = 1}, {b = 2}) # {a = 1, b = 2}
has_key = contains(keys(var.tags), "Name") # true/false
length_val = length(var.subnets) # 3
# Network functions
subnet_1 = cidrsubnet("10.0.0.0/16", 8, 1) # "10.0.1.0/24"
subnet_2 = cidrsubnet("10.0.0.0/16", 8, 2) # "10.0.2.0/24"
# Encoding functions
encoded = base64encode("hello") # "aGVsbG8="
json = jsonencode({key = "value"}) # '{"key":"value"}'
yaml = yamlencode({key = "value"}) # "key: value\n"
# Filesystem functions
config = file("${path.module}/config.txt") # Read file contents
template = templatefile("${path.module}/tpl.sh", { name = "app" })
# Type conversion
to_num = tonumber("42") # 42
to_str = tostring(42) # "42"
to_list = tolist(toset(["a", "b"])) # ["a", "b"]
}