DevOps Admin

O que Todo Dev Deve Saber sobre Terraform com AWS: VPC, EC2, RDS e IAM na Prática Já leu

Fundamentos do Terraform e AWS O Terraform é uma ferramenta de Infrastructure as Code (IaC) que permite descrever, versionizar e gerenciar toda a infraestrutura em nuvem através de arquivos de configuração declarativos. Diferente de scripts imperativos que dizem como fazer algo, o Terraform descreve o que você quer que exista, permitindo que a ferramenta figure out os passos necessários. Para trabalhar com AWS, você precisa autenticar o Terraform com suas credenciais AWS — isso pode ser feito através de variáveis de ambiente, arquivos de configuração local ou, em produção, através de roles IAM. A estrutura básica de um projeto Terraform consiste em arquivos que definem providers, recursos, variáveis e outputs. O provider é o intermediário entre seu código e a AWS — sem ele, o Terraform não sabe como se comunicar com a nuvem. Quando você executa , a ferramenta cria um arquivo que rastreia o estado atual da infraestrutura. Este arquivo é crítico e deve ser versionado com cuidado

Fundamentos do Terraform e AWS

O Terraform é uma ferramenta de Infrastructure as Code (IaC) que permite descrever, versionizar e gerenciar toda a infraestrutura em nuvem através de arquivos de configuração declarativos. Diferente de scripts imperativos que dizem como fazer algo, o Terraform descreve o que você quer que exista, permitindo que a ferramenta figure out os passos necessários. Para trabalhar com AWS, você precisa autenticar o Terraform com suas credenciais AWS — isso pode ser feito através de variáveis de ambiente, arquivos de configuração local ou, em produção, através de roles IAM.

A estrutura básica de um projeto Terraform consiste em arquivos .tf que definem providers, recursos, variáveis e outputs. O provider é o intermediário entre seu código e a AWS — sem ele, o Terraform não sabe como se comunicar com a nuvem. Quando você executa terraform apply, a ferramenta cria um arquivo terraform.tfstate que rastreia o estado atual da infraestrutura. Este arquivo é crítico e deve ser versionado com cuidado em ambientes reais.

# provider.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

Antes de começar com VPC, EC2, RDS e IAM, você precisa ter uma conta AWS ativa e credenciais configuradas. Recomendo usar o AWS CLI para testar a conexão: aws sts get-caller-identity. Se retornar seus dados de account ID, você está pronto.

VPC: Fundação de Toda Infraestrutura AWS

A Virtual Private Cloud (VPC) é sua rede privada na AWS. Tudo o que você deploy — EC2, RDS, Lambda — precisa existir dentro de uma VPC. Uma VPC contém subnets (redes menores dentro da VPC), route tables (definem como o tráfego flui), internet gateways (permitem comunicação com a internet) e security groups (firewalls em nível de instância).

Quando você cria uma VPC, precisa definir um CIDR block — um intervalo de endereços IP privados. Por exemplo, 10.0.0.0/16 oferece 65.536 endereços IP. As subnets são divisões deste bloco: você pode ter 10.0.1.0/24 (256 IPs) como subnet pública e 10.0.2.0/24 como subnet privada. A subnet pública tem acesso à internet via internet gateway, enquanto a subnet privada não — perfeito para RDS que não precisa ser acessível da internet.

# vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
    Name = "private-subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

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 = "public-rt"
  }
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

data "aws_availability_zones" "available" {
  state = "available"
}

Neste código, criamos uma VPC com CIDR 10.0.0.0/16, duas subnets em availability zones diferentes (essencial para alta disponibilidade), um internet gateway para acesso à internet e uma route table pública que roteia todo tráfego (0.0.0.0/0) para o internet gateway. A subnet pública tem map_public_ip_on_launch = true, o que significa que qualquer EC2 lançada nela recebe automaticamente um IP público.

EC2 e Security Groups: Computação na Nuvem

EC2 (Elastic Compute Cloud) são máquinas virtuais na AWS. Você especifica o tipo de instância (t2.micro, t3.small, m5.large, etc.), a AMI (Amazon Machine Image — basicamente o sistema operacional), e a quantidade de vCPU e memória que precisa. Security groups funcionam como firewalls — definem quais portas estão abertas e de onde o tráfego é permitido.

Ao criar uma EC2, você deve especificar a subnet (pública ou privada), atribuir uma role IAM (discutiremos depois), e configurar um security group. É extremamente importante não abrir a porta SSH (22) para 0.0.0.0/0 a menos que você tenha um motivo muito específico — isto é um risco de segurança grave. Em produção, use bastion hosts ou Session Manager da AWS.

# security_group.tf
resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.bastion.id]
  }

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

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

resource "aws_security_group" "rds" {
  name        = "rds-sg"
  description = "Security group for RDS"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }

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

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

resource "aws_security_group" "bastion" {
  name        = "bastion-sg"
  description = "Security group for bastion host"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.allowed_ssh_cidr]
  }

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

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

Agora vamos criar as instâncias EC2. A primeira é um bastion host (jump host) que você usa para acessar outras instâncias privadas. A segunda é um web server na subnet pública que será acessado via HTTP/HTTPS.

# ec2.tf
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "bastion" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.bastion.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2_profile.name
  key_name               = aws_key_pair.deployer.key_name

  tags = {
    Name = "bastion-host"
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  iam_instance_profile   = aws_iam_instance_profile.ec2_profile.name
  key_name               = aws_key_pair.deployer.key_name

  user_data = base64encode(templatefile("${path.module}/user_data.sh", {
    db_endpoint = aws_db_instance.main.endpoint
  }))

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

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = var.public_key
}

Note que usamos data.aws_ami para buscar a AMI Ubuntu mais recente automaticamente — isto é melhor que hardcoding um AMI ID porque evita que seu código fique desatualizado. O user_data é um script que roda na primeira inicialização da instância; aqui usamos templatefile para injetar o endpoint do RDS dinamicamente.

RDS: Banco de Dados Gerenciado

RDS (Relational Database Service) é o serviço gerenciado de banco de dados da AWS. Você especifica o engine (MySQL, PostgreSQL, MariaDB, etc.), o tamanho da instância, armazenamento, e o Terraform cuida do resto — backup automático, patch management, replicação multi-AZ são todos configuráveis. O grande diferencial do RDS é que você não administra o servidor — AWS cuida de disponibilidade, performance e segurança no nível do sistema operacional.

Ao criar um RDS, você deve colocá-lo em uma subnet privada e nunca expor seu endpoint públicamente. O acesso deve vir apenas de instâncias EC2 que precisam se conectar a ele, controlado via security groups. Também é crítico configurar backup retention e habilitar multi-AZ para produção — isto garante que você tenha cópias e que o banco continue rodando se uma availability zone inteira cair.

# rds.tf
resource "aws_db_subnet_group" "main" {
  name       = "main-db-subnet-group"
  subnet_ids = [aws_subnet.private.id, aws_subnet.private_az2.id]

  tags = {
    Name = "main-db-subnet-group"
  }
}

resource "aws_db_instance" "main" {
  identifier            = "myapp-db"
  allocated_storage    = 20
  db_name              = var.db_name
  engine               = "mysql"
  engine_version       = "8.0.35"
  instance_class       = "db.t3.micro"
  username             = var.db_username
  password             = random_password.db_password.result
  parameter_group_name = aws_db_parameter_group.main.name
  skip_final_snapshot  = false
  final_snapshot_identifier = "myapp-db-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}"

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  publicly_accessible    = false

  multi_az               = true
  backup_retention_days  = 30
  backup_window          = "03:00-04:00"
  maintenance_window     = "mon:04:00-mon:05:00"

  storage_encrypted      = true
  kms_key_id            = aws_kms_key.rds.arn

  enable_cloudwatch_logs_exports = ["error", "general", "slowquery"]

  tags = {
    Name = "myapp-database"
  }
}

resource "aws_db_parameter_group" "main" {
  family = "mysql8.0"
  name   = "myapp-params"

  parameter {
    name  = "character_set_server"
    value = "utf8mb4"
  }

  parameter {
    name  = "collation_server"
    value = "utf8mb4_unicode_ci"
  }

  tags = {
    Name = "myapp-parameter-group"
  }
}

resource "random_password" "db_password" {
  length  = 16
  special = true
}

resource "aws_secretsmanager_secret" "db_password" {
  name                    = "myapp/db/password"
  recovery_window_in_days = 7
}

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

resource "aws_kms_key" "rds" {
  description             = "KMS key for RDS encryption"
  deletion_window_in_days = 10
  enable_key_rotation     = true
}

resource "aws_kms_alias" "rds" {
  name          = "alias/myapp-rds"
  target_key_id = aws_kms_key.rds.key_id
}

resource "aws_subnet" "private_az2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
    Name = "private-subnet-az2"
  }
}

Vários pontos importantes aqui: usamos random_password para gerar uma senha segura e a armazenamos no AWS Secrets Manager, que é o lugar certo para guardar credenciais. O skip_final_snapshot = false força a criação de um snapshot antes de destruir o banco — isto salva você de acidentes catastróficos. Multi-AZ = true significa que o RDS é replicado sincronamente para outra availability zone. Encryption está habilitada com KMS, e exportamos logs do MySQL para CloudWatch para monitoramento e troubleshooting.

IAM: Controle de Acesso Granular

IAM (Identity and Access Management) é o sistema de controle de acesso da AWS. Você cria roles (conjuntos de permissões) e as associa a usuários, grupos, serviços ou, como fazemos aqui, a instâncias EC2. Uma role é composta de uma trust relationship (quem pode usar esta role) e policies (o que ela pode fazer).

O princípio fundamental do IAM é o principle of least privilege — uma aplicação ou usuário deve ter apenas as permissões que precisa para fazer seu trabalho. Se seu web server apenas precisa ler de S3 e escrever logs no CloudWatch, não dê permissão para deletar RDS ou modificar VPCs. Você vai criar policies detalhadas que especificam exatamente quais ações são permitidas em quais recursos.

# iam.tf
resource "aws_iam_role" "ec2_role" {
  name = "ec2-app-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_policy" "cloudwatch_logs" {
  name        = "ec2-cloudwatch-logs-policy"
  description = "Policy for EC2 to write logs to CloudWatch"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "logs:DescribeLogStreams"
        ]
        Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/myapp/*"
      }
    ]
  })
}

resource "aws_iam_policy" "s3_read" {
  name        = "ec2-s3-read-policy"
  description = "Policy for EC2 to read from specific S3 bucket"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          "arn:aws:s3:::myapp-bucket",
          "arn:aws:s3:::myapp-bucket/*"
        ]
      }
    ]
  })
}

resource "aws_iam_policy" "secretsmanager_read" {
  name        = "ec2-secretsmanager-policy"
  description = "Policy for EC2 to read database password from Secrets Manager"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue"
        ]
        Resource = aws_secretsmanager_secret.db_password.arn
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logs" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = aws_iam_policy.cloudwatch_logs.arn
}

resource "aws_iam_role_policy_attachment" "s3_read" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = aws_iam_policy.s3_read.arn
}

resource "aws_iam_role_policy_attachment" "secretsmanager_read" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = aws_iam_policy.secretsmanager_read.arn
}

resource "aws_iam_instance_profile" "ec2_profile" {
  name = "ec2-app-profile"
  role = aws_iam_role.ec2_role.name
}

data "aws_caller_identity" "current" {}

Observe que cada policy é específica — a política CloudWatch Logs só permite escrever em log groups que começam com /myapp/, a S3 só permite ler do bucket myapp-bucket, e Secrets Manager só permite ler o segredo específico do banco de dados. Quando você cria a instância EC2 com iam_instance_profile = aws_iam_instance_profile.ec2_profile.name, a instância automaticamente pode usar essas permissões — não precisa de credentials hardcoded no código ou em arquivos de configuração.

Variáveis, Outputs e Estrutura do Projeto

Para manter seu código reutilizável e seguro, você deve parametrizar valores que mudam entre ambientes. Use variables.tf para declarar e terraform.tfvars (ou arquivos de variáveis específicos por ambiente) para fornecer valores. Nunca commite terraform.tfvars com valores sensíveis — use git-crypt ou terraform cloud para gerenciar secrets.

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "db_name" {
  description = "Database name"
  type        = string
  sensitive   = true
}

variable "db_username" {
  description = "Database username"
  type        = string
  sensitive   = true
}

variable "public_key" {
  description = "Public SSH key for EC2 access"
  type        = string
  sensitive   = true
}

variable "allowed_ssh_cidr" {
  description = "CIDR block allowed to SSH to bastion"
  type        = string
  validation {
    condition     = can(cidrhost(var.allowed_ssh_cidr, 0))
    error_message = "allowed_ssh_cidr must be a valid CIDR block."
  }
}
# outputs.tf
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "bastion_public_ip" {
  description = "Public IP of the bastion host"
  value       = aws_instance.bastion.public_ip
}

output "web_server_public_ip" {
  description = "Public IP of the web server"
  value       = aws_instance.web.public_ip
}

output "rds_endpoint" {
  description = "RDS database endpoint"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}

output "rds_port" {
  description = "RDS database port"
  value       = aws_db_instance.main.port
}

output "database_password_secret_arn" {
  description = "ARN of the Secrets Manager secret containing the database password"
  value       = aws_secretsmanager_secret.db_password.arn
}

Uma estrutura de projeto bem organizada fica assim:

terraform/
├── main.tf              # Configurações gerais
├── provider.tf          # Provider AWS
├── variables.tf         # Declarações de variáveis
├── outputs.tf           # Declarações de outputs
├── terraform.tfvars     # Valores das variáveis (NÃO commitar secrets aqui)
├── vpc.tf               # Recursos de VPC
├── security_group.tf    # Security groups
├── ec2.tf               # Instâncias EC2
├── rds.tf               # Banco de dados RDS
├── iam.tf               # Roles e policies IAM
├── kms.tf               # Chaves KMS para criptografia
├── user_data.sh         # Script de inicialização da EC2
└── .terraform.lock.hcl  # Lock file do Terraform (commitar)

Fluxo de Execução: Plan, Apply e Destroy

Antes de aplicar qualquer mudança, você sempre executa terraform plan para ver o que será criado, modificado ou destruído. Este é seu safety net — revise cuidadosamente antes de aplicar.

# Inicializar o diretório Terraform
terraform init

# Ver o plano de execução
terraform plan -out=tfplan

# Aplicar as mudanças
terraform apply tfplan

# Verificar o estado
terraform state list
terraform state show aws_db_instance.main

# Destruir toda a infraestrutura (use com cuidado!)
terraform destroy

Um detalhe importante: terraform state é armazenado localmente em terraform.tfstate por padrão. Em produção, use remote state — guarde no S3 com versionamento e criptografia, ou use Terraform Cloud/Enterprise. Isto previne que alguém delete o arquivo local e perca o tracking da infraestrutura.

# backend.tf
terraform {
  backend "s3" {
    bucket         = "myapp-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Conclusão

Nesta aula, você aprendeu que Terraform permite descrever infraestrutura AWS de forma declarativa e versionável, transformando o gerenciamento de nuvem em código — é reproduzível, testável e colaborativo. Segundo, você compreendeu como VPC, EC2, RDS e IAM trabalham juntos: a VPC é o container de rede, EC2 são computadores nela, RDS é o banco de dados protegido em subnet privada, e IAM controla exatamente o que cada componente pode acessar. Terceiro, você aprendeu que segurança não é um add-on mas o fundamento — desde security groups que restringem tráfego até policies IAM granulares que seguem least privilege, cada decisão de arquitetura tem implicações de segurança.

Referências


Artigos relacionados