Quando gestisci infrastruttura su decine di account AWS, hai bisogno di pattern che scalino. In questo articolo condivido l’approccio che uso per gestire infrastruttura AWS multi-account e multi-ambiente usando Terraform workspaces, codice modulare e una strategia di tagging coerente.
Il Problema#
Immagina questo scenario: hai più scope organizzativi (team, business unit, progetti), ognuno con i propri account AWS per non-produzione e produzione. Sopra a questo, il tuo account non-produzione ospita più ambienti (dev, integrazione, certificazione). Moltiplica questo per diversi paesi o regioni, e hai molta infrastruttura da gestire.
L’approccio naive di copiare e incollare codice Terraform per ogni ambiente diventa rapidamente insostenibile. Hai bisogno di una strategia che ti permetta di definire l’infrastruttura una volta e distribuirla in modo coerente in tutti gli ambienti.
Separazione degli Ambienti Basata su Workspace#
I workspace Terraform sono la base di questo approccio. Ogni workspace mappa a un tier di ambiente:
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {}
}Usiamo la configurazione backend parziale con file .hcl per ambiente:
bucket = "my-scope-terraform-state-qual"
key = "my-service/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-lock"
encrypt = trueInizializza con il backend appropriato:
terraform init -backend-config=backend-qual.hcl
terraform workspace select qual || terraform workspace new qualVariabili Specifiche per Ambiente con Lookup Map#
Invece di file .tfvars separati, uso lookup map con chiave terraform.workspace. Questo mantiene tutto in un posto e rende immediatamente visibili le differenze tra ambienti:
locals {
environment = terraform.workspace
# Configurazione ECS per ambiente
ecs_cpu = lookup({
qual = 512
prod = 1024
}, terraform.workspace, 512)
ecs_memory = lookup({
qual = 1024
prod = 2048
}, terraform.workspace, 1024)
# Scaling Aurora Serverless v2
aurora_min_acu = lookup({
qual = 0.5
prod = 1
}, terraform.workspace, 0.5)
aurora_max_acu = lookup({
qual = 2
prod = 4
}, terraform.workspace, 2)
# Strategia capacity provider Fargate
fargate_spot_weight = lookup({
qual = 4
prod = 1
}, terraform.workspace, 4)
# Tag comuni applicati a tutte le risorse
common_tags = {
Environment = local.environment
Project = var.project_name
ManagedBy = "terraform"
Team = "platform"
}
}Questo pattern rende facile vedere a colpo d’occhio come gli ambienti differiscono. Non-produzione ottiene istanze più piccole e più capacità Spot; produzione ottiene istanze più grandi e più stabilità On-Demand.
Infrastruttura Modulare#
Ogni concernere infrastrutturale vive nel proprio modulo:
terraform/
modules/
alb/
aurora/
cloudwatch/
ecr/
ecs/
route53/
security-groups/
waf/
main.tf
locals.tf
terraform.tf
backend-qual.hcl
backend-prod.hclIl modulo root li compone:
module "ecs" {
source = "./modules/ecs"
service_name = var.service_name
cpu = local.ecs_cpu
memory = local.ecs_memory
image = "${module.ecr.repository_url}:latest"
target_group_arn = module.alb.target_group_arn
spot_weight = local.fargate_spot_weight
ondemand_weight = local.fargate_ondemand_weight
tags = local.common_tags
}
module "aurora" {
source = "./modules/aurora"
cluster_name = "${var.service_name}-${local.environment}"
min_acu = local.aurora_min_acu
max_acu = local.aurora_max_acu
tags = local.common_tags
}Il Pattern dei Security Group a Tre Tier#
Ogni servizio segue lo stesso modello di sicurezza a strati:
# ALB accetta HTTPS dal VPC
# ECS accetta traffico solo dall'ALB
# Aurora accetta connessioni solo dall'ECS
# Nessun CIDR hardcoded tra i tierIl principio chiave: ogni layer accetta solo traffico dal layer direttamente superiore. Nessun CIDR hardcoded tra i tier.
IAM con Permissions Boundary#
In un setup enterprise multi-account, tipicamente hai un layer di governance che limita cosa ogni scope può fare. I permissions boundary sono il meccanismo:
resource "aws_iam_role" "ecs_task" {
name = "${var.service_name}-ecs-task-${local.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
permissions_boundary = data.aws_iam_policy.scope_boundary.arn
tags = local.common_tags
}Ogni ruolo IAM ottiene il permissions boundary dello scope. Questo garantisce che anche se una policy di ruolo è troppo permissiva, non possa superare ciò che lo scope organizzativo permette.
Integrazione CI/CD#
La pipeline GitLab CI segue un flusso di promozione. Un commit su master attiva un plan; il merge su release attiva l’apply:
plan:qual:
stage: plan
script:
- terraform init -backend-config=backend-qual.hcl
- terraform workspace select qual
- terraform plan -out=plan.tfplan
rules:
- if: $CI_COMMIT_BRANCH == "master"
apply:qual:
stage: apply
script:
- terraform apply plan.tfplan
rules:
- if: $CI_COMMIT_BRANCH == "release"
when: manualLezioni Imparate#
Workspace invece di directory. Avere directory separate per ambiente porta a drift. I workspace con lookup map mantengono un’unica fonte di verità.
Moduli con opinioni. Ogni modulo dovrebbe incorporare le best practice (circuit breaker, Container Insights, policy di retention dei log) invece di esporre ogni parametro. Se il 90% dei servizi ha bisogno della stessa config, rendila il default.
Tagga tutto. Il tagging coerente su tutte le risorse è ciò che rende possibile l’allocazione dei costi, il reporting di conformità e la pulizia automatizzata su larga scala.
I permissions boundary non sono negoziabili. In un’impresa multi-team, hai bisogno di guardrail. I permissions boundary permettono ai team di operare autonomamente entro limiti sicuri.
Plan prima dell’apply, sempre. Anche in non-produzione. Un Terraform plan che mostra 47 risorse da eliminare è molto più economico da revisionare che da ripristinare.