Chega de teoria. Vamos subir uma aplicação de verdade. Você vai criar uma API REST de "lista de tarefas" em Go, com banco PostgreSQL no RDS e upload de anexos no S3, rodando atrás de um Load Balancer com 2 instâncias EC2 em AZs diferentes.
O que vamos construir
Arquitetura final:
- VPC com 2 subnets públicas (para o ALB) e 2 subnets privadas (para EC2 e RDS)
- ALB recebendo HTTP na porta 80
- Auto Scaling Group com 2 EC2 (
t3.micro) rodando a API Go - RDS PostgreSQL Multi-AZ
- Bucket S3 para uploads
- IAM Role na EC2 dando acesso ao S3 e Secrets Manager
- Senha do banco no Secrets Manager
- Logs e métricas no CloudWatch
Pré-requisitos
- Conta AWS (Free Tier resolve)
- AWS CLI instalado e configurado (
aws configure) - Go 1.26+ na sua máquina (para compilar o binário localmente)
- Um par de chaves SSH gerado (
.pem)
Passo a passo
Passo 1 — Criar a VPC e subnets
Vamos usar a VPC default que já existe na sua conta. Ela já tem subnets públicas em todas as AZs. Para um projeto sério você criaria uma VPC dedicada, mas para o aprendizado simplifica.
# Listar VPCs existentes
aws ec2 describe-vpcs --filters "Name=is-default,Values=true"
# Listar subnets da VPC default
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<VPC_ID>"Passo 2 — Criar Security Groups
Vamos criar 3 SGs:
sg-alb: aceita 80 da internet (0.0.0.0/0)sg-app: aceita 8080 apenas vindo desg-albsg-db: aceita 5432 apenas vindo desg-app
# Criar SG do ALB
aws ec2 create-security-group --group-name sg-alb \
--description "ALB SG" --vpc-id <VPC_ID>
aws ec2 authorize-security-group-ingress --group-id <SG_ALB_ID> \
--protocol tcp --port 80 --cidr 0.0.0.0/0
# Criar SG da aplicação
aws ec2 create-security-group --group-name sg-app \
--description "App SG" --vpc-id <VPC_ID>
aws ec2 authorize-security-group-ingress --group-id <SG_APP_ID> \
--protocol tcp --port 8080 --source-group <SG_ALB_ID>
# Criar SG do banco
aws ec2 create-security-group --group-name sg-db \
--description "DB SG" --vpc-id <VPC_ID>
aws ec2 authorize-security-group-ingress --group-id <SG_DB_ID> \
--protocol tcp --port 5432 --source-group <SG_APP_ID>Passo 3 — Criar o RDS PostgreSQL
Primeiro, guarde a senha no Secrets Manager:
aws secretsmanager create-secret \
--name prod/tasks/db \
--secret-string '{"username":"appuser","password":"StrongPassword123!"}'Agora crie o RDS:
aws rds create-db-instance \
--db-instance-identifier tasks-db \
--db-instance-class db.t3.micro \
--engine postgres --engine-version 16.1 \
--master-username appuser \
--master-user-password 'StrongPassword123!' \
--allocated-storage 20 --multi-az \
--vpc-security-group-ids <SG_DB_ID> \
--backup-retention-period 7 \
--no-publicly-accessible \
--storage-encrypted
# Aguarde alguns minutos. Status final = 'available'
aws rds describe-db-instances --db-instance-identifier tasks-dbPasso 4 — Criar o bucket S3
# Nome global e único, escolha algo único
aws s3api create-bucket \
--bucket tasks-uploads-2026 \
--region sa-east-1 \
--create-bucket-configuration LocationConstraint=sa-east-1
# Habilitar criptografia no bucket
aws s3api put-bucket-encryption \
--bucket tasks-uploads-2026 \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
# Bloquear acesso público
aws s3api put-public-access-block \
--bucket tasks-uploads-2026 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=truePasso 5 — Criar a IAM Role para a EC2
A role permite que a EC2 acesse S3 e Secrets Manager sem precisar de access key.
# Trust policy (quem pode assumir a role)
cat > trust.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOF
aws iam create-role --role-name TasksAppRole \
--assume-role-policy-document file://trust.json
# Permission policy
cat > policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject","s3:GetObject"],
"Resource": "arn:aws:s3:::tasks-uploads-2026/*"
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:*:*:secret:prod/tasks/db-*"
}
]
}
EOF
aws iam put-role-policy --role-name TasksAppRole \
--policy-name TasksAppPolicy --policy-document file://policy.json
# Instance profile (necessário para anexar à EC2)
aws iam create-instance-profile --instance-profile-name TasksAppProfile
aws iam add-role-to-instance-profile \
--instance-profile-name TasksAppProfile \
--role-name TasksAppRolePasso 6 — Código da API em Go
A API usa só a biblioteca padrão (net/http, database/sql, encoding/json) mais pgx para PostgreSQL e o AWS SDK v2 para acessar o Secrets Manager com a IAM Role — sem access key no código.
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/jackc/pgx/v5/pgxpool"
)
type Task struct {
ID int64 `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
}
type dbSecret struct {
Username string `json:"username"`
Password string `json:"password"`
}
// loadDBSecret busca credenciais no Secrets Manager usando a IAM Role da EC2.
// Nenhuma access key no código — a role cuida da autenticação.
func loadDBSecret(ctx context.Context) (dbSecret, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("sa-east-1"))
if err != nil {
return dbSecret{}, fmt.Errorf("aws config: %w", err)
}
sm := secretsmanager.NewFromConfig(cfg)
out, err := sm.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: aws.String("prod/tasks/db"),
})
if err != nil {
return dbSecret{}, fmt.Errorf("get secret: %w", err)
}
var s dbSecret
if err := json.Unmarshal([]byte(*out.SecretString), &s); err != nil {
return dbSecret{}, fmt.Errorf("parse secret: %w", err)
}
return s, nil
}
func main() {
ctx := context.Background()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
// Credenciais via Secrets Manager (IAM Role — sem access key!)
secret, err := loadDBSecret(ctx)
if err != nil {
slog.Error("falha ao carregar secret", "err", err)
os.Exit(1)
}
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=postgres sslmode=require",
os.Getenv("DB_HOST"), secret.Username, secret.Password,
)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
slog.Error("falha ao conectar no banco", "err", err)
os.Exit(1)
}
defer pool.Close()
// Cria a tabela se não existir
_, err = pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS tasks (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`)
if err != nil {
slog.Error("falha ao criar tabela", "err", err)
os.Exit(1)
}
mux := http.NewServeMux()
// GET /health — usado pelo ALB para saber se a instância está saudável
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
if err := pool.Ping(r.Context()); err != nil {
http.Error(w, "db unreachable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// POST /tasks — cria uma nova tarefa
mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Title == "" {
http.Error(w, "title required", http.StatusBadRequest)
return
}
var t Task
err := pool.QueryRow(r.Context(),
"INSERT INTO tasks(title) VALUES($1) RETURNING id, title, done, created_at",
body.Title,
).Scan(&t.ID, &t.Title, &t.Done, &t.CreatedAt)
if err != nil {
slog.Error("insert task", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(t)
})
// GET /tasks — lista todas as tarefas
mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
rows, err := pool.Query(r.Context(),
"SELECT id, title, done, created_at FROM tasks ORDER BY id DESC LIMIT 100",
)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
defer rows.Close()
tasks := []Task{}
for rows.Next() {
var t Task
if err := rows.Scan(&t.ID, &t.Title, &t.Done, &t.CreatedAt); err != nil {
continue
}
tasks = append(tasks, t)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
})
addr := ":8080"
slog.Info("servidor iniciado", "addr", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
slog.Error("servidor encerrado", "err", err)
}
}go.mod: github.com/aws/aws-sdk-go-v2/config, github.com/aws/aws-sdk-go-v2/service/secretsmanager e github.com/jackc/pgx/v5. Execute go mod tidy após criar o arquivo.Passo 7 — Compilar e subir o binário para o S3
Go compila para um binário estático — não precisa de runtime na EC2. A estratégia é compilar localmente (ou em CI), subir para S3, e a EC2 baixa na inicialização.
# Compila para Linux x86-64 (target da EC2)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o tasks-api .
# Sobe o binário para S3
aws s3 cp tasks-api s3://tasks-uploads-2026/releases/tasks-apiPasso 8 — Criar Launch Template
O User Data baixa o binário do S3 e sobe o serviço via systemd:
#!/bin/bash
# user-data.sh — roda na primeira inicialização da EC2
# Baixa o binário do S3 (a IAM Role autentica automaticamente)
aws s3 cp s3://tasks-uploads-2026/releases/tasks-api /opt/tasks-api
chmod +x /opt/tasks-api
# Pega o endpoint do RDS
DB_HOST=$(aws rds describe-db-instances \
--db-instance-identifier tasks-db \
--query 'DBInstances[0].Endpoint.Address' --output text)
# Cria unit do systemd para manter o processo vivo e reiniciar em falhas
cat > /etc/systemd/system/tasks-api.service <<EOF
[Unit]
Description=Tasks API
After=network.target
[Service]
ExecStart=/opt/tasks-api
Restart=always
RestartSec=5s
Environment=DB_HOST=${DB_HOST}
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now tasks-apiPasso 9 — Criar o Application Load Balancer
# Criar ALB nas subnets públicas (2 AZs diferentes)
aws elbv2 create-load-balancer \
--name tasks-alb \
--subnets <SUBNET_PUB_A> <SUBNET_PUB_B> \
--security-groups <SG_ALB_ID>
# Criar Target Group apontando para a porta 8080 da API Go
aws elbv2 create-target-group \
--name tasks-tg \
--protocol HTTP --port 8080 \
--vpc-id <VPC_ID> \
--health-check-path /health \
--target-type instance
# Criar listener (porta 80 -> Target Group)
aws elbv2 create-listener \
--load-balancer-arn <ALB_ARN> \
--protocol HTTP --port 80 \
--default-actions Type=forward,TargetGroupArn=<TG_ARN>Passo 10 — Criar Auto Scaling Group
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name tasks-asg \
--launch-template LaunchTemplateName=tasks-lt,Version=1 \
--min-size 2 --max-size 4 --desired-capacity 2 \
--target-group-arns <TG_ARN> \
--vpc-zone-identifier "<SUBNET_PRIV_A>,<SUBNET_PRIV_B>" \
--health-check-type ELB --health-check-grace-period 60
# Política de scaling: CPU > 70%
aws autoscaling put-scaling-policy \
--auto-scaling-group-name tasks-asg \
--policy-name cpu-target \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {"PredefinedMetricType":"ASGAverageCPUUtilization"},
"TargetValue": 70.0
}'health-check-grace-period é 60s aqui porque o binário Go sobe em menos de 1 segundo — bem mais rápido do que um processo Node.js com npm install.Passo 11 — Testar
# Pegar DNS do ALB
aws elbv2 describe-load-balancers --names tasks-alb \
--query 'LoadBalancers[0].DNSName' --output text
# Testar o health check
curl http://<ALB_DNS>/health
# {"status":"ok"}
# Criar uma tarefa
curl -X POST http://<ALB_DNS>/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Estudar AWS"}'
# {"id":1,"title":"Estudar AWS","done":false,"created_at":"..."}
# Listar tarefas
curl http://<ALB_DNS>/tasksPasso 12 — Configurar alarme no CloudWatch
# Alarme: 5xx do ALB > 10 em 5 minutos
aws cloudwatch put-metric-alarm \
--alarm-name api-5xx-high \
--metric-name HTTPCode_Target_5XX_Count \
--namespace AWS/ApplicationELB \
--statistic Sum --period 300 \
--threshold 10 --comparison-operator GreaterThanThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:sa-east-1:123:api-alertsO que você aprendeu nesse projeto
- Como criar uma rede segura (Security Groups encadeados)
- Como provisionar um banco gerenciado com Multi-AZ
- Como não colocar credenciais no código (IAM Role + Secrets Manager)
- Como compilar e distribuir um binário Go para EC2 via S3
- Como ter alta disponibilidade real (ALB + 2 instâncias em AZs diferentes)
- Como escalar automaticamente (ASG com target tracking)
- Como observar a aplicação (alarmes do CloudWatch)
Esses são os mesmos blocos usados por aplicações em produção da Netflix, Itaú, Nubank e Magalu. A vantagem do Go aqui é concreta: um binário de ~10MB, sem runtime, que sobe em milissegundos e não precisa de gerenciador de processos externo.