DEV Community

olcortesb for AWS Español

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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\"}'"]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}"
Enter fullscreen mode Exit fullscreen mode

Comparativa Js vs Go

Aspecto JavaScript Go
Cold Start ~200ms ~50ms
Memoria 128 MB mínimo 128 MB eficiente
Costo x86_64 estándar ARM64 (-50%) Aproximado
Tipado Dinámico Estático
Compilación Runtime Build 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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)