O problema de subir infra de ML no clique
Quem treina modelo acaba criando bucket no console, sobe um endpoint SageMaker na mão, ajusta uma role IAM às pressas e segue para o próximo experimento. Funciona no começo. Depois ninguém lembra qual bucket guarda qual artefato, o endpoint de staging virou produção sem querer, e recriar o ambiente em outra conta leva um dia inteiro de cliques.
Terraform resolve isso descrevendo a infraestrutura como código. Você declara o que quer (um bucket, uma role, um endpoint), o Terraform calcula a diferença entre o desejado e o que existe, e aplica só o necessário.
Para ML isso importa mais que para uma API comum. Treino e inferência envolvem recursos caros e interdependentes: storage de artefatos, registry de imagens, permissões finas, GPU sob demanda. Errar uma permissão ou esquecer de destruir um endpoint custa dinheiro real.
Conceitos antes do código
Terraform gira em torno de quatro ideias.
Provider é o plugin que fala com a nuvem. Para AWS, o provider traduz seu código em chamadas de API.
Resource é cada peça de infra: um aws_s3_bucket, um aws_iam_role, um aws_sagemaker_endpoint.
State é o arquivo onde o Terraform guarda o que já criou. Sem ele, o Terraform não sabe o que existe. Em equipe, o state fica remoto (num bucket S3) com trava para dois não aplicarem ao mesmo tempo.
Plan e apply são o ciclo de trabalho. terraform plan mostra o que vai mudar antes de mexer em nada. terraform apply executa. Você lê o plan como quem lê um diff de PR.
Estrutura mínima de um projeto
Comece separando o que muda por ambiente do que é comum. Uma organização que funciona bem:
infra/
├── backend.tf # estado remoto
├── providers.tf # AWS + versão
├── variables.tf # entradas (região, ambiente, nome do projeto)
├── s3.tf # bucket de artefatos
├── iam.tf # roles de treino e inferência
├── sagemaker.tf # modelo e endpoint
└── envs/
├── staging.tfvars
└── prod.tfvars
O .tfvars carrega valores por ambiente. Assim o mesmo código sobe staging e produção sem duplicação.
Estado remoto: o primeiro passo
Antes de qualquer recurso de ML, configure o backend. O state precisa viver fora da sua máquina.
# backend.tf
terraform {
backend "s3" {
bucket = "minha-empresa-terraform-state"
key = "ml-infra/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks" # trava contra apply simultâneo
encrypt = true
}
}
A tabela DynamoDB guarda o lock. Se um colega estiver aplicando, seu apply espera em vez de corromper o state. Esse é o erro mais comum em times que começam com Terraform sem backend remoto.
Bucket de artefatos e registry
Modelo treinado precisa de onde morar. Um bucket versionado guarda checkpoints, métricas e o artefato final.
# s3.tf
resource "aws_s3_bucket" "artifacts" {
bucket = "${var.project}-${var.environment}-ml-artifacts"
}
resource "aws_s3_bucket_versioning" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
versioning_configuration {
status = "Enabled" # cada modelo sobrescrito mantém histórico
}
}
resource "aws_s3_bucket_lifecycle_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
id = "expire-old-checkpoints"
status = "Enabled"
filter {
prefix = "checkpoints/"
}
expiration {
days = 30 # checkpoints intermediarios somem; artefato final fica
}
}
}
O versionamento protege contra sobrescrever um modelo bom por engano. O lifecycle evita pagar storage eterno por checkpoints que ninguém vai usar.
Roles IAM com permissão mínima
Treino e inferência têm necessidades diferentes. Dar AdministratorAccess para o job de treino é o atalho que vira incidente de segurança depois.
# iam.tf
resource "aws_iam_role" "training" {
name = "${var.project}-${var.environment}-training"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "sagemaker.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
# Treino lê dados e escreve artefatos, nada mais
resource "aws_iam_role_policy" "training_s3" {
name = "training-s3-access"
role = aws_iam_role.training.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = [
aws_s3_bucket.artifacts.arn,
"${aws_s3_bucket.artifacts.arn}/*",
]
}]
})
}
Repare que a policy referencia aws_s3_bucket.artifacts.arn em vez de escrever o ARN na mão. Se o bucket mudar de nome, a permissão acompanha. Essa ligação entre recursos é o que torna Terraform mais seguro que scripts soltos.
Endpoint de inferência no SageMaker
O modelo servido precisa de três recursos encadeados: o modelo (aponta para o artefato e a imagem), a configuração do endpoint (tipo de instância) e o endpoint em si.
# sagemaker.tf
resource "aws_sagemaker_model" "this" {
name = "${var.project}-${var.environment}-model"
execution_role_arn = aws_iam_role.training.arn
primary_container {
image = var.inference_image # imagem no ECR
model_data_url = "s3://${aws_s3_bucket.artifacts.id}/models/latest/model.tar.gz"
}
}
resource "aws_sagemaker_endpoint_configuration" "this" {
name = "${var.project}-${var.environment}-config"
production_variants {
variant_name = "primary"
model_name = aws_sagemaker_model.this.name
initial_instance_count = var.instance_count
instance_type = var.instance_type # ml.t2.medium em staging, ml.m5.xlarge em prod
}
}
resource "aws_sagemaker_endpoint" "this" {
name = "${var.project}-${var.environment}-endpoint"
endpoint_config_name = aws_sagemaker_endpoint_configuration.this.name
}
O tipo de instância vem de variável. Staging roda numa instância barata, produção numa com mais memória. Mesmo código, .tfvars diferentes.
Ambientes separados sem duplicar código
As variáveis definem o que muda:
# variables.tf
variable "project" {
type = string
default = "credit-score"
}
variable "environment" {
type = string # "staging" ou "prod"
}
variable "instance_type" {
type = string
default = "ml.t2.medium"
}
variable "instance_count" {
type = number
default = 1
}
E cada ambiente preenche os valores:
# envs/prod.tfvars
environment = "prod"
instance_type = "ml.m5.xlarge"
instance_count = 2
Aplicar por ambiente fica explícito:
# Sempre revise o plan antes de aplicar
terraform plan -var-file=envs/prod.tfvars
terraform apply -var-file=envs/prod.tfvars
Onde Terraform em ML costuma dar errado
Alguns tropeços aparecem com frequência.
Guardar credenciais ou dados sensíveis no state. O terraform.tfstate grava valores de recursos em texto. Senha de banco, token de API, tudo fica lá. Por isso o backend S3 com encrypt = true e acesso restrito não é opcional.
Deixar endpoint de teste ligado. Um aws_sagemaker_endpoint cobra por hora enquanto existe. Em staging, terraform destroy ao fim do dia economiza. Melhor ainda: use ambientes efêmeros que sobem e caem no CI.
Misturar infra e pipeline de ML no mesmo state. Terraform provisiona a plataforma (bucket, role, endpoint). O deploy do modelo em si, atualizar o model_data_url para uma nova versão, costuma ser melhor no pipeline de CI/CD, não num terraform apply manual. Separe o que muda a cada release do que muda raramente.
Ignorar o plan. Aplicar direto sem ler o diff é como fazer merge sem revisar. Em recursos caros de ML, um plan mal lido pode recriar um endpoint e derrubar produção por minutos.
Encaixe com o resto do MLOps
Terraform ocupa a camada de fundação. Acima dele:
- O pipeline de dados (Glue, Lambda) grava features no bucket que o Terraform criou
- O CI de treino assume a role IAM que o Terraform declarou e escreve o artefato no bucket versionado
- O deploy atualiza o SageMaker model apontando para a nova versão do artefato
- O monitoramento lê métricas do endpoint provisionado
A regra prática: Terraform cuida do que existe uma vez e muda pouco. O pipeline cuida do que muda a cada modelo novo.
Conclusão
Terraform transforma infra de ML em código revisável, reproduzível e destrutível. Comece pelo estado remoto com trava, declare bucket de artefatos versionado e roles com permissão mínima, e provisione o endpoint por variável de ambiente.
O próximo passo é integrar o terraform plan no CI como gate de revisão e separar o provisionamento da plataforma do deploy do modelo. Com a fundação versionada, subir um ambiente novo deixa de ser um dia de cliques e vira um comando.