Laboratorio: Docker, Terraform, IAM ed ECS
1. Obiettivo e prerequisiti
Distribuire l'API del capstone come container su ECS/Fargate usando Terraform.
- Terraform: strumento Infrastructure as Code che confronta configurazione dichiarata e stato reale, producendo un piano di modifiche.
- ECS: orchestratore container AWS.
- Fargate: capacità serverless per eseguire task ECS senza gestire istanze.
- ECR: registry di immagini.
- task definition: specifica immagine, CPU, memoria, porte, log e ruoli.
- task: istanza della task definition.
- service: mantiene il numero desiderato di task e gestisce rollout.
Il laboratorio assume VPC, subnet private e target group ALB già creati da un modulo di networking separato. Separare networking e servizio riduce lo scope, ma gli input restano espliciti.
2. Dockerfile
FROM python:3.12-slim AS builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /build
COPY pyproject.toml .
COPY src/ src/
RUN pip install --no-cache-dir .
FROM python:3.12-slim AS runtime
RUN useradd --create-home --uid 10001 appuser
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
PYTHONUNBUFFERED="1"
USER appuser
EXPOSE 8080
CMD ["uvicorn", "ai_ops.api.app:app", "--host=0.0.0.0", "--port=8080"]
Build e test:
docker build --tag ai-ops:local .
docker run --rm --publish 8080:8080 --read-only --tmpfs /tmp ai-ops:local
curl http://localhost:8080/health/live
Usa un digest immutabile nel deploy:
3. Struttura Terraform
Non committare terraform.tfstate: può contenere dati sensibili. In team usa backend
remoto S3 con versioning, cifratura e locking.
4. Provider e variabili
terraform {
required_version = ">= 1.8"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "ai-ops"
Environment = var.environment
ManagedBy = "terraform"
}
}
}
variable "aws_region" { type = string }
variable "environment" { type = string }
variable "image_uri" { type = string }
variable "private_subnet_ids" { type = list(string) }
variable "service_security_group" { type = string }
variable "target_group_arn" { type = string }
variable "knowledge_bucket_arn" { type = string }
variable "provider_secret_arn" { type = string }
In CI valida che image_uri contenga @sha256: invece di un tag mutabile.
5. Due ruoli distinti
Il execution role viene usato da ECS per avviare il task, leggere secret referenziati e inviare log. Il task role viene assunto dall'applicazione per accedere a S3 o altri servizi. Non confonderli.
data "aws_iam_policy_document" "ecs_assume" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "execution" {
name = "ai-ops-${var.environment}-execution"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
resource "aws_iam_role_policy_attachment" "execution_managed" {
role = aws_iam_role.execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
data "aws_iam_policy_document" "execution_secret" {
statement {
actions = ["secretsmanager:GetSecretValue"]
resources = [var.provider_secret_arn]
}
}
resource "aws_iam_role_policy" "execution_secret" {
role = aws_iam_role.execution.id
policy = data.aws_iam_policy_document.execution_secret.json
}
resource "aws_iam_role" "task" {
name = "ai-ops-${var.environment}-task"
assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}
data "aws_iam_policy_document" "task" {
statement {
actions = ["s3:GetObject"]
resources = ["${var.knowledge_bucket_arn}/approved/*"]
}
}
resource "aws_iam_role_policy" "task" {
role = aws_iam_role.task.id
policy = data.aws_iam_policy_document.task.json
}
La policy non concede s3:*, non usa resource * e non consente scrittura. Aggiungi
permessi solo quando un acceptance test li richiede.
6. Cluster, log e task definition
resource "aws_ecs_cluster" "main" {
name = "ai-ops-${var.environment}"
}
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/ai-ops/${var.environment}"
retention_in_days = 30
}
resource "aws_ecs_task_definition" "api" {
family = "ai-ops-${var.environment}-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 512
memory = 1024
execution_role_arn = aws_iam_role.execution.arn
task_role_arn = aws_iam_role.task.arn
container_definitions = jsonencode([{
name = "api"
image = var.image_uri
essential = true
readonlyRootFilesystem = true
portMappings = [{
containerPort = 8080
protocol = "tcp"
}]
environment = [
{ name = "ENVIRONMENT", value = var.environment },
{ name = "LOG_LEVEL", value = "INFO" }
]
secrets = [{
name = "OPENAI_API_KEY"
valueFrom = var.provider_secret_arn
}]
healthCheck = {
command = ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/health/live')\""]
interval = 30
timeout = 5
retries = 3
startPeriod = 20
}
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "api"
}
}
}])
}
CPU e memoria sono una baseline da load testare. Il secret viene risolto dal runtime,
non incluso nell'immagine o nel file .tfvars.
7. Service
resource "aws_ecs_service" "api" {
name = "ai-ops-${var.environment}-api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
health_check_grace_period_seconds = 30
network_configuration {
subnets = var.private_subnet_ids
security_groups = [var.service_security_group]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.target_group_arn
container_name = "api"
container_port = 8080
}
}
Le subnet private richiedono egress controllato verso provider esterni, tramite NAT o proxy/VPC endpoint quando disponibile. Il security group accetta traffico solo dall'ALB.
8. Workflow
terraform fmt -check
terraform init
terraform validate
terraform plan -var-file=environments/staging.tfvars
review del plan
terraform apply del plan approvato
smoke test
canary/monitor
In produzione separa account/ruoli e richiedi approval. terraform apply non sostituisce
quality gate applicativo e rollback.
9. Acceptance test infrastrutturali
- task eseguito come UID non-root;
- filesystem root read-only;
- immagine referenziata per digest;
- secret assente da image, log, state e plan;
- task role non può leggere path S3 di un altro tenant;
- porta task accessibile solo da ALB;
- health check fallito impedisce il rollout;
- due task distribuiti su Availability Zone quando richiesto;
- log retention definita;
- rollback alla task definition precedente provato.
10. Costo e cleanup
Stima separatamente Fargate, ALB, NAT, log, data transfer, storage e provider LLM. In ambienti lab, NAT e ALB possono costare più del container.
Non creare risorse cloud solo per spuntare una casella. È accettabile consegnare:
terraform validatee plan revisionato;- diagramma e threat model;
- deploy temporaneo con screenshot/metriche;
terraform destroyverificato;- costo mensile stimato.