HardПрактика13 min

Модули и State в Terraform

Модули: создание, композиция, версионирование. State: remote backend, locking, import, sensitive data, workspaces

Зачем нужны модули

По мере роста инфраструктуры Terraform-код становится неуправляемым. Модули решают несколько проблем:

Без модулей:                          С модулями:
┌───────────────────┐                 ┌───────────────────┐
│   main.tf         │                 │  root/            │
│   1500 строк...   │                 │  ├── main.tf      │  ~50 строк
│                   │                 │  ├── variables.tf  │
│   VPC + Subnets   │                 │  └── outputs.tf   │
│   + Security Grps │                 │                   │
│   + EC2 instances │                 │  modules/         │
│   + RDS           │                 │  ├── networking/   │  Reusable
│   + ElastiCache   │                 │  │   ├── main.tf   │
│   + S3 buckets    │                 │  │   ├── vars.tf   │
│   + IAM roles     │                 │  │   └── outputs.tf│
│   + CloudWatch    │                 │  ├── compute/      │  Reusable
│   ...             │                 │  ├── database/     │  Reusable
│                   │                 │  └── monitoring/   │  Reusable
└───────────────────┘                 └───────────────────┘

Преимущества модулей:

Преимущество Описание
Переиспользование Один модуль -- много окружений (dev, staging, prod)
Инкапсуляция Скрытие внутренней сложности
Организация Логическое разделение инфраструктуры
Тестируемость Модуль можно тестировать отдельно
Командная работа Разные команды владеют разными модулями
Версионирование Модуль можно версионировать и обновлять контролируемо

Структура модуля

Минимальный модуль

modules/database/
├── main.tf           # Resources
├── variables.tf      # Input variables
├── outputs.tf        # Output values
├── versions.tf       # Provider requirements (optional)
└── README.md         # Documentation

Полный модуль

modules/database/
├── main.tf           # Main resource definitions
├── variables.tf      # Input variable declarations
├── outputs.tf        # Output value declarations
├── versions.tf       # Terraform and provider version constraints
├── locals.tf         # Local values
├── data.tf           # Data sources
├── iam.tf            # IAM resources (if needed)
├── README.md         # Module documentation
├── examples/         # Usage examples
│   ├── simple/
│   │   └── main.tf
│   └── complete/
│       └── main.tf
└── tests/            # Module tests (Terraform 1.6+)
    └── main.tftest.hcl

Пример: модуль PostgreSQL базы данных

modules/database/variables.tf:

variable "name" {
  description = "Database identifier"
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]*$", var.name))
    error_message = "Name must start with a letter and contain only lowercase letters, numbers, and hyphens."
  }
}

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "engine_version" {
  description = "PostgreSQL engine version"
  type        = string
  default     = "18.1"
}

variable "instance_class" {
  description = "RDS instance class"
  type        = string
  default     = "db.t3.medium"
}

variable "allocated_storage" {
  description = "Storage size in GB"
  type        = number
  default     = 50

  validation {
    condition     = var.allocated_storage >= 20 && var.allocated_storage <= 65536
    error_message = "Storage must be between 20 and 65536 GB."
  }
}

variable "max_allocated_storage" {
  description = "Maximum storage for autoscaling (0 to disable)"
  type        = number
  default     = 100
}

variable "multi_az" {
  description = "Enable Multi-AZ deployment"
  type        = bool
  default     = false
}

variable "backup_retention_period" {
  description = "Number of days to retain backups"
  type        = number
  default     = 7
}

variable "vpc_id" {
  description = "VPC ID where database will be created"
  type        = string
}

variable "subnet_ids" {
  description = "List of subnet IDs for the DB subnet group"
  type        = list(string)
}

variable "allowed_security_group_ids" {
  description = "Security group IDs allowed to connect to database"
  type        = list(string)
  default     = []
}

variable "database_name" {
  description = "Name of the default database"
  type        = string
  default     = "app"
}

variable "master_username" {
  description = "Master username"
  type        = string
  default     = "postgres"
}

variable "deletion_protection" {
  description = "Enable deletion protection"
  type        = bool
  default     = true
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}

modules/database/main.tf:

locals {
  name_prefix = "${var.name}-${var.environment}"

  default_tags = {
    Module      = "database"
    Environment = var.environment
  }

  tags = merge(local.default_tags, var.tags)
}

# Random password for master user
resource "random_password" "master" {
  length  = 32
  special = false
}

# Store password in AWS Secrets Manager
resource "aws_secretsmanager_secret" "db_password" {
  name = "${local.name_prefix}-db-master-password"
  tags = local.tags
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = var.master_username
    password = random_password.master.result
    host     = aws_db_instance.main.address
    port     = aws_db_instance.main.port
    dbname   = var.database_name
  })
}

# DB Subnet Group
resource "aws_db_subnet_group" "main" {
  name       = "${local.name_prefix}-db-subnet"
  subnet_ids = var.subnet_ids

  tags = merge(local.tags, {
    Name = "${local.name_prefix}-db-subnet"
  })
}

# Security Group for database
resource "aws_security_group" "db" {
  name        = "${local.name_prefix}-db-sg"
  description = "Security group for ${local.name_prefix} database"
  vpc_id      = var.vpc_id

  ingress {
    description     = "PostgreSQL from allowed security groups"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = var.allowed_security_group_ids
  }

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

  tags = merge(local.tags, {
    Name = "${local.name_prefix}-db-sg"
  })
}

# Parameter Group
resource "aws_db_parameter_group" "main" {
  name   = "${local.name_prefix}-pg18"
  family = "postgres18"

  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "log_disconnections"
    value = "1"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "1000" # Log queries slower than 1 second
  }

  parameter {
    name         = "shared_preload_libraries"
    value        = "pg_stat_statements"
    apply_method = "pending-reboot"
  }

  tags = local.tags
}

# RDS Instance
resource "aws_db_instance" "main" {
  identifier = local.name_prefix

  engine         = "postgres"
  engine_version = var.engine_version
  instance_class = var.instance_class

  allocated_storage     = var.allocated_storage
  max_allocated_storage = var.max_allocated_storage
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = var.database_name
  username = var.master_username
  password = random_password.master.result

  multi_az               = var.multi_az
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]
  parameter_group_name   = aws_db_parameter_group.main.name

  backup_retention_period = var.backup_retention_period
  backup_window           = "03:00-04:00"
  maintenance_window      = "Mon:04:00-Mon:05:00"

  deletion_protection       = var.deletion_protection
  skip_final_snapshot       = var.environment != "prod"
  final_snapshot_identifier = var.environment == "prod" ? "${local.name_prefix}-final" : null

  performance_insights_enabled = true

  tags = merge(local.tags, {
    Name = local.name_prefix
  })

  lifecycle {
    prevent_destroy = false

    precondition {
      condition     = var.environment != "prod" || var.multi_az == true
      error_message = "Production database must have multi_az enabled."
    }

    precondition {
      condition     = var.environment != "prod" || var.backup_retention_period >= 14
      error_message = "Production database must have at least 14 days backup retention."
    }
  }
}

modules/database/outputs.tf:

output "instance_id" {
  description = "RDS instance identifier"
  value       = aws_db_instance.main.id
}

output "endpoint" {
  description = "Database endpoint (host:port)"
  value       = aws_db_instance.main.endpoint
}

output "address" {
  description = "Database hostname"
  value       = aws_db_instance.main.address
}

output "port" {
  description = "Database port"
  value       = aws_db_instance.main.port
}

output "database_name" {
  description = "Name of the default database"
  value       = aws_db_instance.main.db_name
}

output "master_username" {
  description = "Master username"
  value       = aws_db_instance.main.username
}

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

output "secret_arn" {
  description = "ARN of the Secrets Manager secret with credentials"
  value       = aws_secretsmanager_secret.db_password.arn
}

output "connection_string" {
  description = "PostgreSQL connection string"
  value       = "postgresql://${aws_db_instance.main.username}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
  sensitive   = true
}

Root Module vs Child Modules

┌─────────────────────────────────────────────────────────┐
│                    Root Module                            │
│                 (environments/prod/)                      │
│                                                          │
│  module "networking" {                                    │
│    source = "../../modules/networking"                    │
│    ...                                                   │
│  }                                                       │
│                           ┌──────────────────────────┐   │
│  module "database" { ───► │   Child Module           │   │
│    source = "../../mod.." │   modules/database/      │   │
│    vpc_id = module.net..  │                          │   │
│    subnet_ids = module..  │   Has its own:           │   │
│  }                        │   - resources            │   │
│                           │   - variables            │   │
│  module "app" {           │   - outputs              │   │
│    source = "../../mod.." │   - data sources         │   │
│    db_host = module.db..  │                          │   │
│  }                        │   Does NOT have:         │   │
│                           │   - provider config      │   │
│                           │   - backend config       │   │
│                           └──────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Использование модуля

# environments/prod/main.tf (Root Module)

module "networking" {
  source = "../../modules/networking"

  vpc_cidr    = "10.0.0.0/16"
  environment = "prod"
  project     = "myapp"
}

module "database" {
  source = "../../modules/database"

  name        = "myapp"
  environment = "prod"

  # Pass outputs from one module to another
  vpc_id     = module.networking.vpc_id
  subnet_ids = module.networking.private_subnet_ids

  # Module-specific configuration
  instance_class          = "db.r6g.large"
  allocated_storage       = 200
  multi_az                = true
  backup_retention_period = 30
  deletion_protection     = true

  allowed_security_group_ids = [module.app.security_group_id]
}

module "app" {
  source = "../../modules/compute"

  name        = "myapp"
  environment = "prod"

  vpc_id     = module.networking.vpc_id
  subnet_ids = module.networking.private_subnet_ids

  instance_count = 3
  instance_type  = "t3.large"

  db_host     = module.database.address
  db_name     = module.database.database_name
  db_secret   = module.database.secret_arn
}

# Outputs from root module
output "database_endpoint" {
  value = module.database.endpoint
}

output "app_load_balancer" {
  value = module.app.lb_dns_name
}

Module Sources

Terraform поддерживает разные источники для модулей:

# 1. Local path (most common for project-specific modules)
module "database" {
  source = "../../modules/database"
}

# 2. Terraform Registry (public modules)
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["eu-central-1a", "eu-central-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
}

# 3. GitHub
module "database" {
  source = "github.com/myorg/terraform-modules//database?ref=v2.1.0"
}

# 4. Git (generic)
module "database" {
  source = "git::https://gitlab.com/myorg/tf-modules.git//database?ref=v2.1.0"
}

# 5. S3 bucket
module "database" {
  source = "s3::https://s3-eu-central-1.amazonaws.com/my-modules/database.zip"
}

# 6. Terraform Cloud / Enterprise private registry
module "database" {
  source  = "app.terraform.io/myorg/database/aws"
  version = "~> 2.0"
}

Версионирование модулей

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"

  # Version constraints (same as providers)
  version = "5.1.0"        # Exact version
  version = ">= 5.0"       # Minimum
  version = "~> 5.0"        # >= 5.0.0, < 6.0.0
  version = ">= 5.0, < 6.0" # Range
}

Module Composition Patterns

Pattern 1: Facade Module (объединяющий модуль)

# modules/app-stack/main.tf
# Combines multiple modules into a single "app stack"

module "networking" {
  source = "../networking"

  vpc_cidr    = var.vpc_cidr
  environment = var.environment
}

module "database" {
  source = "../database"

  vpc_id     = module.networking.vpc_id
  subnet_ids = module.networking.private_subnet_ids

  instance_class = var.db_instance_class
  multi_az       = var.environment == "prod"
}

module "compute" {
  source = "../compute"

  vpc_id     = module.networking.vpc_id
  subnet_ids = module.networking.private_subnet_ids

  db_endpoint = module.database.endpoint
  db_secret   = module.database.secret_arn
}

# Usage from root:
module "app" {
  source = "./modules/app-stack"

  environment       = "prod"
  vpc_cidr          = "10.0.0.0/16"
  db_instance_class = "db.r6g.large"
}

Pattern 2: Conditional Resources

# Module that creates resources conditionally
variable "create_read_replica" {
  type    = bool
  default = false
}

resource "aws_db_instance" "replica" {
  count = var.create_read_replica ? 1 : 0

  replicate_source_db = aws_db_instance.main.identifier
  instance_class      = var.replica_instance_class
}

output "replica_endpoint" {
  value = var.create_read_replica ? aws_db_instance.replica[0].endpoint : null
}

Terraform State

Что такое State

State -- файл terraform.tfstate, который хранит соответствие между ресурсами в коде и реальными объектами в облаке.

┌────────────────┐         ┌────────────────┐
│   Terraform    │         │   terraform    │
│   Config       │         │   .tfstate     │
│   (.tf files)  │         │                │
│                │         │ {              │
│ resource "aws_ │    ┌───►│  "aws_inst..": │
│ instance" "web"│    │    │  {             │
│ {              │    │    │   "id": "i-abc"│
│   ami = "ami-."│    │    │   "ami": "ami."│
│   type = "t3.m"│    │    │   "type": "t3."│
│ }              │    │    │  }             │
└───────┬────────┘    │    │ }              │
        │             │    └───────┬────────┘
        │  terraform  │            │
        │  plan/apply │            │
        │             │            │
        ▼             │            ▼
┌────────────────────────────────────────┐
│         Terraform Core Engine          │
│                                        │
│  Compares config vs state vs real      │
│  Determines: create, update, delete    │
└───────────────────┬────────────────────┘
                    │
                    ▼
          ┌──────────────────┐
          │   Cloud Provider │
          │   (AWS, GCP...)  │
          │                  │
          │  Real Resources  │
          │  i-abc123...     │
          └──────────────────┘

Зачем нужен State

  1. Маппинг -- связь resource "aws_instance" "web" с реальным i-abc123
  2. Метаданные -- зависимости между ресурсами, порядок удаления
  3. Кэширование -- не нужно опрашивать API для каждого ресурса
  4. Производительность -- план строится быстро, без API-вызовов

Чувствительные данные в State

ВАЖНО: State содержит ВСЕ атрибуты ресурсов в открытом виде, включая пароли, секреты, приватные ключи!

{
  "resources": [
    {
      "type": "aws_db_instance",
      "name": "main",
      "instances": [
        {
          "attributes": {
            "password": "my-super-secret-password",
            "endpoint": "mydb.abc123.eu-central-1.rds.amazonaws.com"
          }
        }
      ]
    }
  ]
}

Правила работы с State:

Правило Описание
НИКОГДА в Git terraform.tfstate НЕ ДОЛЖЕН быть в репозитории
Шифрование Хранить в зашифрованном бэкенде (S3 + encrypt)
Ограничение доступа Только CI/CD и senior-инженеры
Бэкапы Версионирование S3 bucket

Remote Backend

Проблема локального State

Developer A                  Developer B
┌──────────────┐            ┌──────────────┐
│ terraform    │            │ terraform    │
│ .tfstate     │            │ .tfstate     │
│              │            │              │
│ (local copy) │            │ (local copy) │
└──────┬───────┘            └──────┬───────┘
       │ apply                     │ apply
       │                           │
       ▼                           ▼
┌─────────────────────────────────────────┐
│              AWS                         │
│  CONFLICT! Both modify same resources    │
│  State files are out of sync!            │
└─────────────────────────────────────────┘

S3 + DynamoDB Backend (самый популярный)

# backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "projects/myapp/prod/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true                              # Server-side encryption
    dynamodb_table = "terraform-locks"                 # State locking
    kms_key_id     = "arn:aws:kms:eu-central-1:123:key/abc" # KMS encryption
  }
}

Создание backend-инфраструктуры (bootstrap):

# bootstrap/main.tf - run this ONCE with local state
provider "aws" {
  region = "eu-central-1"
}

# S3 bucket for state
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name      = "Terraform State"
    ManagedBy = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = {
    Name      = "Terraform Lock Table"
    ManagedBy = "terraform"
  }
}

State Locking

State Locking (DynamoDB):

Developer A              DynamoDB              Developer B
    │                       │                       │
    │── terraform apply ──► │                       │
    │                       │                       │
    │   Lock acquired ◄──── │                       │
    │                       │                       │
    │   ... applying ...    │ ◄── terraform apply ──│
    │                       │                       │
    │                       │── Lock denied! ──────►│
    │                       │   "Error: state locked │
    │                       │    by Developer A"     │
    │                       │                       │
    │── apply complete ───► │                       │
    │   Lock released       │                       │
    │                       │                       │
    │                       │ ◄── retry apply ──────│
    │                       │   Lock acquired        │

Другие Remote Backends

# Terraform Cloud
terraform {
  cloud {
    organization = "myorg"
    workspaces {
      name = "myapp-prod"
    }
  }
}

# PostgreSQL backend
terraform {
  backend "pg" {
    conn_str = "postgres://user:pass@host/terraform_state"
  }
}

# Consul backend
terraform {
  backend "consul" {
    address = "consul.example.com:8500"
    scheme  = "https"
    path    = "terraform/myapp"
  }
}

# Google Cloud Storage
terraform {
  backend "gcs" {
    bucket = "myproject-terraform-state"
    prefix = "terraform/state"
  }
}

State Operations

terraform state list и show

# List all resources in state
terraform state list
# aws_vpc.main
# aws_subnet.private[0]
# aws_subnet.private[1]
# aws_db_instance.main
# module.compute.aws_instance.web[0]
# module.compute.aws_instance.web[1]

# Show details of specific resource
terraform state show aws_db_instance.main
# resource "aws_db_instance" "main" {
#     allocated_storage       = 100
#     engine                  = "postgres"
#     engine_version          = "18.1"
#     id                      = "myapp-prod"
#     instance_class          = "db.r6g.large"
#     ...
# }

terraform import

Импортирует существующий ресурс в Terraform State. Полезно при миграции с ручного управления на IaC.

# Syntax: terraform import <resource_address> <cloud_id>

# Import existing EC2 instance
terraform import aws_instance.web i-1234567890abcdef0

# Import existing RDS instance
terraform import aws_db_instance.main myapp-production

# Import into module
terraform import module.database.aws_db_instance.main mydb-prod

# Import with for_each
terraform import 'aws_instance.web["api"]' i-abc123

Terraform 1.5+: import block (декларативный импорт):

# Import block - declare in code, apply to import
import {
  to = aws_instance.web
  id = "i-1234567890abcdef0"
}

# Generate config automatically (Terraform 1.5+)
# terraform plan -generate-config-out=generated.tf

terraform state mv

Перемещает ресурс в state без пересоздания:

# Rename resource
terraform state mv aws_instance.web aws_instance.app

# Move to module
terraform state mv aws_instance.web module.compute.aws_instance.web

# Move between modules
terraform state mv module.old.aws_instance.web module.new.aws_instance.web

# Move to different state file
terraform state mv -state-out=other.tfstate aws_instance.web aws_instance.web

terraform state rm

Удаляет ресурс из state БЕЗ удаления реального ресурса:

# Remove from state (resource continues to exist in cloud)
terraform state rm aws_instance.legacy

# Remove entire module
terraform state rm module.old_database

# Useful when:
# - Moving resource management to another Terraform config
# - Resource was imported by mistake
# - Splitting monolithic state into smaller ones

terraform state replace-provider

# Replace provider (useful during provider migration)
terraform state replace-provider hashicorp/aws registry.terraform.io/hashicorp/aws

Workspaces vs Directory Structure

Два подхода к управлению окружениями:

Workspaces

# Create and switch workspace
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# List workspaces
terraform workspace list
#   default
#   dev
# * staging
#   prod

# Switch workspace
terraform workspace select prod

# Current workspace
terraform workspace show
# prod
# Use workspace name in configuration
locals {
  environment = terraform.workspace

  config = {
    dev = {
      instance_type = "t3.micro"
      instance_count = 1
      multi_az = false
    }
    staging = {
      instance_type = "t3.small"
      instance_count = 2
      multi_az = false
    }
    prod = {
      instance_type = "t3.large"
      instance_count = 3
      multi_az = true
    }
  }

  env_config = local.config[local.environment]
}

resource "aws_instance" "web" {
  count         = local.env_config.instance_count
  instance_type = local.env_config.instance_type
}

Directory Structure (рекомендуется)

infrastructure/
├── modules/
│   ├── networking/
│   ├── database/
│   └── compute/
├── environments/
│   ├── dev/
│   │   ├── main.tf          # Module calls
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── backend.tf       # Separate state per env
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── backend.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── backend.tf
│       └── terraform.tfvars
└── global/
    └── iam/                  # Shared IAM roles
        └── main.tf

Сравнение подходов

Критерий Workspaces Directory Structure
Разделение state Автоматическое Явное (разные backend keys)
Сложность Простая Средняя
Blast radius Один конфиг -- одна ошибка на все env Отдельный конфиг на каждый env
Различия между env Через переменные и условия Разные значения tfvars
Code review Одно изменение для всех env Отдельные PR для каждого env
Рекомендация Для простых проектов Для production

Защита чувствительных данных в State

Пароли и секреты

# ❌ BAD: password in plain text
variable "db_password" {
  type    = string
  default = "my-super-secret"  # NEVER do this!
}

# ✅ GOOD: generate random password
resource "random_password" "db" {
  length  = 32
  special = false
}

# ✅ GOOD: store in Secrets Manager
resource "aws_secretsmanager_secret" "db_password" {
  name = "myapp/db-password"
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = random_password.db.result
}

# ✅ GOOD: reference in RDS
resource "aws_db_instance" "main" {
  password = random_password.db.result
  # Password will still be in state, but not in code!
}

Минимизация чувствительных данных

# Mark outputs as sensitive
output "db_password" {
  value     = random_password.db.result
  sensitive = true  # Hidden in plan/apply output
}

# Mark variables as sensitive
variable "api_key" {
  type      = string
  sensitive = true  # Hidden in plan/apply output
}

Рекомендации по безопасности State

┌──────────────────────────────────────────────────┐
│          State Security Checklist                  │
├──────────────────────────────────────────────────┤
│                                                   │
│  [x] Remote backend (S3, Terraform Cloud)         │
│  [x] Encryption at rest (S3 SSE, KMS)             │
│  [x] Encryption in transit (HTTPS)                │
│  [x] State locking (DynamoDB)                     │
│  [x] Versioning (S3 bucket versioning)            │
│  [x] Access control (IAM policies)                │
│  [x] No state in Git (.gitignore)                 │
│  [x] Sensitive outputs marked                     │
│  [x] Passwords generated, not hardcoded           │
│  [x] Secrets stored in Secrets Manager / Vault    │
│                                                   │
└──────────────────────────────────────────────────┘

.gitignore для Terraform-проекта:

# Terraform
*.tfstate
*.tfstate.*
*.tfvars         # May contain secrets
.terraform/
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Keep lock file
!.terraform.lock.hcl

Проверь себя

5 из 8

Что делает terraform import?

Почему terraform.tfstate НЕЛЬЗЯ хранить в Git?

Какие источники модулей поддерживает Terraform?

Для чего используется DynamoDB table в связке с S3 backend?

Какие файлы составляют минимальную структуру модуля Terraform?