Чому структура Terraform має значення
Кожна інфраструктурна команда починає однаково: один файл main.tf, що провізіонить кілька ресурсів. Це чудово працює для proof of concept. Потім проєкт зростає, додається більше інженерів, і раптом цей моноліт має 2,000 рядків, ніхто не знає, що від чого залежить, і кожен terraform plan займає вісім хвилин.
Знайомо? Спосіб, у який ви структуруєте Terraform-код, безпосередньо впливає на швидкість поставки команди, на безпечність змін та легкість онбордингу нових інженерів. У цьому посібнику ми ділимося патернами, які використовуємо в DevOpsVibe у десятках продакшн-середовищ.
Архітектура на основі модулів
Найважливіше рішення, яке ви можете прийняти, — рано перейти на архітектуру на основі модулів. Модулі — це повторно використовувані, тестовані одиниці інфраструктури, що інкапсулюють логічне групування ресурсів.
Структура каталогів
Ось структура, яку ми рекомендуємо для середніх та великих проєктів:
infrastructure/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ ├── database/
│ └── monitoring/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ └── production/
├── global/
│ ├── iam/
│ └── dns/
└── terragrunt.hcl # optional
Кожен каталог середовища компонує модулі разом зі специфічними для середовища змінними. Самі модулі не містять жодних hardcoded значень.
Написання повторно використовуваного модуля
Добре структурований модуль має чіткі входи, виходи та одну відповідальність:
# modules/networking/variables.tf
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
validation {
condition = can(cidrnetmask(var.vpc_cidr))
error_message = "Must be a valid CIDR block."
}
}
variable "environment" {
description = "Environment name (dev, staging, production)"
type = string
}
variable "availability_zones" {
description = "List of AZs to use"
type = list(string)
}
# modules/networking/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}
}
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.environment}-private-${var.availability_zones[count.index]}"
Environment = var.environment
Type = "private"
}
}
# modules/networking/outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
Стратегія управління state
Віддалений state — це не предмет переговорів для команд. Використовуйте S3 з DynamoDB locking (AWS) або GCS з locking (GCP):
# environments/production/backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/networking/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Ключові правила управління state:
- Один файл state на компонент на середовище. Ніколи не кладіть всю інфраструктуру в один state-файл. Якщо state вашої VPC буде пошкоджено, ви не хочете, щоб він забрав і базу даних.
- Увімкніть шифрування at rest. State-файли містять чутливі дані, включно з паролями та приватними ключами.
- Використовуйте state locking. Без нього два інженери, які одночасно запускають
terraform apply, можуть пошкодити ваш state. - Ніколи не комітьте state-файли в систему контролю версій. Додайте
*.tfstateі*.tfstate.backupдо вашого.gitignore.
Управління змінними
Уникайте hardcoding значень. Використовуйте шаровий підхід до змінних:
# environments/production/terraform.tfvars
environment = "production"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
instance_type = "m6i.xlarge"
min_capacity = 3
max_capacity = 10
Для секретів ніколи не зберігайте їх у файлах .tfvars. Натомість посилайтеся на них з менеджера секретів:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/database/master-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ...
}
Стратегія тегування
Послідовне тегування є необхідним для розподілу витрат, аудиту безпеки та управління ресурсами. Визначте спільний модуль тегування:
# modules/tags/main.tf
variable "environment" { type = string }
variable "project" { type = string }
variable "team" { type = string }
locals {
common_tags = {
Environment = var.environment
Project = var.project
Team = var.team
ManagedBy = "terraform"
Repository = "github.com/mycompany/infrastructure"
}
}
output "tags" {
value = local.common_tags
}
Потім використовуйте його скрізь:
module "tags" {
source = "../../modules/tags"
environment = "production"
project = "platform"
team = "infrastructure"
}
resource "aws_instance" "app" {
# ...
tags = merge(module.tags.tags, {
Name = "app-server"
Role = "application"
})
}
CI/CD інтеграція
Terraform ніколи не повинен запускатися з ноутбука розробника в продакшні. Налаштуйте конвеєр:
- Pull request відкривається --
terraform fmt -checkіterraform validateзапускаються автоматично - PR схвалено --
terraform planзапускається, а вивід публікується як коментар до PR - PR злито в main --
terraform apply -auto-approveвиконується в конвеєрі
Використовуйте такі інструменти, як Atlantis або Spacelift, для цього робочого процесу або побудуйте власний з GitHub Actions:
# .github/workflows/terraform.yml
name: Terraform
on:
pull_request:
paths: ['infrastructure/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
working-directory: infrastructure/environments/production
- run: terraform plan -no-color -out=tfplan
working-directory: infrastructure/environments/production
- uses: actions/github-script@v7
with:
script: |
const output = require('fs').readFileSync('tfplan.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan\n\`\`\`\n${output}\n\`\`\``
});
Тестування вашої інфраструктури
Використовуйте Terratest або terraform test (вбудований з Terraform 1.6) для валідації ваших модулів:
# modules/networking/tests/vpc.tftest.hcl
run "creates_vpc_with_correct_cidr" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
environment = "test"
availability_zones = ["us-east-1a"]
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block did not match expected value"
}
}
Поширені анти-патерни, яких слід уникати
- Монолітні state-файли. Розділяйте за компонентом і середовищем.
- Використання
count, колиfor_eachдоречніший.for_eachз мапами дає вам стабільні адреси ресурсів, які не зміщуються при додаванні чи видаленні елементів. - Ігнорування дрифту. Запускайте
terraform planза розкладом, щоб виявляти ручні зміни. - Пропуск
terraform fmt. Забезпечуйте форматування в CI. Непослідовне форматування створює шумні diff'и. - Hardcoding версій провайдерів. Закріплюйте їх явно, але регулярно переглядайте оновлення.
Висновок
Добре структурування Terraform з самого початку економить експоненційні зусилля пізніше. Описані вище патерни -- архітектура на основі модулів, ізольований state, шарові змінні, CI/CD автоматизація та тестування -- становлять фундамент кожного масштабованого інфраструктурного проєкту, який ми постачаємо.
У DevOpsVibe ми допомагаємо командам проєктувати та впроваджувати Terraform-архітектури, що масштабуються від кількох ресурсів до тисяч. Чи ви починаєте з нуля, чи розплутуєте існуючу кодову базу, наші інженери можуть поставити вашу інфраструктуру на тверду основу. Зв'яжіться з нами, щоб дізнатися більше.