Quinto post da série. No anterior, entramos no detalhe da GPU. Agora é hora de automatizar o que fica ao redor dela. Entender GPU é metade do trabalho. Provisionar tudo isso de forma consistente, repetível e auditável é a outra metade.

tl;dr: IaC em AI não é capricho. É o que evita SKU errado, drift, cluster caro esquecido e pipeline sem trilha de auditoria.

O typo de $4.000

Imagina o cenário: você provisiona um cluster GPU manualmente em East US 2 pra um experimento de ML. AKS com node pool Standard_NC6s_v3, accelerated networking, drivers NVIDIA, taints corretos. Leva quase um dia, mas funciona.

Três semanas depois, o mesmo time precisa do setup idêntico em West US 3. Você abre o portal, volta em thread de Slack, consulta uma wiki e tenta completar o resto de cabeça.

Alguém digita o SKU errado. No lugar de Standard_NC6s_v3, o pool sobe com 18 nós Standard_D64s_v5. O job tenta rodar, não acha CUDA e cai pra CPU. Como nada falha de forma óbvia, o time percebe tarde. Três dias depois, a conta passa de $4.000 em compute CPU que nem servia pro workload.

Esse tipo de cena é mais comum do que parece. E é exatamente o tipo de prejuízo que IaC evita.

Por que IaC é não-negociável pra AI

Infra de aplicação web tradicional tolera erro barato. Infra de AI, não. Uma configuração ruim num App Service machuca pouco. Um cluster GPU ou um pool CPU superdimensionado no lugar errado machuca rápido.

RazãoPor que importa pra AI
ComplexidadeQuotas GPU por região, versões de driver, taints, InfiniBand, NVMe ephemeral, private endpoints. Ninguém segura isso tudo na cabeça
Custo4 nós NC24ads_A100_v4 passam de ~$350 por dia. 4 nós ND96asr_v4 passam de $2.600 por dia
ReprodutibilidadeML experiments precisam ser repetíveis. Mesmo SKU, driver e topologia de rede
ComplianceQuem mudou o quê, quando e por quê. Git já entrega esse trilho de auditoria

Tradução infra ↔ AI: quando o ML engineer diz “preciso do mesmo ambiente da semana passada”, ele está pedindo reprodutibilidade de infra. Quando compliance pergunta “o que mudou”, está pedindo audit trail. IaC responde os dois com o mesmo artefato: código versionado.

Landscape de IaC pra AI

CritérioTerraformBicepAzure CLIPulumi
ParadigmaDeclarativoDeclarativoImperativoDeclarativo (code)
Multi-cloudSimNão, só AzureNão, só AzureSim
State managementRemote state fileNenhum (ARM gerencia)NenhumRemote state file
LinguagemHCLBicep DSLBash/PowerShellPython, TS, Go, C#
Learning curveModeradaBaixa (times Azure-native)BaixaModerada-Alta
Melhor praPlataformas multi-cloudTimes 100% AzureAutomação rápida, glueTimes developer-first

Quando usar cada um: Terraform quando você precisa de multi-cloud ou platform engineering em escala. Bicep quando é 100% Azure e você quer o caminho mais curto. Azure CLI pra glue, prototipagem e operação ad-hoc. Em muita empresa, tudo isso convive bem: Terraform ou Bicep pro provisioning, Azure CLI pra operações e GitHub Actions pra orquestrar.

Terraform pra infra de AI

Variables com validação (previne typo)

variable "gpu_vm_size" {
  description = "VM SKU for GPU node pool"
  type        = string
  default     = "Standard_NC6s_v3"

  validation {
    condition     = can(regex("^Standard_N", var.gpu_vm_size))
    error_message = "GPU VM size must be an N-series SKU (e.g., Standard_NC6s_v3, Standard_NC24ads_A100_v4)."
  }
}

variable "gpu_max_nodes" {
  description = "Maximum number of GPU nodes for autoscaling"
  type        = number
  default     = 5
}

Essa validation não está ali de enfeite. Ela pega o erro no terraform plan, quando ainda é barato corrigir.

AKS com GPU node pool

resource "azurerm_kubernetes_cluster" "ai" {
  name                = "aks-ai-${var.environment}"
  location            = azurerm_resource_group.ai.location
  resource_group_name = azurerm_resource_group.ai.name
  dns_prefix          = "aks-ai-${var.environment}"
  kubernetes_version  = "1.30"

  default_node_pool {
    name            = "system"
    vm_size         = "Standard_D4s_v5"
    node_count      = 2
    os_disk_size_gb = 128

    upgrade_settings {
      max_surge = "33%"
    }
  }

  identity {
    type = "SystemAssigned"
  }

  network_profile {
    network_plugin = "azure"
    network_policy = "calico"
  }
}

resource "azurerm_kubernetes_cluster_node_pool" "gpu" {
  name                  = "gpu"
  kubernetes_cluster_id = azurerm_kubernetes_cluster.ai.id
  vm_size               = var.gpu_vm_size
  mode                  = "User"
  os_disk_size_gb       = 256
  auto_scaling_enabled  = true
  min_count             = 0
  max_count             = var.gpu_max_nodes

  node_taints = [
    "sku=gpu:NoSchedule"
  ]

  node_labels = {
    "hardware" = "gpu"
    "gpu-type" = "nvidia"
    "workload" = "ai"
  }
}

O taint sku=gpu:NoSchedule é o tipo de detalhe que salva dinheiro. Sem ele, DaemonSet de monitoramento, agente de log e workload sem GPU acabam pousando nos nós mais caros do cluster.

Remote state (obrigatório)

Nunca guarde state de Terraform localmente quando está lidando com infra cara. Se você perder o state, perde também o mapa do que precisa destruir ou reconciliar.

terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "stterraformstate"
    container_name       = "tfstate"
    key                  = "ai-platform.terraform.tfstate"
    use_oidc             = true
  }
}

Setup do storage, uma vez:

az group create --name rg-terraform-state --location eastus2

az storage account create \
  --name stterraformstate \
  --resource-group rg-terraform-state \
  --sku Standard_LRS \
  --encryption-services blob

az storage container create \
  --name tfstate \
  --account-name stterraformstate \
  --auth-mode login

Bicep pra infra de AI

A grande vantagem do Bicep é simples: sem state file, sem backend, sem locking. O ARM cuida disso. Pra time 100% Azure, isso tira uma categoria inteira de preocupação operacional.

GPU VM com NVIDIA Driver Extension

No exemplo abaixo, eu assumo que VNet, subnet e NIC já vieram de um módulo de rede e que a NIC existe quando esse arquivo roda.

@allowed([
  'Standard_NC6s_v3'
  'Standard_NC12s_v3'
  'Standard_NC24ads_A100_v4'
  'Standard_NC48ads_A100_v4'
  'Standard_NC96ads_A100_v4'
])
@description('GPU VM size: must be an N-series SKU')
param vmSize string = 'Standard_NC6s_v3'

param vmName string = 'vm-gpu-ai'
param nicName string = 'nic-gpu-ai'
param location string = resourceGroup().location
param adminUsername string = 'azureuser'
param sshPublicKey string

resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' existing = {
  name: nicName
}

resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
  name: vmName
  location: location
  properties: {
    hardwareProfile: {
      vmSize: vmSize
    }
    osProfile: {
      computerName: vmName
      adminUsername: adminUsername
      linuxConfiguration: {
        disablePasswordAuthentication: true
        ssh: {
          publicKeys: [
            {
              path: '/home/${adminUsername}/.ssh/authorized_keys'
              keyData: sshPublicKey
            }
          ]
        }
      }
    }
    storageProfile: {
      imageReference: {
        publisher: 'Canonical'
        offer: '0001-com-ubuntu-server-jammy'
        sku: '22_04-lts-gen2'
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
        managedDisk: {
          storageAccountType: 'Premium_LRS'
        }
        diskSizeGB: 256
      }
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: nic.id
        }
      ]
    }
  }
}

resource nvidiaExtension 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = {
  parent: vm
  name: 'NvidiaGpuDriverLinux'
  location: location
  properties: {
    publisher: 'Microsoft.HpcCompute'
    type: 'NvidiaGpuDriverLinux'
    typeHandlerVersion: '1.9'
    autoUpgradeMinorVersion: true
  }
}

O decorator @allowed cumpre o mesmo papel da validation do Terraform: barra SKU errado antes de você descobrir o erro no faturamento.

Estrutura modular pra produção

Estrutura modular pra produçãoDiagrama da pasta infra com o arquivo main.bicep, a pasta modules e a pasta parameters.infra/main.bicepOrquestradormodules/network.bicepVNet, subnets, NSGs, private endpointsaks.bicepAKS cluster com GPU node poolstorage.bicepStorage account pra modelos e dadosmonitoring.bicepLog Analytics, alerts, dashboardskeyvault.bicepKey Vault pra secretsparameters/dev.bicepparamstaging.bicepparamprod.bicepparam

Um time novo consegue subir um ambiente inteiro, com padrão e compliance, mexendo só num arquivo de parâmetros.

CI/CD: plan -> approve -> apply

Mudança de infra de AI não deveria sair do laptop de ninguém direto pra produção. Pipeline serve justamente pra pôr validação, revisão e trilha de auditoria no caminho.

GitHub Actions com OIDC

name: "AI Infrastructure: Plan & Apply"

on:
  push:
    branches: [main]
    paths: ["infra/**"]
  pull_request:
    branches: [main]
    paths: ["infra/**"]

permissions:
  id-token: write
  contents: read
  pull-requests: write

env:
  ARM_USE_OIDC: true
  ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
  ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
  ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

jobs:
  plan:
    name: "Terraform Plan"
    runs-on: ubuntu-latest
    environment: ai-infrastructure
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.9.0"
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: terraform init
        working-directory: infra
      - run: terraform plan -out=tfplan -input=false
        working-directory: infra
      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: infra/tfplan

  apply:
    name: "Terraform Apply"
    runs-on: ubuntu-latest
    needs: plan
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: ai-infrastructure-prod
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.9.0"
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - uses: actions/download-artifact@v4
        with:
          name: tfplan
          path: infra
      - run: terraform init
        working-directory: infra
      - run: terraform apply -input=false -auto-approve tfplan
        working-directory: infra

O fluxo é simples: PR gera plan. Merge em main dispara apply, mas só depois da proteção de environment e do artifact do plan atravessarem o pipeline. Assim o que foi revisado é exatamente o que será aplicado.

Sempre fixe versões de actions. @v4, @v3, @v2. Usar @latest em pipeline de produção é pedir surpresa ruim no pior dia possível.

Governance: guardrails pra GPU

Azure Policy pode enforçar regra no nível da subscription. Pra infra de AI, uma policy simples e útil é barrar VM GPU sem tag cost-center:

{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Compute/virtualMachines"
        },
        {
          "field": "Microsoft.Compute/virtualMachines/hardwareProfile.vmSize",
          "in": [
            "Standard_NC24ads_A100_v4",
            "Standard_NC48ads_A100_v4",
            "Standard_ND96asr_v4"
          ]
        },
        {
          "field": "tags['cost-center']",
          "exists": "false"
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  }
}

Sem tag de cost center, sem GPU. Parece rígido. Na prática, é o tipo de rigidez que evita o cluster esquecido de sexta-feira.

Fechando o loop

Da próxima vez que alguém pedir “o mesmo ambiente da semana passada”, você não precisa catar SKU em thread de Slack. Você aponta pro repositório, roda o pipeline e evita outro typo de $4.000.

Leitura complementar

No próximo post

Agora que a infra está automatizada e governada, o próximo passo é falar do ciclo de vida do modelo: MLOps. Como um modelo sai de “funciona no meu notebook” pra “roda em produção com SLA”. O que muda pra quem é de infra, e o que o time de ML espera de você nesse processo.