8 вересня 2026 р.
Terraform для AWS

Створити EC2 instance через AWS Console нескладно. Складніше через місяць точно відповісти, хто змінив security group, чому production відрізняється від staging і як відтворити всю інфраструктуру в іншому регіоні. Terraform розв’язує цю проблему декларативно: ви описуєте бажаний стан у коді, переглядаєте план змін і лише потім дозволяєте інструменту звернутися до AWS API.
У цій інструкції ми підготуємо невеликий, але повноцінний Terraform-проєкт для AWS. Він створить VPC, public subnet, internet gateway, route table, security group та EC2 instance. Окремо налаштуємо автентифікацію без ключів у `.tf` файлах, remote state в S3 із блокуванням, змінні, outputs і безпечний workflow `plan` → review → `apply`.
Terraform не замінює розуміння AWS. Помилка в HCL може відкрити порт, видалити ресурс або створити витрати в кількох Availability Zones. Тому код інфраструктури потребує такого самого review, тестування й контролю доступу, як код застосунку. Якщо потрібно спроєктувати перенесення сервісів і цільову AWS-архітектуру до початку автоматизації, варто окремо спланувати міграцію в хмару.
Структура Terraform-проєкту та підготовка інструментів
Встановлюйте Terraform із офіційного репозиторію HashiCorp або перевіреного release package. Версію CLI не фіксуйте за випадковим прикладом зі статті: перевірте актуальний supported release для вашого середовища та зафіксуйте constraint у коді. Так само встановіть AWS CLI v2, адже він зручний для входу через IAM Identity Center і перевірки активної identity.
terraform version
aws --version
git --versionСтворіть окремий каталог і базові файли:
mkdir terraform-aws-demo
cd terraform-aws-demo
touch versions.tf providers.tf variables.tf main.tf outputs.tf
touch backend.hcl terraform.tfvars.example .gitignoreУ `.gitignore` не повинні потрапляти локальний state, plan-файли, crash logs і файли зі значеннями змінних:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
crash.*.log
terraform.tfvars
*.auto.tfvars
*.auto.tfvars.jsonФайл `.terraform.lock.hcl`, навпаки, додавайте в Git. Він фіксує вибрану версію provider та checksums, щоб локальний запуск і CI використовували однакову залежність.
У `versions.tf` обмежте Terraform і AWS provider. Constraint `~> 6.0` дозволяє сумісні оновлення в межах major-версії, але не переводить проєкт на наступний major автоматично:
terraform {
required_version = ">= 1.10, < 2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}Перед використанням constraints звірте їх з актуальними releases та перевірте upgrade у тестовому середовищі. Не запускайте `terraform init -upgrade` без review змін lock-файла та release notes provider.
Зручна структура з одним root module підходить для навчального прикладу. У реальному репозиторії розділяйте незалежні stacks: наприклад, network, compute і data. Не складайте dev, stage та prod в один state. Окремий state зменшує blast radius, прискорює plan і дозволяє видати різні IAM-права.
Автентифікація AWS і конфігурація provider
Не записуйте `access_key` і `secret_key` у `providers.tf`, variables або `terraform.tfvars`. Terraform state та plan можуть містити чутливі значення, а Git зберігає історію навіть після видалення рядка. Для локальної роботи використовуйте IAM Identity Center, короткоживучий profile або AssumeRole; для CI — OIDC і окрему IAM role з мінімальними правами.
Після входу перевірте, від чийого імені виконається apply:
aws sso login --profile platform-dev
export AWS_PROFILE=platform-dev
aws sts get-caller-identity
aws configure get region --profile "$AWS_PROFILE"Вивід `get-caller-identity` потрібно звірити з очікуваним account ID. Захист від випадкового запуску в іншому account можна додати безпосередньо в provider через `allowed_account_ids`.
`providers.tf`:
provider "aws" {
region = var.aws_region
allowed_account_ids = [var.aws_account_id]
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
}`variables.tf`:
variable "aws_region" {
description = "AWS region for all resources"
type = string
default = "eu-central-1"
}
variable "aws_account_id" {
description = "Allowed AWS account ID"
type = string
}
variable "project_name" {
type = string
default = "web-platform"
}
variable "environment" {
type = string
default = "dev"
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "environment must be dev, stage or prod"
}
}
variable "admin_cidr" {
description = "Trusted IPv4 CIDR allowed to reach the web port"
type = string
}Створіть `terraform.tfvars` локально, не додаючи його в Git:
aws_account_id = "111122223333"
admin_cidr = "203.0.113.10/32"IP з документаційного діапазону потрібно замінити на фактичну довірену адресу. Для production краще не відкривати адміністративний доступ до EC2 взагалі, а використовувати AWS Systems Manager Session Manager через instance role і приватні endpoints.
Remote state у S3 та блокування операцій
За замовчуванням Terraform зберігає `terraform.tfstate` локально. Для командної роботи це небезпечно: файл легко втратити, двоє інженерів можуть одночасно виконати apply, а локальна копія не має централізованого аудиту. Створіть окремий S3 bucket для state до підключення backend. Bucket має бути не в тому самому state, який він зберігає, інакше виникає циклічна залежність під час bootstrap або destroy.
Для bucket увімкніть versioning, server-side encryption, Block Public Access і заборону незашифрованого transport. Доступ до state та lock-файла надавайте тільки Terraform execution roles і вузькому колу адміністраторів. State може містити identifiers, attributes та secrets, навіть якщо output позначено `sensitive`.
Додайте backend block у `versions.tf`:
terraform {
backend "s3" {
use_lockfile = true
encrypt = true
}
}А несекретні параметри збережіть у `backend.hcl`:
bucket = "company-terraform-state-eu-central-1"
key = "web-platform/dev/terraform.tfstate"
region = "eu-central-1"Ініціалізуйте backend:
terraform init -backend-config=backend.hcl
terraform providersСучасний S3 backend підтримує native locking через `use_lockfile = true`. Старий спосіб із DynamoDB table для locking позначений як deprecated, тому не додавайте нову таблицю лише за застарілими прикладами. Під час міграції старого проєкту HashiCorp дозволяє тимчасово мати обидві конфігурації, але цільовий стан має відповідати документації вашої версії Terraform.
IAM policy для backend повинна дозволяти list потрібного bucket prefix, читання й запис конкретного state object, а для locking — також роботу з object `<key>.tflock`, включно з видаленням lock. Не видавайте wildcard-доступ до всіх S3 buckets account. Versioning допоможе повернути попередню версію object після людської помилки, але відновлення state все одно потрібно документувати й тестувати.
Створення VPC, subnet і EC2 instance
Тепер опишемо ресурси у `main.tf`. Спочатку отримаємо доступні Availability Zones і актуальний Ubuntu AMI через AWS Systems Manager public parameter. Так конфігурація не залежатиме від AMI ID, який різниться між регіонами та змінюється з новими образами.
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ssm_parameter" "ubuntu_ami" {
name = "/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id"
}
resource "aws_vpc" "main" {
cidr_block = "10.40.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "${var.project_name}-${var.environment}-vpc" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.project_name}-${var.environment}-igw" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.40.10.0/24"
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = { Name = "${var.project_name}-${var.environment}-public-a" }
}
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 = "${var.project_name}-${var.environment}-public" }
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}Security group відкриє тільки HTTP-порт із CIDR, переданого змінною. Вихідний traffic тут дозволено повністю для демонстрації; у захищеному середовищі egress також потрібно обмежити відповідно до залежностей workload.
resource "aws_security_group" "web" {
name_prefix = "${var.project_name}-${var.environment}-web-"
description = "Web access from a trusted CIDR"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from trusted network"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.admin_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
tags = { Name = "${var.project_name}-${var.environment}-web" }
}
resource "aws_instance" "web" {
ami = data.aws_ssm_parameter.ubuntu_ami.value
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
associate_public_ip_address = true
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
}
root_block_device {
encrypted = true
volume_type = "gp3"
volume_size = 12
}
tags = { Name = "${var.project_name}-${var.environment}-web-01" }
}Значення `t3.micro` не є універсально безкоштовним: eligibility залежить від account, регіону та чинних умов AWS. Перед apply перевірте pricing, service quotas і створіть AWS Budget. Для production потрібні щонайменше private subnets, кілька Availability Zones, load balancer, Auto Scaling, централізовані logs, backup і monitoring. Аналогічно, якщо EC2 використовується як основа для control plane, сам Terraform provisioning ще не створює HA Kubernetes кластер — для нього потрібні окремі bootstrap та lifecycle-процедури.
У `outputs.tf` виведіть лише потрібні значення:
output "instance_id" {
value = aws_instance.web.id
}
output "public_ip" {
value = aws_instance.web.public_ip
}
output "vpc_id" {
value = aws_vpc.main.id
}Виконайте форматування і статичну перевірку до звернення до AWS:
terraform fmt -check -recursive
terraform validate
terraform plan -out=dev.tfplan
terraform show dev.tfplanУважно прочитайте summary: скільки ресурсів буде created, changed або destroyed. Застосовуйте саме переглянутий plan-файл, а не новий неузгоджений plan:
terraform apply dev.tfplan
terraform outputSaved plan містить повну заплановану зміну й може містити sensitive data, тому не завантажуйте його як публічний artifact і задавайте короткий retention у CI.
Workflow, drift, import і CI/CD
Зберігайте HCL, `.terraform.lock.hcl`, `terraform.tfvars.example` і backend template у Git. На merge request запускайте `terraform fmt -check`, `terraform init`, `terraform validate`, policy/security scan і `terraform plan`. Apply дозволяйте лише з protected branch або захищеного environment після review. Такий CI/CD-процес повинен отримувати короткоживучі AWS credentials через OIDC, а не довгостроковий secret key.
Не запускайте паралельні apply для одного state. S3 lock захистить від одночасного запису, але не вирішить організаційний конфлікт двох різних merge requests. Серіалізуйте jobs для кожного stack та environment. `terraform force-unlock` використовуйте тільки після перевірки, що попередній процес справді завершився: примусове зняття активного lock може пошкодити state.
Періодично запускайте read-only plan для пошуку drift:
terraform plan -detailed-exitcodeExit code `0` означає відсутність змін, `2` — наявність diff, `1` — помилку. У shell або CI не можна трактувати `2` як звичайний failure без окремої логіки. Знайдений drift не слід автоматично «виправляти» apply: спочатку визначте, чи була ручна зміна аварійною, чи HCL більше не відповідає потрібному стану.
Існуючий AWS resource можна взяти під керування через import block:
import {
to = aws_instance.web
id = "i-0123456789abcdef0"
}Import додає binding у state, але не створює правильну конфігурацію автоматично. Спочатку опишіть resource, виконайте plan і вирівняйте аргументи так, щоб не отримати несподіваний replace або destroy. Після успішного import block можна прибрати окремим reviewed commit.
Для контрольованого видалення спочатку створіть destroy plan:
terraform plan -destroy -out=destroy.tfplan
terraform show destroy.tfplan
terraform apply destroy.tfplanНе запускайте `terraform destroy -auto-approve` у спільному account. Ресурси з даними мають мати backup, retention і окремий захист від видалення. Видалення root stack не гарантує, що зникнуть snapshots, зовнішні DNS records, manual resources або витрати в іншому регіоні.
Типові помилки під час роботи з Terraform і AWS
Перша помилка — commit AWS keys, `terraform.tfvars` або state у Git. Якщо секрет уже потрапив в історію, простого видалення файла недостатньо: негайно відкличте credentials, перевірте CloudTrail, оцініть виконані API calls і лише потім очищуйте історію за погодженою процедурою.
Друга — один state для всіх середовищ. Невдалий apply тоді має надто великий blast radius, а IAM role отримує зайві права. Розділяйте stacks та accounts, використовуйте окремі keys backend і не покладайтеся на workspaces як на єдиний security boundary.
Третя — apply без saved plan або після того, як plan застарів. Між plan і apply могла змінитися інфраструктура, credentials чи provider configuration. У CI застосовуйте той самий reviewed artifact протягом короткого вікна й повторюйте plan після зміни branch.
Четверта — надто широкі IAM policies та security groups із `0.0.0.0/0`. Terraform автоматизує помилку так само швидко, як правильне рішення. Додавайте `allowed_account_ids`, least privilege, policy checks, IMDSv2, encryption і default tags. Перевіряйте не лише синтаксис, а й наслідки для мережі та доступу.
П’ята — ручне редагування state або сліпе використання `force-unlock`. Для state operations застосовуйте Terraform CLI, робіть backup і спершу перевіряйте lock owner. Відновлення попередньої S3 object version має бути аварійною процедурою з тестом, а не щоденним способом виправлення конфігурації.
Висновок
Надійний Terraform-проєкт для AWS складається не лише з `aws_instance`. Йому потрібні зафіксовані версії provider, короткоживуча IAM identity, захист від неправильного account, remote state у versioned S3 bucket, native lockfile, контрольовані variables і review кожного plan.
Навчальний приклад VPC, subnet, route table, security group та EC2 показує головний цикл: `fmt`, `validate`, `plan`, review і `apply` з того самого plan-файла. Далі цю основу можна розділити на modules і stacks, додати private networks, load balancers, databases, monitoring та policy checks без ручного створення ресурсів у Console.
Починайте з невеликого dev account, перевіряйте identity й витрати до apply, зберігайте state окремо від коду та регулярно шукайте drift. Автоматизація стає безпечною лише тоді, коли доступ мінімальний, зміни видимі до виконання, а видалення й відновлення відпрацьовані так само ретельно, як створення інфраструктури.