Laboratório de estudo de Terraform multicloud, que nasceu dos estudos na graduação em Cloud da FIAP. A ideia é subir a mesma aplicação nas duas nuvens: um site estático servido por 4 servidores web atrás de um load balancer, uma vez na AWS e outra na Azure, com o código organizado do mesmo jeito nas duas.
Cada nuvem tem a sua raiz Terraform com três módulos simétricos (network, compute e lb). O site é uma página só, que cada servidor preenche no primeiro boot com os próprios dados (nome, região, zona, hostname e IP), então dá para ver o balanceamento acontecendo: a cada recarga, outra instância responde.
| Módulo | AWS (terraform/aws) |
Azure (terraform/azure) |
|---|---|---|
network |
VPC, Internet Gateway, 2 subnets públicas (us-east-1a e us-east-1c), route table | VNet, 2 subnets, NSG com HTTP 80 (e SSH opcional) |
compute |
Security group, 4 EC2 Amazon Linux 2023 com Apache, IMDSv2 obrigatório | Availability set, 4 NICs, 4 VMs Ubuntu 22.04 com Apache, login só por chave SSH |
lb |
Application Load Balancer, target group com health check, listener HTTP 80 | IP público e Load Balancer Standard, probe HTTP, regra de entrada e regra de saída |
| AWS | Azure | Padrão |
|---|---|---|
instance_count |
vm_count |
4 |
instance_type |
vm_size |
t3.micro / Standard_B1s |
public_subnets |
subnets |
2 subnets /24 |
ssh_allowed_cidrs |
ssh_allowed_cidrs |
[] (SSH fechado) |
key_name |
admin_ssh_public_key |
opcional / obrigatório |
| · | dns_label |
obrigatório (nome DNS do IP público) |
| Output | O que traz |
|---|---|
site_url |
URL do site pelo load balancer (DNS do ALB ou FQDN do IP público da Azure) |
instances / vms |
Mapa nome => IP privado dos 4 servidores |
| Ponto | Como ficou |
|---|---|
| Senha das VMs | Não existe: a Azure usa só chave SSH (disable_password_authentication = true) |
| SSH | Fechado por padrão nas duas nuvens; abre só para os CIDRs de ssh_allowed_cidrs |
| HTTP nas EC2 | Liberado só a partir do security group do ALB |
| Metadados da EC2 | IMDSv2 obrigatório (http_tokens = "required") |
| Estado | Backend remoto com configuração parcial (backend.hcl, fora do git); *.tfvars e *.tfstate no .gitignore |
| Pipeline | apply e destroy só por disparo manual |
| Workflow | Quando roda | O que faz |
|---|---|---|
ci.yml |
todo push e PR | terraform fmt -check, init -backend=false e validate na AWS e na Azure; shellcheck do script do site; sobe a simulação local e confere que as 4 instâncias respondem |
deploy.yml |
só workflow_dispatch |
Escolhe a nuvem e a ação (plan, apply ou destroy) |
- Terraform (
>= 1.5) com os providers AWS (>= 5.70) e AzureRM (>= 4.5); - AWS: VPC, EC2, Application Load Balancer;
- Azure: Virtual Network, NSG, Linux Virtual Machines, Load Balancer Standard;
- Apache httpd nas instâncias, configurado por user data / cloud-init;
- HTML e CSS puros no site (sem dependências externas, com tema escuro automático);
- GitHub Actions para CI e deploy manual;
- Docker Compose + nginx para a simulação local do balanceamento.
studies-lab-multicloud-terraform/
├── terraform/
│ ├── aws/
│ │ ├── versions.tf # Providers e backend S3 (parcial)
│ │ ├── variables.tf · main.tf · outputs.tf
│ │ ├── terraform.tfvars.example · backend.hcl.example
│ │ └── modules/
│ │ ├── network/ # VPC, IGW, subnets, route table
│ │ ├── compute/ # SG, EC2 e user-data.sh.tftpl
│ │ └── lb/ # ALB, target group, listener
│ └── azure/
│ ├── versions.tf # Provider e backend azurerm (parcial)
│ ├── variables.tf · main.tf · outputs.tf
│ ├── terraform.tfvars.example · backend.hcl.example
│ └── modules/
│ ├── network/ # VNet, subnets, NSG
│ ├── compute/ # Availability set, NICs, VMs e cloud-init.sh.tftpl
│ └── lb/ # IP público, LB, probe, regras
├── site/
│ ├── index.html # Página com marcadores {{...}}
│ └── render.sh # Preenche os marcadores no boot
├── local/
│ ├── docker-compose.yml # 4 Apache + nginx em round-robin
│ └── nginx.conf
├── .github/workflows/ # ci.yml e deploy.yml
└── docs/ # arch.gif e demo.webp
- O Terraform lê
site/index.htmlesite/render.she embute os dois (em base64) no user data de cada EC2 e nocustom_datade cada VM. - No primeiro boot, o script instala o Apache, consulta o serviço de metadados da nuvem (IMDSv2 na AWS, IMDS na Azure) para saber região e zona, e roda o
render.sh, que troca os marcadores pelo nome, hostname e IP da máquina. - O load balancer checa a saúde de cada servidor com
GET /na porta 80 e só manda tráfego para os saudáveis. - O usuário abre o
site_url; a cada requisição o balanceador escolhe um servidor, e a página mostra qual respondeu. - Na Azure as VMs não têm IP público: entram pelo LB e saem para a internet (para o
apt) pela regra de outbound do próprio LB.
docker compose -f local/docker-compose.yml up -d --wait
# abra http://localhost:8080 e recarregue a página
docker compose -f local/docker-compose.yml downSobe 4 containers Apache com a mesma página, preenchida pelo mesmo render.sh, atrás de um nginx em round-robin fazendo o papel do load balancer. É daí que vem a demo do topo. A porta muda com LOCAL_PORT=9090.
cd terraform/aws
cp backend.hcl.example backend.hcl # bucket e tabela de lock do tfstate
cp terraform.tfvars.example terraform.tfvars
terraform init -backend-config=backend.hcl
terraform apply
terraform output site_urlaz login
export ARM_SUBSCRIPTION_ID="<id-da-assinatura>"
cd terraform/azure
cp backend.hcl.example backend.hcl # storage account do tfstate
cp terraform.tfvars.example terraform.tfvars # dns_label único
export TF_VAR_admin_ssh_public_key="$(cat ~/.ssh/id_ed25519.pub)"
terraform init -backend-config=backend.hcl
terraform apply
terraform output site_urlPara só testar sem backend remoto: terraform init -backend=false. Ao terminar: terraform destroy.
O deploy.yml roda pela aba Actions → Deploy (manual), escolhendo a nuvem e a ação. Ele usa o environment com o nome da nuvem e espera estes secrets:
| Secret / variável | Nuvem |
|---|---|
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN |
AWS |
ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID |
Azure |
ADMIN_SSH_PUBLIC_KEY (secret) e AZURE_DNS_LABEL (variável) |
Azure |
BACKEND_HCL: conteúdo do backend.hcl da nuvem |
as duas |
Sem credenciais (é o que o CI faz):
TF="docker run --rm -v $PWD:/w -w /w hashicorp/terraform:1.9"
$TF fmt -check -recursive
for c in aws azure; do
docker run --rm -v "$PWD":/w -w /w/terraform/$c hashicorp/terraform:1.9 init -backend=false
docker run --rm -v "$PWD":/w -w /w/terraform/$c hashicorp/terraform:1.9 validate
doneSimulação local: recarregar http://localhost:8080 8 vezes deve mostrar web-01, web-02, web-03 e web-04.
Com a infraestrutura no ar:
terraform output site_urldevolve o endereço do ALB (AWS) ou<dns_label>.brazilsouth.cloudapp.azure.com(Azure);curl -s <site_url> | grep -o 'web-<em>[0-9]*'(ouvm-) muda de instância entre as chamadas. O ALB distribui por requisição; o Load Balancer da Azure distribui por conexão (hash de 5 tuplas), então no navegador, que reaproveita a conexão, a troca aparece melhor comcurlou numa aba anônima;- o target group da AWS e o probe da Azure mostram os 4 servidores saudáveis;
- a página mostra a região e a zona reais lidas do serviço de metadados;
terraform destroyremove tudo ao final.
William Coelho · @willtechdev

