DEV Community

Edgar (Homz) Macias
Edgar (Homz) Macias

Posted on

AWS Modulo 3: Lambda con Go

Compilé para Linux sin Salir de mi Mac (y Costó $0.06)

📚 Serie: AWS Zero to Architect - Módulo 3

⏱️ Tiempo de lectura: 20 minutos

💻 Tiempo de implementación: 120 minutos

En los módulos anteriores configuramos AWS, Terraform e IAM. Ahora viene lo divertido: crear tu primera función Lambda en Go que cuesta centavos y es 3x más rápida que Python.


🤔 El Problema que Todos Tenemos

Escenario común:

"Tengo una API simple. ¿Monto un servidor 24/7 que me cuesta $50/mes aunque solo reciba 100 requests/día?"

Respuesta: NO. Usa Lambda.


⚡ Lambda Explicada: Food Truck vs Restaurant

Imagina que tienes un negocio de comida:

🏢 Restaurant (EC2 - Servidor Tradicional)

- Rentas local completo: $500/mes
- Pagas luz/agua/gas 24/7
- Staff de tiempo completo
- Si hay 3 clientes o 300, pagas lo mismo
- Si hay demanda, el local se satura

Costo fijo: $500/mes
Enter fullscreen mode Exit fullscreen mode

🚚 Food Truck (Lambda - Serverless)

- Solo pagas cuando sirves un plato
- $0.20 por cada 1 millón de platos
- Sin staff fijo (AWS lo maneja)
- Escala automáticamente
- 3 clientes = $0.0006
- 300 clientes = $0.06

Costo variable: $0-$100/mes
Enter fullscreen mode Exit fullscreen mode

¿Cuál tiene más sentido para una API que recibe tráfico esporádico?


💰 Pricing Real (Sin Marketing BS)

Mi Lambda Actual

100,000 requests/mes
128MB RAM
200ms promedio de duración

Cálculo:
Requests: 100,000 ÷ 1,000,000 × $0.20 = $0.02
Compute:  0.128GB × 0.2s × 100,000 × $0.0000166667 = $0.04

Total: $0.06/mes
Enter fullscreen mode Exit fullscreen mode

Free Tier:

  • 1 millón de requests/mes
  • 400,000 GB-segundos

Traducción: Gratis para desarrollo y apps pequeñas.


🐹 ¿Por Qué Go y No Python/Node.js?

Benchmarks Honestos (Mis Propias Mediciones)

Lenguaje Cold Start Memory Used Warm Invoke
Go 156ms 48MB 45ms
Node.js 342ms 89MB 89ms
Python 478ms 127MB 134ms
Java 1,240ms 186MB 203ms

Resultado: Go es 3x más rápido en cold start.

¿Qué es un Cold Start?

Primera invocación del día:
1. AWS crea un container → 100ms
2. Carga tu runtime (Node.js/Python) → 200ms
3. Carga tu código → 100ms
Total: ~400ms

Con Go:
1. AWS crea container → 100ms
2. Ejecuta binario → 50ms
Total: ~150ms
Enter fullscreen mode Exit fullscreen mode

Para el usuario final: Go responde en 150ms, Python en 400ms.

Ventaja de Costos

Python (512MB):
$0.0000083 por 100ms

Go (128MB):
$0.0000021 por 100ms

Ahorro: 75%
Enter fullscreen mode Exit fullscreen mode

Con 1M requests/mes:

  • Python: $83/mes
  • Go: $21/mes

Diferencia: $62/mes × 12 meses = $744/año


🏗️ Arquitectura: Hexagonal en Serverless

¿Qué es Arquitectura Hexagonal?

Idea simple: La lógica de negocio NO debe depender de AWS.

❌ MAL (Acoplado a AWS):
func CreateSession(userID string) {
    dynamodb.PutItem(...)  // Directo a AWS
}

✅ BIEN (Desacoplado):
// Domain (puro Go, sin AWS)
type Session struct {
    ID     string
    UserID string
}

func NewSession(userID string) *Session { ... }

// Adapter (traduce a AWS)
type DynamoDBRepo struct { ... }
func (r *DynamoDBRepo) Save(session *Session) { ... }
Enter fullscreen mode Exit fullscreen mode

Ventajas:

  • ✅ Puedes cambiar DynamoDB por Postgres sin tocar el dominio
  • ✅ Testeable sin AWS
  • ✅ Lógica de negocio clara

Mi Estructura

go-hexagonal-auth/
├── cmd/lambda/main.go           # Lambda handler
├── internal/
│   ├── core/domain/
│   │   └── session.go           # Lógica de negocio
│   └── adapters/repository/
│       └── dynamodb_session.go  # AWS adapter
└── terraform/
    └── lambda.tf                # Infrastructure
Enter fullscreen mode Exit fullscreen mode

💻 El Código que Importa

Domain Model (Sin AWS)

package domain

import (
    "time"
    "github.com/google/uuid"
)

type Session struct {
    SessionID string
    UserID    string
    ExpiresAt time.Time
    CreatedAt time.Time
}

func NewSession(userID string, ttl time.Duration) *Session {
    now := time.Now()
    return &Session{
        SessionID: uuid.New().String(),
        UserID:    userID,
        CreatedAt: now,
        ExpiresAt: now.Add(ttl),
    }
}

func (s *Session) IsValid() bool {
    return s.SessionID != "" && s.UserID != ""
}
Enter fullscreen mode Exit fullscreen mode

Nota: CERO imports de AWS. Lógica pura.

DynamoDB Adapter

package repository

import (
    "context"
    "github.com/aws/aws-sdk-go-v2/service/dynamodb"
    "go-hexagonal-auth/internal/core/domain"
)

type DynamoDBRepo struct {
    client    *dynamodb.Client
    tableName string
}

func (r *DynamoDBRepo) Save(ctx context.Context, session *domain.Session) error {
    // Conversión domain → DynamoDB
    item := map[string]interface{}{
        "session_id": session.SessionID,
        "user_id":    session.UserID,
        "expires_at": session.ExpiresAt.Unix(),
    }

    // PutItem en DynamoDB
    _, err := r.client.PutItem(ctx, &dynamodb.PutItemInput{
        TableName: aws.String(r.tableName),
        Item:      marshalMap(item),
    })
    return err
}
Enter fullscreen mode Exit fullscreen mode

Lambda Handler

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
    "go-hexagonal-auth/internal/core/domain"
    "go-hexagonal-auth/internal/adapters/repository"
)

func handler(ctx context.Context, request APIGatewayRequest) (Response, error) {
    // 1. Parse input
    var req CreateSessionRequest
    json.Unmarshal([]byte(request.Body), &req)

    // 2. Create session (domain logic)
    session := domain.NewSession(req.UserID, 24*time.Hour)

    // 3. Save (adapter)
    repo := repository.NewDynamoDBRepo(ctx)
    repo.Save(ctx, session)

    // 4. Response
    return Response{
        StatusCode: 201,
        Body: json.Marshal(session),
    }, nil
}

func main() {
    lambda.Start(handler)
}
Enter fullscreen mode Exit fullscreen mode

Flujo:

  1. Parse JSON
  2. Lógica de negocio (domain)
  3. Persistencia (adapter)
  4. Respuesta

🔨 Compilación Cross-Platform (La Magia de Go)

El Problema

Desarrollo en: macOS ARM64 (M1/M2/M3)
Lambda ejecuta: Linux ARM64

¿Cómo compilo para Linux sin salir de macOS?
Enter fullscreen mode Exit fullscreen mode

La Solución de Go

# Un solo comando
GOOS=linux GOARCH=arm64 go build -o bootstrap ./cmd/lambda
Enter fullscreen mode Exit fullscreen mode

Eso es todo.

No necesitas:

  • ❌ Docker
  • ❌ Máquina virtual Linux
  • ❌ Compilar en CI/CD

Go compila nativamente para otras plataformas.

Makefile (Automatización)

build:
    GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
        go build -ldflags="-s -w" \
        -o build/bootstrap ./cmd/lambda

zip: build
    cd build && zip lambda.zip bootstrap
Enter fullscreen mode Exit fullscreen mode

Comandos:

make build   # Compila para Lambda
make zip     # Crea lambda.zip
Enter fullscreen mode Exit fullscreen mode

Tamaños:

build/bootstrap: 7.2 MB (sin comprimir)
build/lambda.zip: 2.8 MB (comprimido)
Enter fullscreen mode Exit fullscreen mode

Flags Importantes

-ldflags="-s -w"  # Reduce tamaño 30-40%
CGO_ENABLED=0     # Binario estático (sin deps C)
Enter fullscreen mode Exit fullscreen mode

🚀 Deploy con Terraform

Lambda Configuration

resource "aws_lambda_function" "create_session" {
  filename      = "../build/lambda.zip"
  function_name = "go-hexagonal-auth-dev-create-session"
  role          = aws_iam_role.lambda_execution.arn
  handler       = "bootstrap"
  runtime       = "provided.al2023"

  # ARM64 (Graviton2 - 20% más barato)
  architectures = ["arm64"]

  memory_size = 128
  timeout     = 10

  environment {
    variables = {
      DYNAMODB_TABLE_NAME = aws_dynamodb_table.sessions.name
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Características:

  • runtime = "provided.al2023": Custom runtime para Go
  • architectures = ["arm64"]: Graviton2 (procesadores de AWS)
  • memory_size = 128: Suficiente para Go

¿Por qué ARM64?

Benchmarks:

Arquitectura Precio/GB-seg Performance Relación
x86_64 $0.0000166667 Baseline 1.0x
ARM64 $0.0000133334 +10-15% 1.15x

Resultado: ARM64 es 20% más barato Y 10% más rápido.

Deploy

# 1. Compilar
make build zip

# 2. Deploy
cd terraform
terraform apply
Enter fullscreen mode Exit fullscreen mode

Tiempo de deploy: ~15 segundos


🧪 Testing (La Parte Satisfactoria)

Test Básico

# Invocar Lambda
aws lambda invoke \
  --function-name go-hexagonal-auth-dev-create-session \
  --payload '{"body": "{\"user_id\": \"test-123\"}"}' \
  response.json

# Ver resultado
cat response.json | jq .
Enter fullscreen mode Exit fullscreen mode

Respuesta:

{
  "statusCode": 201,
  "body": {
    "session_id": "f7a3b2c1-4d5e-6789-abcd-ef0123456789",
    "user_id": "test-123",
    "expires_at": 1734393600,
    "message": "Session created successfully"
  }
}
Enter fullscreen mode Exit fullscreen mode

Primera invocación: 156ms (cold start)

Segunda invocación: 45ms (warm)

Verificar en DynamoDB

SESSION_ID=$(cat response.json | jq -r '.body.session_id')

aws dynamodb get-item \
  --table-name sessions \
  --key "{\"session_id\": {\"S\": \"$SESSION_ID\"}}"
Enter fullscreen mode Exit fullscreen mode

¡Ahí está la sesión! 🎉

CloudWatch Logs

aws logs tail /aws/lambda/go-hexagonal-auth-dev-create-session --follow
Enter fullscreen mode Exit fullscreen mode

Output:

START RequestId: abc-123
Received request: {"body": "{\"user_id\":\"test-123\"}"}
Session created: f7a3b2c1... for user: test-123
END RequestId: abc-123
REPORT Duration: 156ms Memory: 48MB
Enter fullscreen mode Exit fullscreen mode

Métricas:

  • Duration: 156ms
  • Memory Used: 48MB (de 128MB)
  • Billed Duration: 200ms

🎯 Resultados Reales

Performance

Cold Start (primera invocación del día):

  • Mi Lambda Go: 156ms
  • Lambda Node.js equivalente: 342ms
  • Mejora: 2.2x más rápido

Warm Invocations:

  • Go: 45ms
  • Node.js: 89ms

Costos (100k requests/mes)

Go Lambda:
128MB × 200ms × 100,000 requests
= $0.06/mes

Node.js Lambda (mismo workload):
256MB × 200ms × 100,000 requests
= $0.12/mes

Ahorro: $0.06/mes × 12 = $0.72/año
Enter fullscreen mode Exit fullscreen mode

Multiplicado por 10 Lambdas: $7.20/año de ahorro.


🆘 Problemas que Tuve (Y Cómo los Resolví)

Error: "Runtime.ExitError exit status 2"

Causa: Faltaba import de net/http en main.go

Fix:

import (
    "net/http"  // ← Agregar esto
    // ... otros imports
)
Enter fullscreen mode Exit fullscreen mode

Error: "DYNAMODB_TABLE_NAME not set"

Causa: Variable de entorno mal configurada en Terraform

Fix:

environment {
  variables = {
    DYNAMODB_TABLE_NAME = aws_dynamodb_table.sessions.name
    # NO: DYNAMODB_TABLE_SESSIONS
  }
}
Enter fullscreen mode Exit fullscreen mode

Error: "exec format error"

Causa: Compilé para x86 en lugar de ARM64

Fix:

# Verificar arquitectura
file build/bootstrap
# Debe decir: ARM aarch64

# Recompilar
GOARCH=arm64 make build
Enter fullscreen mode Exit fullscreen mode

🔐 Seguridad: ¿Es Pública Mi Lambda?

Respuesta Corta: NO

Situación actual (Módulo 3):

Internet → ❌ NO PUEDE ACCEDER
AWS CLI con credenciales → ✅ PUEDE INVOCAR
Enter fullscreen mode Exit fullscreen mode

Solo es invocable con:

  • AWS CLI + credenciales
  • IAM permissions

Nadie en internet puede ejecutarla.

Módulo 4: API Gateway (Próximo)

Ahí sí la haremos pública con:

  • ✅ HTTPS endpoint
  • ✅ Throttling (límite de requests)
  • ✅ API Keys (opcional)

💡 Lo Que Aprendí

  1. Go es RÁPIDO para serverless

    • Cold starts 3x más rápidos
    • 75% más barato en memoria
  2. Compilación cross-platform es magia

    • Un comando, no Docker
  3. Arquitectura Hexagonal vale la pena

    • Testeable sin AWS
    • Fácil cambiar DynamoDB
  4. ARM64 > x86_64

    • 20% más barato
    • 10% más rápido
  5. Terraform > ClickOps

    • Reproducible
    • Versionado

📊 Comparación Final

Característica EC2 t3.micro Lambda Go
Costo base $7.50/mes $0.00
Escalado Manual Automático
Cold start N/A 156ms
Warm invoke N/A 45ms
Mantenimiento AWS
100k req/mes $7.50 $0.06

Ganador: Lambda (para tráfico esporádico)


🎓 Lo Que Lograste

Si llegaste hasta aquí e implementaste todo:

  • ✅ Primera Lambda function en Go
  • ✅ Compilación cross-platform
  • ✅ Arquitectura Hexagonal
  • ✅ Integración con DynamoDB
  • ✅ Deploy con Terraform
  • ✅ Testing funcional

Y todo por < $0.10/mes. 🎉


🚀 Próximo Paso: API Gateway

En el Módulo 4 vamos a:

  • Crear endpoint público HTTPS
  • Configurar CORS
  • Agregar throttling
  • Testear con Postman

La Lambda será accesible desde cualquier lugar (con controles de seguridad).


📦 Código Completo

Todo el código está en GitHub:

GitHub logo edgar-macias-se / go-hexagonal-auth

Production-ready Authentication Microservice in Go. Implements Hexagonal Architecture, JWT, Redis Blacklisting, and Rate Limiting.

🛡️ Go Secure Authentication Service

Un microservicio de autenticación robusto, escalable y listo para producción, escrito en Go siguiendo principios de Arquitectura Hexagonal.

Diseñado con la seguridad como prioridad, implementando las mejores prácticas de OWASP para la gestión de identidad, sesiones y protección contra ataques.


🚀 Key Security Highlights

Este no es solo un login básico. Este proyecto implementa capas de defensa en profundidad:

  • 🔒 Arquitectura Hexagonal (Ports & Adapters): Desacoplamiento total entre la lógica de negocio, la base de datos y la API HTTP. Código testable y mantenible.
  • 🔑 Estrategia de Tokens Duales (JWT):
    • Access Token (15 min): JWT firmado (HS256) de vida corta para minimizar riesgos en caso de robo.
    • Refresh Token (7 días): Token opaco rotativo almacenado en BD para renovar sesiones sin exponer credenciales.
  • 🛡️ Protección contra Fuerza Bruta (Rate Limiting): Middleware distribuido usando Redis. Bloquea IPs/Usuarios tras 5 intentos fallidos por 15 minutos.




Carpetas:

  • terraform/ - Lambda config
  • cmd/lambda/ - Handler
  • internal/ - Domain y adapters

💬 Tu Turno

¿Has usado Lambda con otros lenguajes? ¿Cuál ha sido tu experiencia con cold starts?

¿Prefieres serverless o servidores tradicionales? Cuenta tu caso de uso en los comentarios 👇


🔗 Conecta


Serie: AWS Zero to Architect

Anterior: Módulo 2 - IAM Roles & DynamoDB

Siguiente: Módulo 4 - API Gateway (próximamente)


💡 Tip: Si este tutorial te ahorró horas de debugging, compártelo con alguien que esté empezando con serverless.

Top comments (0)