mimic 👓 (Api gateway + Lambda + Dynamo) en golang

Continuando con la serie de mimic, después de explorar la implementación básica en JavaScript y la relación con las distintas herramientas de IaC, vamos a probar esta implementación con Go y Terraform. En este artículo te muestro cómo crear una API completa de almacenamiento JSON usando Lambda en Go, API Gateway y DynamoDB.


¿Qué es mimic?

Como vimos en el artículo anterior, mimic es un stack serverless simple, que permite:

POST /mimic - Almacenar cualquier JSON y obtener un ID único
GET /mimic/{id} - Recuperar el JSON almacenado por su ID

Es como una base de datos en memoria que acepta cualquier estructura JSON, perfecta para testing, mocking de servicios y entornos efímeros.

Imagen: Diagrama de arquitectura mimic con Go


¿Por qué Go + Terraform?

En el artículo sobre Lambda en Go con Terraform, exploramos las ventajas del runtime provided.al2023. Para mimic, estas ventajas se multiplican:

  • Rendimiento superior: Go compila a binarios nativos, ideal para APIs de alta frecuencia

  • Costo optimizado: ARM64 (Graviton2) reduce costos hasta 50%

  • Escalabilidad: Cada operación (POST/GET) tiene su propia Lambda

  • Infraestructura como código: Terraform nos da control total y reproducibilidad


Estructura del proyecto

Aquí Link dejaré el repositorio con el código completo.

01_GST_mimic/
├── src/
│   ├── request/
│   │   ├── go.mod          # Módulo Go para POST
│   │   ├── main.go         # Handler POST
│   │   └── bootstrap       # Binario compilado
│   └── response/
│       ├── go.mod          # Módulo Go para GET
│       ├── main.go         # Handler GET
│       └── bootstrap       # Binario compilado
├── apigateway.tf           # API Gateway + API Key
├── dynamo.tf               # Tabla DynamoDB
├── lambdarequest.tf        # Lambda POST + IAM
├── lambdaresponse.tf       # Lambda GET
├── random.tf               # Sufijos aleatorios
├── variables.tf            # Variables configurables
├── outputs.tf              # Outputs del módulo
└── README.md               # Documentación

Implementación en Go

Lambda POST (Request)

La lambda de almacenamiento acepta cualquier JSON válido(Esto está así a propósito):

package main

import (
    "context"
    "encoding/json"
    "log"
    "os"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
    "github.com/google/uuid"
)

type MimicItem struct {
    ID   string                 `json:"id"`
    Body map[string]interface{} `json:"body"`
}

var dynamoClient *dynamodb.DynamoDB
var tableName string

func init() {
    sess := session.Must(session.NewSession())
    dynamoClient = dynamodb.New(sess)
    tableName = os.Getenv("MIMIC_TABLE")
    if tableName == "" {
        tableName = "mimic-table"
    }
}

func createBodyResponse(body map[string]interface{}) (string, error) {
    id := uuid.New().String()
    mimicItem := MimicItem{
        ID:   id,
        Body: body,
    }

    av, err := dynamodbattribute.MarshalMap(mimicItem)
    if err != nil {
        return "", err
    }

    _, err = dynamoClient.PutItem(&dynamodb.PutItemInput{
        TableName: aws.String(tableName),
        Item:      av,
    })

    return id, err
}

func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    log.Printf("Creating new mimic item")

    var body map[string]interface{}
    err := json.Unmarshal([]byte(request.Body), &body)
    if err != nil {
        log.Printf("Error parsing JSON: %v", err)
        return events.APIGatewayProxyResponse{
            StatusCode: 400,
            Body:       "Invalid JSON",
        }, nil
    }

    id, err := createBodyResponse(body)
    if err != nil {
        log.Printf("Error creating item: %v", err)
        return events.APIGatewayProxyResponse{
            StatusCode: 500,
            Body:       "Internal server error",
        }, nil
    }

    log.Printf("Created item with ID: %s", id)
    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body:       id,
    }, nil
}

func main() {
    lambda.Start(handler)
}

Clave: Usamos map[string]interface{} para aceptar cualquier estructura JSON sin validaciones específicas.


Lambda GET (Response)

La lambda de recuperación devuelve el JSON original:

// get.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

type MimicItem struct {
    ID   string                 `json:"id"`
    Body map[string]interface{} `json:"body"`
}

var dynamoClient *dynamodb.DynamoDB
var tableName string

func init() {
    sess := session.Must(session.NewSession())
    dynamoClient = dynamodb.New(sess)
    tableName = os.Getenv("MIMIC_TABLE")
    if tableName == "" {
        tableName = "mimic-table"
    }
}

func getBodyResponse(id string) (*MimicItem, error) {
    result, err := dynamoClient.GetItem(&dynamodb.GetItemInput{
        TableName: aws.String(tableName),
        Key: map[string]*dynamodb.AttributeValue{
            "id": {
                S: aws.String(id),
            },
        },
    })

    if err != nil {
        return nil, err
    }

    if result.Item == nil {
        return nil, fmt.Errorf("item not found")
    }

    var item MimicItem
    err = dynamodbattribute.UnmarshalMap(result.Item, &item)
    if err != nil {
        return nil, err
    }

    return &item, nil
}

func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    id, exists := request.PathParameters["id"]
    if !exists {
        return events.APIGatewayProxyResponse{
            StatusCode: 400,
            Body:       "Missing id parameter",
        }, nil
    }

    log.Printf("Getting mimic item with ID: %s", id)

    item, err := getBodyResponse(id)
    if err != nil {
        log.Printf("Error getting item: %v", err)
        return events.APIGatewayProxyResponse{
            StatusCode: 404,
            Body:       "Item not found",
        }, nil
    }

    responseBody, err := json.Marshal(item)
    if err != nil {
        log.Printf("Error marshaling response: %v", err)
        return events.APIGatewayProxyResponse{
            StatusCode: 500,
            Body:       "Internal server error",
        }, nil
    }

    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body:       string(responseBody),
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
    }, nil
}

func main() {
    lambda.Start(handler)
}

Infraestructura Terraform

DynamoDB en terraform.

resource "aws_dynamodb_table" "mimic_table" {
  name           = "${var.table_name}-${random_id.suffix.hex}"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "id"

  attribute {
    name = "id"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }

  tags = {
    Name = "MimicTable"
  }
}

API Gateway con API Key y Cuotas en terraform

# API Gateway con autenticación
resource "aws_api_gateway_rest_api" "mimic_api" {
  name        = "${var.api_name}-${random_id.suffix.hex}"
  description = "Mimic API for storing and retrieving JSON data"
}

# API Key para autenticación
resource "aws_api_gateway_api_key" "mimic_api_key" {
  name = "${var.api_name}-key-${random_id.suffix.hex}"
  description = "API Key for Mimic API"
}

# Usage Plan con cuotas mensuales
resource "aws_api_gateway_usage_plan" "mimic_usage_plan" {
  name = "${var.api_name}-usage-plan-${random_id.suffix.hex}"

  quota_settings {
    limit  = var.api_quota_limit  # 1000 requests/month
    period = "MONTH"
  }

  throttle_settings {
    rate_limit  = var.api_rate_limit   # 10 req/sec
    burst_limit = var.api_burst_limit  # 20 burst
  }
}

Compilación automática separada (No implementar external en PROD ⚠️)

Esto es para el despliegue desde el local, lo recomendable es hacer el build en los pipelines y no usar external.

# Build REQUEST Lambda
data "external" "build_create_lambda" {
  program = ["bash", "-c", "cd src/request && go mod tidy && env GOOS=linux GOARCH=arm64 go build -o bootstrap main.go && echo '{\"filename\":\"bootstrap\"}'"]
}

# Build RESPONSE Lambda
data "external" "build_get_lambda" {
  program = ["bash", "-c", "cd src/response && go mod tidy && env GOOS=linux GOARCH=arm64 go build -o bootstrap main.go && echo '{\"filename\":\"bootstrap\"}'"]
}

Importante: Cada lambda tiene su propio módulo Go y se compila independientemente.


Despliegue

# Clonar e inicializar
git clone https://github.com/tu-usuario/golang-serverless-terraform.git
cd golang-serverless-terraform/01_GST_mimic

# Configurar credenciales AWS 
# Ref: https://gist.github.com/olcortesb/a471797eb1d45c54ad51d920b78aa664

# Desplegar
terraform init
terraform plan
terraform apply

Probando la API

# Obtener valores de salida
API_URL=$(terraform output -raw api_gateway_url)
API_KEY=$(terraform output -raw api_key_value)

# Almacenar JSON de usuario
curl -X POST "${API_URL}/mimic" \
  -H "x-api-key: ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice",
    "email": "alice@example.com",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  }'

# Respuesta: "550e8400-e29b-41d4-a716-446655440000"

# Recuperar JSON
curl -X GET "${API_URL}/mimic/550e8400-e29b-41d4-a716-446655440000" \
  -H "x-api-key: ${API_KEY}"

Comparativa Js vs Go

AspectoJavaScriptGo
Cold Start~200ms~50ms
Memoria128 MB mínimo128 MB eficiente
Costox86_64 estándarARM64 (-50%) Aproximado
TipadoDinámicoEstático
CompilaciónRuntimeBuild time

Monitoreo y observabilidad

Terraform automáticamente configura:

  • CloudWatch Logs: Para debugging de las lambdas

  • API Gateway Metrics: Latencia, errores, throttling

  • DynamoDB Metrics: Read/Write capacity, throttling

  • Usage Plan Monitoring: Cuotas y límites de rate


Limpieza

terraform destroy

Conclusiones

La implementación de mimic en Go + Terraform nos ofrece:

  • Rendimiento superior: Cold starts más rápidos y mejor throughput

  • Costo optimizado: ARM64 reduce significativamente los costos

  • Infraestructura reproducible: Terraform garantiza consistencia

  • POC: Utilizaré este código como una POC para ir mejorando, actualizando y realizando pruebas sobre Golang + AWS Lambda

Este stack es perfecto para:

  • Entornos de desarrollo y testing

  • Mocking de servicios externos

  • Prototipado rápido de APIs

  • Cache temporal de datos

  • 🙋 Laboratorios de infraestructura (Fundamentalmente para lo que lo uso …)

En el próximo artículo exploraremos cómo integrar mimic con otros servicios AWS como S3 Events y SQS para crear arquitecturas event-driven más complejas.

¡Gracias por leer, saludos!


Referencias

0
Subscribe to my newsletter

Read articles from Oscar Cortes Bracho directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Oscar Cortes Bracho
Oscar Cortes Bracho

Cloud Software Engineer