<h2>Fundamentos do Terraform e AWS</h2>
<p>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 <em>como</em> fazer algo, o Terraform descreve <em>o que</em> 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.</p>
<p>A estrutura básica de um projeto Terraform consiste em arquivos <code>.tf</code> 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 <code>terraform apply</code>, a ferramenta cria um arquivo <code>terraform.tfstate</code> que rastreia o estado atual da infraestrutura. Este arquivo é crítico e deve ser versionado com cuidado em ambientes reais.</p>
<pre><code class="language-hcl"># provider.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}</code></pre>
<p>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: <code>aws sts get-caller-identity</code>. Se retornar seus dados de account ID, você está pronto.</p>
<h2>VPC: Fundação de Toda Infraestrutura AWS</h2>
<p>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).</p>
<p>Quando você cria uma VPC, precisa definir um CIDR block — um intervalo de endereços IP privados. Por exemplo, <code>10.0.0.0/16</code> oferece 65.536 endereços IP. As subnets são divisões deste bloco: você pode ter <code>10.0.1.0/24</code> (256 IPs) como subnet pública e <code>10.0.2.0/24</code> 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.</p>
<pre><code class="language-hcl"># 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"
}</code></pre>
<p>Neste código, criamos uma VPC com CIDR <code>10.0.0.0/16</code>, 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 <code>map_public_ip_on_launch = true</code>, o que significa que qualquer EC2 lançada nela recebe automaticamente um IP público.</p>
<h2>EC2 e Security Groups: Computação na Nuvem</h2>
<p>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.</p>
<p>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 <code>0.0.0.0/0</code> 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.</p>
<pre><code class="language-hcl"># 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"
}
}</code></pre>
<p>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.</p>
<pre><code class="language-hcl"># 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
}</code></pre>
<p>Note que usamos <code>data.aws_ami</code> para buscar a AMI Ubuntu mais recente automaticamente — isto é melhor que hardcoding um AMI ID porque evita que seu código fique desatualizado. O <code>user_data</code> é um script que roda na primeira inicialização da instância; aqui usamos <code>templatefile</code> para injetar o endpoint do RDS dinamicamente.</p>
<h2>RDS: Banco de Dados Gerenciado</h2>
<p>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.</p>
<p>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.</p>
<pre><code class="language-hcl"># 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"
}
}</code></pre>
<p>Vários pontos importantes aqui: usamos <code>random_password</code> para gerar uma senha segura e a armazenamos no AWS Secrets Manager, que é o lugar certo para guardar credenciais. O <code>skip_final_snapshot = false</code> 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.</p>
<h2>IAM: Controle de Acesso Granular</h2>
<p>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).</p>
<p>O princípio fundamental do IAM é o <em>principle of least privilege</em> — 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.</p>
<pre><code class="language-hcl"># 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" {}</code></pre>
<p>Observe que cada policy é específica — a política CloudWatch Logs só permite escrever em log groups que começam com <code>/myapp/</code>, a S3 só permite ler do bucket <code>myapp-bucket</code>, e Secrets Manager só permite ler o segredo específico do banco de dados. Quando você cria a instância EC2 com <code>iam_instance_profile = aws_iam_instance_profile.ec2_profile.name</code>, a instância automaticamente pode usar essas permissões — não precisa de credentials hardcoded no código ou em arquivos de configuração.</p>
<h2>Variáveis, Outputs e Estrutura do Projeto</h2>
<p>Para manter seu código reutilizável e seguro, você deve parametrizar valores que mudam entre ambientes. Use <code>variables.tf</code> para declarar e <code>terraform.tfvars</code> (ou arquivos de variáveis específicos por ambiente) para fornecer valores. Nunca commite <code>terraform.tfvars</code> com valores sensíveis — use <code>git-crypt</code> ou <code>terraform cloud</code> para gerenciar secrets.</p>
<pre><code class="language-hcl"># 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."
}
}</code></pre>
<pre><code class="language-hcl"># 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
}</code></pre>
<p>Uma estrutura de projeto bem organizada fica assim:</p>
<pre><code>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)</code></pre>
<h2>Fluxo de Execução: Plan, Apply e Destroy</h2>
<p>Antes de aplicar qualquer mudança, você sempre executa <code>terraform plan</code> para ver o que será criado, modificado ou destruído. Este é seu safety net — revise cuidadosamente antes de aplicar.</p>
<pre><code class="language-bash"># 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</code></pre>
<p>Um detalhe importante: <code>terraform state</code> é armazenado localmente em <code>terraform.tfstate</code> por padrão. Em produção, use <em>remote state</em> — 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.</p>
<pre><code class="language-hcl"># backend.tf
terraform {
backend "s3" {
bucket = "myapp-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}</code></pre>
<h2>Conclusão</h2>
<p>Nesta aula, você aprendeu que <strong>Terraform permite descrever infraestrutura AWS de forma declarativa e versionável</strong>, transformando o gerenciamento de nuvem em código — é reproduzível, testável e colaborativo. Segundo, você compreendeu como <strong>VPC, EC2, RDS e IAM trabalham juntos</strong>: 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 <strong>segurança não é um add-on mas o fundamento</strong> — 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" target="_blank" rel="noopener noreferrer">Terraform AWS Provider Documentation</a></li>
<li><a href="https://docs.aws.amazon.com/vpc/latest/userguide/" target="_blank" rel="noopener noreferrer">AWS VPC User Guide</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_BestPractices.html" target="_blank" rel="noopener noreferrer">AWS RDS Best Practices</a></li>
<li><a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" target="_blank" rel="noopener noreferrer">AWS IAM Best Practices</a></li>
<li><a href="https://developer.hashicorp.com/terraform/language/state" target="_blank" rel="noopener noreferrer">Terraform State Management</a></li>
</ul>
<p><!-- FIM --></p>