Ir para o conteúdo
Ferramenta para Plataforma1519 palavras

Terraform para Azure: Guia Completo de Deploy

Por Vivo de Código | Atualizado em 2024

Introdução

Se você já precisou criar manualmente recursos na Azure pelo portal, sabe o quanto isso é frágil, lento e propenso a erros. Agora imagine replicar essa mesma infraestrutura em três ambientes diferentes: desenvolvimento, homologação e produção. O trabalho manual se torna insustentável.

É aqui que o Terraform entra como peça fundamental. Desenvolvido pela HashiCorp, o Terraform permite que você defina toda a sua infraestrutura em arquivos declarativos, versionáveis e reproduzíveis. Quando combinado com a Azure, você ganha o poder de provisionar desde um simples Resource Group até arquiteturas complexas com AKS, Application Gateway e bancos gerenciados, tudo com um único comando.

Neste guia, vou compartilhar o que aprendi em anos de trabalho com Terraform na Azure em cenários reais de produção. Vamos cobrir desde a configuração inicial até boas práticas que vão evitar muita dor de cabeça no futuro.

Por que usar Terraform com Azure

Vantagens da infraestrutura como código na nuvem

A Azure oferece seu próprio mecanismo de IaC com ARM Templates e o mais recente Bicep. Então por que escolher Terraform? Existem razões técnicas sólidas para essa decisão.

Primeiro: Terraform é multi-cloud. Se a sua empresa utiliza Azure como cloud principal mas tem serviços na AWS ou GCP, você mantém uma única linguagem (HCL) para toda a infraestrutura. Isso reduz a curva de aprendizado da equipe e padroniza os processos de deploy.

Segundo: o estado (state) do Terraform funciona como uma fonte da verdade. Ele sabe exatamente o que existe na nuvem e calcula o plano de execução antes de aplicar qualquer mudança. Isso dá uma previsibilidade que ARM Templates simplesmente não oferecem da mesma forma.

Terceiro: o ecossistema de módulos é gigantesco. O Terraform Registry possui centenas de módulos prontos e testados para Azure, acelerando o desenvolvimento e promovendo reutilização.

Casos de uso reais onde essa combinação brilha

Na prática, a combinação Terraform e Azure se destaca em cenários como: provisionamento de clusters AKS com políticas de rede configuradas, criação de ambientes efêmeros para testes de integração (criando e destruindo infraestrutura automaticamente em pipelines de CI/CD), e gestão de múltiplas subscriptions com governança centralizada usando módulos compartilhados. Outro caso clássico é a migração gradual de infraestrutura legada, onde o Terraform permite importar recursos existentes e passar a gerenciá-los como código.

Setup inicial do Terraform com Azure

Instalando e configurando o ambiente

Antes de escrever qualquer linha de HCL, você precisa configurar três coisas: o Terraform CLI, o Azure CLI e a autenticação via Service Principal.

Instale o Terraform e o Azure CLI:

TYPESCRIPT
// Script de setup automatizado (Node.js/TypeScript)
// Arquivo: scripts/setup-azure-terraform.ts
 
import { execSync } from "child_process";
 
interface AzureConfig {
  subscriptionId: string;
  servicePrincipalName: string;
  resourceGroup: string;
  storageAccount: string;
  containerName: string;
}
 
function executeCommand(command: string, description: string): string {
  console.log(`[SETUP] ${description}...`);
  try {
    const result = execSync(command, { encoding: "utf-8" });
    console.log(`[OK] ${description} concluído.`);
    return result.trim();
  } catch (error) {
    console.error(`[ERRO] Falha ao executar: ${description}`);
    throw error;
  }
}
 
function createServicePrincipal(config: AzureConfig): void {
  // Define a subscription ativa
  executeCommand(
    `az account set --subscription ${config.subscriptionId}`,
    "Definindo subscription"
  );
 
  // Cria o Service Principal com role de Contributor
  const spOutput = executeCommand(
    `az ad sp create-for-rbac --name "${config.servicePrincipalName}" --role Contributor --scopes /subscriptions/${config.subscriptionId} --sdk-auth`,
    "Criando Service Principal"
  );
 
  console.log("Guarde estas credenciais com segurança:");
  console.log(spOutput);
}
 
function createRemoteBackend(config: AzureConfig): void {
  // Cria o Resource Group para o state
  executeCommand(
    `az group create --name ${config.resourceGroup} --location brazilsouth`,
    "Criando Resource Group para backend"
  );
 
  // Cria a Storage Account
  executeCommand(
    `az storage account create --name ${config.storageAccount} --resource-group ${config.resourceGroup} --sku Standard_LRS --encryption-services blob`,
    "Criando Storage Account"
  );
 
  // Cria o container para o state do Terraform
  executeCommand(
    `az storage container create --name ${config.containerName} --account-name ${config.storageAccount}`,
    "Criando container para Terraform state"
  );
}
 
// Execução principal
const config: AzureConfig = {
  subscriptionId: "sua-subscription-id-aqui",
  servicePrincipalName: "sp-terraform-prod",
  resourceGroup: "rg-terraform-state",
  storageAccount: "stterraformstateprod",
  containerName: "tfstate",
};
 
createServicePrincipal(config);
createRemoteBackend(config);

Agora, os comandos CLI que você vai executar diretamente no terminal:

Bash
# Login na Azure
az login
 
# Verificar a subscription ativa
az account show
 
# Verificar a instalação do Terraform
terraform version

Configurando o backend remoto

Nunca armazene o state do Terraform localmente em produção. Use sempre um backend remoto. Na Azure, a solução padrão é o Azure Blob Storage. Crie o arquivo de configuração principal:

HCL
# providers.tf
terraform {
  required_version = ">= 1.5.0"
 
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.85.0"
    }
  }
 
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstateprod"
    container_name       = "tfstate"
    key                  = "prod.terraform.tfstate"
  }
}
 
provider "azurerm" {
  features {}
}

Exemplo prático: provisionando infraestrutura completa

Criando uma aplicação web com banco de dados na Azure

Vamos criar um cenário real: uma aplicação web rodando em App Service com um banco PostgreSQL gerenciado e Application Insights para monitoramento.

HCL
# variables.tf
variable "environment" {
  type        = string
  description = "Ambiente de deploy (dev, staging, prod)"
  default     = "dev"
}
 
variable "location" {
  type        = string
  description = "Região da Azure"
  default     = "Brazil South"
}
 
variable "project_name" {
  type        = string
  description = "Nome do projeto"
  default     = "vivoapp"
}
HCL
# main.tf
locals {
  resource_suffix = "${var.project_name}-${var.environment}"
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "Terraform"
  }
}
 
resource "azurerm_resource_group" "main" {
  name     = "rg-${local.resource_suffix}"
  location = var.location
  tags     = local.common_tags
}
 
resource "azurerm_service_plan" "main" {
  name                = "asp-${local.resource_suffix}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  os_type             = "Linux"
  sku_name            = var.environment == "prod" ? "P1v3" : "B1"
  tags                = local.common_tags
}
 
resource "azurerm_linux_web_app" "main" {
  name                = "app-${local.resource_suffix}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  service_plan_id     = azurerm_service_plan.main.id
 
  site_config {
    application_stack {
      node_version = "18-lts"
    }
    always_on        = var.environment == "prod" ? true : false
    health_check_path = "/health"
  }
 
  app_settings = {
    "DATABASE_URL"                    = azurerm_postgresql_flexible_server.main.fqdn
    "APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.main.connection_string
    "NODE_ENV"                        = var.environment
  }
 
  tags = local.common_tags
}
 
resource "azurerm_postgresql_flexible_server" "main" {
  name                   = "psql-${local.resource_suffix}"
  resource_group_name    = azurerm_resource_group.main.name
  location               = azurerm_resource_group.main.location
  version                = "15"
  administrator_login    = "psqladmin"
  administrator_password = var.db_password
  storage_mb             = 32768
  sku_name               = var.environment == "prod" ? "GP_Standard_D2s_v3" : "B_Standard_B1ms"
  zone                   = "1"
 
  tags = local.common_tags
}
 
resource "azurerm_postgresql_flexible_server_database" "main" {
  name      = "appdb"
  server_id = azurerm_postgresql_flexible_server.main.id
  collation = "en_US.utf8"
  charset   = "utf8"
}
 
resource "azurerm_application_insights" "main" {
  name                = "appi-${local.resource_suffix}"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  application_type    = "Node.JS"
  tags                = local.common_tags
}

Para aplicar essa infraestrutura:

Bash
# Inicializar o Terraform
terraform init
 
# Ver o plano de execução
terraform plan -var="environment=dev" -var="db_password=SuaSenhaSegura123!"
 
# Aplicar as mudanças
terraform apply -var="environment=dev" -var="db_password=SuaSenhaSegura123!" -auto-approve

Automatizando validações com TypeScript

Em projetos maduros, é comum ter scripts de validação que rodam antes e depois do Terraform. Este segundo exemplo mostra como automatizar a verificação da infraestrutura provisionada:

TYPESCRIPT
// scripts/validate-infrastructure.ts
 
import { DefaultAzureCredential } from "@azure/identity";
import { WebSiteManagementClient } from "@azure/arm-appservice";
import { PostgreSQLManagementFlexibleServerClient } from "@azure/arm-postgresql-flexible";
 
interface ValidationResult {
  resource: string;
  status: "ok" | "erro";
  details: string;
}
 
async function validateInfrastructure(
  subscriptionId: string,
  resourceGroup: string,
  environment: string
): Promise<ValidationResult[]> {
  const credential = new DefaultAzureCredential();
  const results: ValidationResult[] = [];
 
  // Valida o App Service
  const webClient = new WebSiteManagementClient(credential, subscriptionId);
  try {
    const appName = `app-vivoapp-${environment}`;
    const app = await webClient.webApps.get(resourceGroup, appName);
 
    results.push({
      resource: "App Service",
      status: app.state === "Running" ? "ok" : "erro",
      details: `Estado: ${app.state} | Runtime: ${app.siteConfig?.linuxFxVersion}`,
    });
 
    // Verifica se o health check está configurado
    const healthCheckPath = app.siteConfig?.healthCheckPath;
    results.push({
      resource: "Health Check",
      status: healthCheckPath ? "ok" : "erro",
      details: healthCheckPath
        ? `Path configurado: ${healthCheckPath}`
        : "Health check não configurado",
    });
  } catch (error) {
    results.push({
      resource: "App Service",
      status: "erro",
      details: `Recurso não encontrado: ${error}`,
    });
  }
 
  // Valida o PostgreSQL
  const pgClient = new PostgreSQLManagementFlexibleServerClient(
    credential,
    subscriptionId
  );
  try {
    const serverName = `psql-vivoapp-${environment}`;
    const server = await pgClient.servers.get(resourceGroup, serverName);
 
    results.push({
      resource: "PostgreSQL Flexible Server",
      status: server.state === "Ready" ? "ok" : "erro",
      details: `Estado: ${server.state} | Versão: ${server.version} | SKU: ${server.sku?.name}`,
    });
 
    // Valida as databases
    const databases = pgClient.databases.listByServer(
      resourceGroup,
      serverName
    );
    let dbCount = 0;
    for await (const db of databases) {
      dbCount++;
      if (db.name === "appdb") {
        results.push({
          resource: "Database (appdb)",
          status: "ok",
          details: `Charset: ${db.charset} | Collation: ${db.collation}`,
        });
      }
    }
  } catch (error) {
    results.push({
      resource: "PostgreSQL",
      status: "erro",
      details: `Recurso não encontrado: ${error}`,
    });
  }
 
  return results;
}
 
// Execução
async function main(): Promise<void> {
  const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID || "";
  const environment = process.env.ENVIRONMENT || "dev";
  const resourceGroup = `rg-vivoapp-${environment}`;
 
  console.log(`Validando infraestrutura no ambiente: ${environment}\n`);
 
  const results = await validateInfrastructure(
    subscriptionId,
    resourceGroup,
    environment
  );
 
  let hasErrors = false;
  for (const result of results) {
    const indicator = result.status === "ok" ? "[OK]" : "[ERRO]";
    console.log(`${indicator} ${result.resource}: ${result.details}`);
    if (result.status === "erro") hasErrors = true;
  }
 
  if (hasErrors) {
    console.log("\nValidação finalizada com erros. Verifique os itens acima.");
    process.exit(1);
  }
 
  console.log("\nTodos os recursos validados com sucesso.");
}
 
main().catch(console.error);

Boas práticas de Terraform na Azure

Organização de arquivos e módulos

Evite colocar tudo em um único arquivo. Uma estrutura recomendada para projetos de médio e grande porte:

Text
infrastructure/
  modules/
    app-service/
      main.tf
      variables.tf
      outputs.tf
    database/
      main.tf
      variables.tf
      outputs.tf
  environments/
    dev/
      main.tf
      terraform.tfvars

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias