Skip to content
English
On this page

Ejercicio: API en GoLang con DocumentDB Multi-región

Parte 1: Estructura y DocumentDB

Escenario

Implementaremos una API en GoLang con:

  • DocumentDB en múltiples regiones
  • Alta disponibilidad con Route 53
  • Variables en Parameter Store
  • Monitoreo cross-region
  • Entornos DEV, STG y PROD

Estructura del Proyecto

go-documentdb-ha/
├── cmd/
│   └── api/
│       └── main.go

├── internal/
│   ├── config/
│   │   ├── config.go
│   │   └── environments.go
│   │
│   ├── database/
│   │   ├── documentdb.go
│   │   └── models/
│   │       └── user.go
│   │
│   ├── handlers/
│   │   ├── health.go
│   │   └── users.go
│   │
│   └── middleware/
│       ├── logging.go
│       └── metrics.go

├── infrastructure/
│   ├── cloudformation/
│   │   ├── documentdb/
│   │   │   ├── primary.yaml
│   │   │   └── secondary.yaml
│   │   │
│   │   └── route53/
│   │       └── failover.yaml
│   │
│   ├── scripts/
│   │   ├── deploy.sh
│   │   └── setup-replication.sh
│   │
│   └── terraform/
│       ├── modules/
│       │   ├── documentdb/
│       │   └── route53/
│       └── environments/
│           ├── dev/
│           ├── stg/
│           └── prod/

├── pkg/
│   └── utils/
│       ├── aws/
│       │   ├── ssm.go
│       │   └── route53.go
│       └── mongodb/
│           └── connection.go

├── scripts/
│   ├── backup/
│   │   └── backup.sh
│   └── monitoring/
│       └── health-check.sh

├── go.mod
└── go.sum

1. Configuración de DocumentDB

1.1 CloudFormation para DocumentDB Primario

yaml
# infrastructure/cloudformation/documentdb/primary.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'DocumentDB Cluster - Primary Region'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]
    Description: Environment name
    
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: VPC ID
    
  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: List of subnet IDs
    
  InstanceClass:
    Type: String
    Default: db.r5.large
    AllowedValues: 
      - db.r5.large
      - db.r5.xlarge
      - db.r5.2xlarge

Resources:
  DocumentDBCluster:
    Type: AWS::DocDB::DBCluster
    Properties:
      Engine: docdb
      MasterUsername: !Sub '{{resolve:ssm:/${Environment}/docdb/master_username:1}}'
      MasterUserPassword: !Sub '{{resolve:ssm:/${Environment}/docdb/master_password:1}}'
      VpcSecurityGroupIds: 
        - !Ref DocumentDBSecurityGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      StorageEncrypted: true
      BackupRetentionPeriod: !If [IsProd, 30, 7]
      PreferredBackupWindow: "02:00-04:00"
      PreferredMaintenanceWindow: "sun:04:00-sun:06:00"
      EnableCloudwatchLogsExports:
        - audit
        - profiler
      Tags:
        - Key: Environment
          Value: !Ref Environment

  DocumentDBInstance1:
    Type: AWS::DocDB::DBInstance
    Properties:
      DBClusterIdentifier: !Ref DocumentDBCluster
      DBInstanceClass: !Ref InstanceClass
      Tags:
        - Key: Environment
          Value: !Ref Environment

  DocumentDBInstance2:
    Type: AWS::DocDB::DBInstance
    Condition: IsProd
    Properties:
      DBClusterIdentifier: !Ref DocumentDBCluster
      DBInstanceClass: !Ref InstanceClass
      Tags:
        - Key: Environment
          Value: !Ref Environment

  DBSubnetGroup:
    Type: AWS::DocDB::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Subnet group for DocumentDB
      SubnetIds: !Ref SubnetIds
      Tags:
        - Key: Environment
          Value: !Ref Environment

  DocumentDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for DocumentDB
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 27017
          ToPort: 27017
          SourceSecurityGroupId: !Ref ApplicationSecurityGroup
      Tags:
        - Key: Environment
          Value: !Ref Environment

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Application
      VpcId: !Ref VpcId
      Tags:
        - Key: Environment
          Value: !Ref Environment

Conditions:
  IsProd: !Equals [!Ref Environment, prod]

Outputs:
  ClusterEndpoint:
    Description: The cluster endpoint
    Value: !GetAtt DocumentDBCluster.Endpoint
    Export:
      Name: !Sub '${AWS::StackName}-ClusterEndpoint'

  ClusterArn:
    Description: The cluster ARN
    Value: !GetAtt DocumentDBCluster.ClusterResourceId
    Export:
      Name: !Sub '${AWS::StackName}-ClusterArn'

1.2 Modelo de Base de Datos

go
// internal/database/models/user.go
package models

import (
    "time"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

type User struct {
    ID        primitive.ObjectID `bson:"_id,omitempty" json:"id"`
    Name      string            `bson:"name" json:"name"`
    Email     string            `bson:"email" json:"email"`
    CreatedAt time.Time         `bson:"created_at" json:"created_at"`
    UpdatedAt time.Time         `bson:"updated_at" json:"updated_at"`
    Region    string            `bson:"region" json:"region"`
    Version   int              `bson:"version" json:"version"`
}

type UserRepository interface {
    Create(*User) error
    Update(*User) error
    Delete(primitive.ObjectID) error
    FindByID(primitive.ObjectID) (*User, error)
    FindAll() ([]*User, error)
}

1.3 Conexión a DocumentDB

go
// pkg/utils/mongodb/connection.go
package mongodb

import (
    "context"
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"
    "time"

    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type Config struct {
    Username   string
    Password   string
    Host       string
    Port       string
    Database   string
    CAFile     string
    ReplicaSet string
}

func NewConnection(cfg *Config) (*mongo.Client, error) {
    // Leer certificado CA
    ca, err := ioutil.ReadFile(cfg.CAFile)
    if err != nil {
        return nil, fmt.Errorf("error reading CA file: %v", err)
    }

    // Configurar TLS
    tlsConfig := &tls.Config{
        RootCAs: x509.NewCertPool(),
    }
    if !tlsConfig.RootCAs.AppendCertsFromPEM(ca) {
        return nil, fmt.Errorf("failed to append CA certificates")
    }

    // Construir URI
    uri := fmt.Sprintf(
        "mongodb://%s:%s@%s:%s/%s?tls=true&replicaSet=%s&retryWrites=false",
        cfg.Username,
        cfg.Password,
        cfg.Host,
        cfg.Port,
        cfg.Database,
        cfg.ReplicaSet,
    )

    // Opciones de cliente
    clientOptions := options.Client().
        ApplyURI(uri).
        SetTLSConfig(tlsConfig).
        SetConnectTimeout(10 * time.Second).
        SetServerSelectionTimeout(5 * time.Second).
        SetMaxPoolSize(100)

    // Conectar
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    client, err := mongo.Connect(ctx, clientOptions)
    if err != nil {
        return nil, fmt.Errorf("error connecting to database: %v", err)
    }

    // Verificar conexión
    err = client.Ping(ctx, nil)
    if err != nil {
        return nil, fmt.Errorf("error pinging database: %v", err)
    }

    return client, nil
}

2. Scripts de Configuración

2.1 Script de Despliegue

bash
#!/bin/bash
# infrastructure/scripts/deploy.sh

ENVIRONMENT=$1
REGION=$2
STACK_NAME="documentdb-${ENVIRONMENT}"

# Validar parámetros
if [[ ! "$ENVIRONMENT" =~ ^(dev|stg|prod)$ ]]; then
    echo "Environment must be dev, stg, or prod"
    exit 1
fi

# Obtener parámetros de SSM
USERNAME=$(aws ssm get-parameter --name "/${ENVIRONMENT}/docdb/master_username" --with-decryption --query 'Parameter.Value' --output text)
PASSWORD=$(aws ssm get-parameter --name "/${ENVIRONMENT}/docdb/master_password" --with-decryption --query 'Parameter.Value' --output text)

# Desplegar stack
aws cloudformation deploy \
    --template-file infrastructure/cloudformation/documentdb/primary.yaml \
    --stack-name $STACK_NAME \
    --parameter-overrides \
        Environment=$ENVIRONMENT \
        Username=$USERNAME \
        Password=$PASSWORD \
    --capabilities CAPABILITY_IAM \
    --region $REGION

2.2 Script de Replicación

bash
#!/bin/bash
# infrastructure/scripts/setup-replication.sh

SOURCE_REGION=$1
DEST_REGION=$2
ENVIRONMENT=$3
CLUSTER_ID=$4

# Configurar replicación global
aws docdb create-global-cluster \
    --global-cluster-identifier "global-${ENVIRONMENT}" \
    --source-db-cluster-identifier $CLUSTER_ID \
    --region $SOURCE_REGION

# Crear cluster secundario
aws docdb create-db-cluster \
    --db-cluster-identifier "docdb-${ENVIRONMENT}-secondary" \
    --global-cluster-identifier "global-${ENVIRONMENT}" \
    --engine docdb \
    --region $DEST_REGION

Verificación Parte 1

1. Verificar DocumentDB

  • [ ] Cluster creado
  • [ ] Instancias corriendo
  • [ ] Seguridad configurada
  • [ ] Monitoreo activo

2. Verificar Conexión

  • [ ] SSL configurado
  • [ ] Credenciales correctas
  • [ ] Timeout apropiado
  • [ ] Pool size configurado

3. Verificar Scripts

  • [ ] Despliegue funcional
  • [ ] Replicación configurada
  • [ ] Parámetros seguros
  • [ ] Logs disponibles

Troubleshooting Común

Errores de DocumentDB

  1. Verificar VPC/Subnet
  2. Revisar seguridad
  3. Verificar certificados

Errores de Conexión

  1. Verificar credenciales
  2. Revisar TLS
  3. Verificar timeouts

Errores de Despliegue

  1. Verificar IAM
  2. Revisar parámetros
  3. Verificar regiones

Parte 2: Alta Disponibilidad y Configuración

1. Configuración de Route 53

1.1 CloudFormation para Route 53

yaml
# infrastructure/cloudformation/route53/failover.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Route 53 Failover Configuration'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]
    
  DomainName:
    Type: String
    Description: Domain name for the application
    
  PrimaryEndpoint:
    Type: String
    Description: Primary region endpoint
    
  SecondaryEndpoint:
    Type: String
    Description: Secondary region endpoint

Resources:
  HealthCheck:
    Type: AWS::Route53::HealthCheck
    Properties:
      HealthCheckConfig:
        Port: 443
        Type: HTTPS
        ResourcePath: /health
        FullyQualifiedDomainName: !Ref PrimaryEndpoint
        RequestInterval: 30
        FailureThreshold: 3
      HealthCheckTags:
        - Key: Environment
          Value: !Ref Environment

  DNSRecordPrimary:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub '${DomainName}.'
      Name: !Sub 'api.${DomainName}'
      Type: A
      SetIdentifier: Primary
      Failover: PRIMARY
      HealthCheckId: !Ref HealthCheck
      AliasTarget:
        DNSName: !Ref PrimaryEndpoint
        HostedZoneId: !Ref PrimaryHostedZoneId
        EvaluateTargetHealth: true

  DNSRecordSecondary:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub '${DomainName}.'
      Name: !Sub 'api.${DomainName}'
      Type: A
      SetIdentifier: Secondary
      Failover: SECONDARY
      AliasTarget:
        DNSName: !Ref SecondaryEndpoint
        HostedZoneId: !Ref SecondaryHostedZoneId
        EvaluateTargetHealth: true

Outputs:
  HealthCheckId:
    Description: ID of the created health check
    Value: !Ref HealthCheck
    Export:
      Name: !Sub '${AWS::StackName}-HealthCheckId'

1.2 Configuración de Health Check

go
// internal/handlers/health.go
package handlers

import (
    "encoding/json"
    "net/http"
    "time"
    
    "github.com/your-org/your-app/internal/database"
)

type HealthStatus struct {
    Status      string            `json:"status"`
    Database    DatabaseStatus    `json:"database"`
    LastChecked time.Time        `json:"last_checked"`
    Region      string           `json:"region"`
}

type DatabaseStatus struct {
    Connected bool    `json:"connected"`
    Latency   int64  `json:"latency_ms"`
}

func HealthCheckHandler(db *database.DocumentDB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        status := HealthStatus{
            Status:      "healthy",
            LastChecked: time.Now(),
            Region:     os.Getenv("AWS_REGION"),
        }

        // Verificar conexión a DB
        start := time.Now()
        err := db.Ping()
        latency := time.Since(start).Milliseconds()

        status.Database = DatabaseStatus{
            Connected: err == nil,
            Latency:   latency,
        }

        if err != nil {
            status.Status = "unhealthy"
            w.WriteHeader(http.StatusServiceUnavailable)
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(status)
    }
}

2. Parameter Store Configuration

2.1 Gestión de Variables de Entorno

go
// internal/config/config.go
package config

import (
    "context"
    "fmt"
    "github.com/aws/aws-sdk-go-v2/service/ssm"
)

type Config struct {
    Environment    string
    DocDB         DocDBConfig
    App           AppConfig
    AWS           AWSConfig
}

type DocDBConfig struct {
    Username    string
    Password    string
    Host        string
    Port        string
    Database    string
    ReplicaSet  string
}

type AppConfig struct {
    Port        string
    LogLevel    string
    Region      string
}

type AWSConfig struct {
    Region      string
    Profile     string
}

func LoadConfig(ctx context.Context, environment string) (*Config, error) {
    ssmClient := ssm.New(ssm.Options{
        Region: os.Getenv("AWS_REGION"),
    })

    // Cargar parámetros por path
    params, err := getParametersByPath(ctx, ssmClient, fmt.Sprintf("/%s/", environment))
    if err != nil {
        return nil, err
    }

    config := &Config{
        Environment: environment,
        DocDB: DocDBConfig{
            Username:    params["/docdb/username"],
            Password:    params["/docdb/password"],
            Host:        params["/docdb/host"],
            Port:        params["/docdb/port"],
            Database:    params["/docdb/database"],
            ReplicaSet:  params["/docdb/replica_set"],
        },
        App: AppConfig{
            Port:     params["/app/port"],
            LogLevel: params["/app/log_level"],
            Region:   params["/app/region"],
        },
        AWS: AWSConfig{
            Region:  params["/aws/region"],
            Profile: params["/aws/profile"],
        },
    }

    return config, nil
}

func getParametersByPath(ctx context.Context, client *ssm.Client, path string) (map[string]string, error) {
    params := make(map[string]string)
    
    input := &ssm.GetParametersByPathInput{
        Path:           &path,
        Recursive:      aws.Bool(true),
        WithDecryption: aws.Bool(true),
    }

    paginator := ssm.NewGetParametersByPathPaginator(client, input)
    for paginator.HasMorePages() {
        output, err := paginator.NextPage(ctx)
        if err != nil {
            return nil, fmt.Errorf("error getting parameters: %v", err)
        }

        for _, param := range output.Parameters {
            // Remover el prefijo del path para tener nombres más limpios
            name := strings.TrimPrefix(*param.Name, path)
            params[name] = *param.Value
        }
    }

    return params, nil
}

2.2 Script de Configuración de Parámetros

python
# scripts/setup_parameters.py
import boto3
import json
import os

def setup_parameters(environment):
    ssm = boto3.client('ssm')
    
    # Cargar configuración base
    with open(f'config/{environment}.json') as f:
        config = json.load(f)
        
    # Establecer parámetros por entorno
    parameters = {
        # DocDB
        f'/{environment}/docdb/username': {'value': config['docdb']['username'], 'type': 'SecureString'},
        f'/{environment}/docdb/password': {'value': config['docdb']['password'], 'type': 'SecureString'},
        f'/{environment}/docdb/host': {'value': config['docdb']['host'], 'type': 'String'},
        f'/{environment}/docdb/port': {'value': config['docdb']['port'], 'type': 'String'},
        f'/{environment}/docdb/database': {'value': config['docdb']['database'], 'type': 'String'},
        f'/{environment}/docdb/replica_set': {'value': config['docdb']['replica_set'], 'type': 'String'},
        
        # App
        f'/{environment}/app/port': {'value': config['app']['port'], 'type': 'String'},
        f'/{environment}/app/log_level': {'value': config['app']['log_level'], 'type': 'String'},
        f'/{environment}/app/region': {'value': config['app']['region'], 'type': 'String'},
        
        # AWS
        f'/{environment}/aws/region': {'value': config['aws']['region'], 'type': 'String'},
        f'/{environment}/aws/profile': {'value': config['aws']['profile'], 'type': 'String'},
    }
    
    # Crear o actualizar parámetros
    for name, param in parameters.items():
        try:
            ssm.put_parameter(
                Name=name,
                Value=param['value'],
                Type=param['type'],
                Overwrite=True,
                Tags=[
                    {
                        'Key': 'Environment',
                        'Value': environment
                    }
                ]
            )
            print(f"Parameter {name} created/updated successfully")
        except Exception as e:
            print(f"Error creating parameter {name}: {str(e)}")

if __name__ == "__main__":
    environments = ['dev', 'stg', 'prod']
    for env in environments:
        print(f"\nConfiguring parameters for {env} environment")
        setup_parameters(env)

2.3 Configuración de Entornos

json
// config/prod.json
{
    "docdb": {
        "host": "prod-cluster.cluster-xxx.region.docdb.amazonaws.com",
        "port": "27017",
        "database": "production_db",
        "replica_set": "rs0"
    },
    "app": {
        "port": "8080",
        "log_level": "info",
        "region": "us-east-1"
    },
    "aws": {
        "region": "us-east-1",
        "profile": "production"
    }
}

Verificación Parte 2

1. Verificar Route 53

  • [ ] Health checks activos
  • [ ] Failover configurado
  • [ ] DNS propagado
  • [ ] Endpoints respondiendo

2. Verificar Parameter Store

  • [ ] Parámetros creados
  • [ ] Encriptación correcta
  • [ ] Acceso configurado
  • [ ] Valores correctos

3. Verificar Alta Disponibilidad

  • [ ] Failover funcionando
  • [ ] Latencia aceptable
  • [ ] Replicación activa
  • [ ] Monitoreo configurado

Troubleshooting Común

Errores de Route 53

  1. Verificar health checks
  2. Revisar DNS
  3. Verificar endpoints

Errores de Parameter Store

  1. Verificar permisos
  2. Revisar encriptación
  3. Verificar nombres

Errores de Configuración

  1. Verificar valores
  2. Revisar regiones
  3. Verificar entornos

Parte 3: Implementación API y Lógica de Negocio

1. Implementación API Principal

1.1 Main Application

go
// cmd/api/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/your-org/your-app/internal/config"
    "github.com/your-org/your-app/internal/database"
    "github.com/your-org/your-app/internal/handlers"
    "github.com/your-org/your-app/internal/middleware"
    "github.com/gorilla/mux"
    "go.uber.org/zap"
)

func main() {
    // Inicializar logger
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    // Cargar configuración
    cfg, err := config.LoadConfig(context.Background(), os.Getenv("ENVIRONMENT"))
    if err != nil {
        logger.Fatal("Failed to load configuration", zap.Error(err))
    }

    // Inicializar base de datos
    db, err := database.NewDocumentDB(cfg.DocDB)
    if err != nil {
        logger.Fatal("Failed to connect to database", zap.Error(err))
    }
    defer db.Close()

    // Configurar router
    router := mux.NewRouter()
    
    // Middleware
    router.Use(middleware.RequestID)
    router.Use(middleware.Logging(logger))
    router.Use(middleware.Metrics())
    router.Use(middleware.Recovery(logger))

    // Rutas
    api := router.PathPrefix("/api/v1").Subrouter()
    api.Handle("/health", handlers.HealthCheckHandler(db)).Methods("GET")
    api.Handle("/users", handlers.CreateUserHandler(db)).Methods("POST")
    api.Handle("/users/{id}", handlers.GetUserHandler(db)).Methods("GET")
    api.Handle("/users/{id}", handlers.UpdateUserHandler(db)).Methods("PUT")
    api.Handle("/users/{id}", handlers.DeleteUserHandler(db)).Methods("DELETE")

    // Servidor HTTP
    srv := &http.Server{
        Addr:         ":" + cfg.App.Port,
        Handler:      router,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Iniciar servidor en goroutine
    go func() {
        logger.Info("Starting server", zap.String("port", cfg.App.Port))
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            logger.Fatal("Server failed", zap.Error(err))
        }
    }()

    // Graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    logger.Info("Shutting down server...")
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        logger.Fatal("Server forced to shutdown", zap.Error(err))
    }

    logger.Info("Server exiting")
}

1.2 User Handlers

go
// internal/handlers/users.go
package handlers

import (
    "encoding/json"
    "net/http"
    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "github.com/your-org/your-app/internal/database"
    "github.com/your-org/your-app/internal/models"
)

type UserHandler struct {
    db *database.DocumentDB
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func CreateUserHandler(db *database.DocumentDB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req CreateUserRequest
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            http.Error(w, "Invalid request body", http.StatusBadRequest)
            return
        }

        user := &models.User{
            Name:      req.Name,
            Email:     req.Email,
            CreatedAt: time.Now(),
            UpdatedAt: time.Now(),
            Region:    os.Getenv("AWS_REGION"),
            Version:   1,
        }

        if err := db.CreateUser(r.Context(), user); err != nil {
            http.Error(w, "Failed to create user", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        json.NewEncoder(w).Encode(user)
    }
}

func GetUserHandler(db *database.DocumentDB) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)
        id, err := primitive.ObjectIDFromHex(vars["id"])
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }

        user, err := db.GetUserByID(r.Context(), id)
        if err != nil {
            if err == database.ErrNotFound {
                http.Error(w, "User not found", http.StatusNotFound)
                return
            }
            http.Error(w, "Internal server error", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    }
}

1.3 Database Operations

go
// internal/database/documentdb.go
package database

import (
    "context"
    "errors"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "github.com/your-org/your-app/internal/models"
)

var ErrNotFound = errors.New("document not found")

type DocumentDB struct {
    client *mongo.Client
    db     *mongo.Database
}

func (d *DocumentDB) CreateUser(ctx context.Context, user *models.User) error {
    collection := d.db.Collection("users")

    result, err := collection.InsertOne(ctx, user)
    if err != nil {
        return err
    }

    user.ID = result.InsertedID.(primitive.ObjectID)
    return nil
}

func (d *DocumentDB) GetUserByID(ctx context.Context, id primitive.ObjectID) (*models.User, error) {
    collection := d.db.Collection("users")

    var user models.User
    err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&user)
    if err != nil {
        if err == mongo.ErrNoDocuments {
            return nil, ErrNotFound
        }
        return nil, err
    }

    return &user, nil
}

func (d *DocumentDB) UpdateUser(ctx context.Context, user *models.User) error {
    collection := d.db.Collection("users")

    update := bson.M{
        "$set": bson.M{
            "name":       user.Name,
            "email":      user.Email,
            "updated_at": time.Now(),
            "version":    user.Version + 1,
        },
    }

    // Actualizar solo si la versión coincide
    filter := bson.M{
        "_id":     user.ID,
        "version": user.Version,
    }

    result, err := collection.UpdateOne(ctx, filter, update)
    if err != nil {
        return err
    }

    if result.MatchedCount == 0 {
        return ErrNotFound
    }

    user.Version++
    user.UpdatedAt = time.Now()
    return nil
}

2. Middleware y Utilidades

2.1 Middleware

go
// internal/middleware/logging.go
package middleware

import (
    "net/http"
    "time"
    "go.uber.org/zap"
)

func Logging(logger *zap.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // Wrap ResponseWriter para capturar el status code
            wrapped := wrapResponseWriter(w)
            
            // Procesar request
            next.ServeHTTP(wrapped, r)
            
            // Log request
            logger.Info("Request processed",
                zap.String("method", r.Method),
                zap.String("path", r.URL.Path),
                zap.Int("status", wrapped.status),
                zap.Duration("duration", time.Since(start)),
                zap.String("request_id", r.Context().Value("request_id").(string)),
            )
        })
    }
}

// internal/middleware/metrics.go
func Metrics() func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            wrapped := wrapResponseWriter(w)
            next.ServeHTTP(wrapped, r)
            
            // Publicar métricas
            duration := time.Since(start).Milliseconds()
            metrics.PublishRequestMetrics(r.Method, r.URL.Path, wrapped.status, duration)
        })
    }
}

2.2 Utilidades

go
// pkg/utils/response.go
package utils

import (
    "encoding/json"
    "net/http"
)

type ErrorResponse struct {
    Error   string `json:"error"`
    Code    string `json:"code,omitempty"`
    Details string `json:"details,omitempty"`
}

func JSONError(w http.ResponseWriter, message string, code int) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(ErrorResponse{
        Error: message,
    })
}

func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(data)
}

3. Tests

3.1 Unit Tests

go
// internal/handlers/users_test.go
package handlers

import (
    "bytes"
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/your-org/your-app/internal/database"
    "github.com/your-org/your-app/internal/models"
)

func TestCreateUser(t *testing.T) {
    // Mock database
    db := &database.MockDocumentDB{}
    
    // Test cases
    tests := []struct {
        name       string
        payload    CreateUserRequest
        wantStatus int
    }{
        {
            name: "Valid user",
            payload: CreateUserRequest{
                Name:  "John Doe",
                Email: "john@example.com",
            },
            wantStatus: http.StatusCreated,
        },
        {
            name: "Invalid email",
            payload: CreateUserRequest{
                Name:  "John Doe",
                Email: "invalid-email",
            },
            wantStatus: http.StatusBadRequest,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Create request
            body, _ := json.Marshal(tt.payload)
            req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBuffer(body))
            rec := httptest.NewRecorder()

            // Handle request
            handler := CreateUserHandler(db)
            handler.ServeHTTP(rec, req)

            // Assert response
            assert.Equal(t, tt.wantStatus, rec.Code)
        })
    }
}

3.2 Integration Tests

go
// tests/integration/api_test.go
package integration

import (
    "context"
    "testing"
    "time"

    "github.com/stretchr/testify/suite"
    "github.com/your-org/your-app/internal/config"
    "github.com/your-org/your-app/internal/database"
)

type APITestSuite struct {
    suite.Suite
    db  *database.DocumentDB
    cfg *config.Config
}

func (s *APITestSuite) SetupSuite() {
    // Cargar configuración de test
    cfg, err := config.LoadConfig(context.Background(), "test")
    s.Require().NoError(err)
    s.cfg = cfg

    // Conectar a base de datos
    db, err := database.NewDocumentDB(cfg.DocDB)
    s.Require().NoError(err)
    s.db = db
}

func (s *APITestSuite) TearDownSuite() {
    s.db.Close()
}

func (s *APITestSuite) TestUserLifecycle() {
    // Create user
    user := &models.User{
        Name:  "Test User",
        Email: "test@example.com",
    }

    err := s.db.CreateUser(context.Background(), user)
    s.Require().NoError(err)
    s.NotEmpty(user.ID)

    // Get user
    found, err := s.db.GetUserByID(context.Background(), user.ID)
    s.Require().NoError(err)
    s.Equal(user.Name, found.Name)

    // Update user
    user.Name = "Updated Name"
    err = s.db.UpdateUser(context.Background(), user)
    s.Require().NoError(err)

    // Verify update
    found, err = s.db.GetUserByID(context.Background(), user.ID)
    s.Require().NoError(err)
    s.Equal("Updated Name", found.Name)
}

func TestAPI(t *testing.T) {
    suite.Run(t, new(APITestSuite))
}

Verificación Parte 3

1. Verificar API

  • [ ] Endpoints funcionando
  • [ ] CRUD completo
  • [ ] Validaciones activas
  • [ ] Errores manejados

2. Verificar Middleware

  • [ ] Logging configurado
  • [ ] Métricas recolectadas
  • [ ] Seguridad implementada
  • [ ] Performance monitoreado

3. Verificar Tests

  • [ ] Unit tests pasando
  • [ ] Integration tests exitosos
  • [ ] Cobertura adecuada
  • [ ] CI/CD integrado

Troubleshooting Común

Errores de API

  1. Verificar rutas
  2. Revisar handlers
  3. Verificar validaciones

Errores de Middleware

  1. Verificar logging
  2. Revisar métricas
  3. Verificar segurida

Parte 4: Monitoreo, Logging y Deploy

1. Configuración de Monitoreo

1.1 CloudWatch Metrics Custom

go
// internal/monitoring/metrics.go
package monitoring

import (
    "context"
    "time"
    
    "github.com/aws/aws-sdk-go-v2/service/cloudwatch"
    "github.com/aws/aws-sdk-go-v2/aws"
)

type Metrics struct {
    client      *cloudwatch.Client
    environment string
    region      string
}

func NewMetrics(client *cloudwatch.Client, environment, region string) *Metrics {
    return &Metrics{
        client:      client,
        environment: environment,
        region:      region,
    }
}

func (m *Metrics) PutMetric(name string, value float64, unit string, dimensions map[string]string) error {
    metricDimensions := []cloudwatch.Dimension{
        {
            Name:  aws.String("Environment"),
            Value: aws.String(m.environment),
        },
        {
            Name:  aws.String("Region"),
            Value: aws.String(m.region),
        },
    }

    for k, v := range dimensions {
        metricDimensions = append(metricDimensions, cloudwatch.Dimension{
            Name:  aws.String(k),
            Value: aws.String(v),
        })
    }

    _, err := m.client.PutMetricData(context.Background(), &cloudwatch.PutMetricDataInput{
        Namespace: aws.String("Application/GoAPI"),
        MetricData: []cloudwatch.MetricDatum{
            {
                MetricName: aws.String(name),
                Value:      aws.Float64(value),
                Unit:      aws.String(unit),
                Dimensions: metricDimensions,
                Timestamp: aws.Time(time.Now()),
            },
        },
    })

    return err
}

func (m *Metrics) TrackAPILatency(endpoint string, duration time.Duration) {
    m.PutMetric("APILatency", float64(duration.Milliseconds()), "Milliseconds", map[string]string{
        "Endpoint": endpoint,
    })
}

func (m *Metrics) TrackDatabaseOperations(operation string, success bool) {
    value := 0.0
    if success {
        value = 1.0
    }
    
    m.PutMetric("DatabaseOperations", value, "Count", map[string]string{
        "Operation": operation,
        "Status":    map[bool]string{true: "Success", false: "Failure"}[success],
    })
}

1.2 Dashboard Definition

yaml
# infrastructure/cloudformation/monitoring/dashboard.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Application Monitoring Dashboard'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, stg, prod]

Resources:
  ApplicationDashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub "${Environment}-application-dashboard"
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "width": 12,
              "height": 6,
              "properties": {
                "metrics": [
                  [ "Application/GoAPI", "APILatency", "Environment", "${Environment}", "Region", "${AWS::Region}", { "stat": "Average" } ],
                  [ "...", { "stat": "p95" } ],
                  [ "...", { "stat": "p99" } ]
                ],
                "period": 300,
                "region": "${AWS::Region}",
                "title": "API Latency"
              }
            },
            {
              "type": "metric",
              "width": 12,
              "height": 6,
              "properties": {
                "metrics": [
                  [ "Application/GoAPI", "DatabaseOperations", "Status", "Success", "Environment", "${Environment}" ],
                  [ "...", "Failure", ".", "." ]
                ],
                "period": 300,
                "region": "${AWS::Region}",
                "title": "Database Operations"
              }
            },
            {
              "type": "metric",
              "width": 12,
              "height": 6,
              "properties": {
                "metrics": [
                  [ "AWS/DocDB", "CPUUtilization", "DBClusterIdentifier", "${DBClusterIdentifier}" ],
                  [ ".", "FreeableMemory", ".", "." ],
                  [ ".", "DatabaseConnections", ".", "." ]
                ],
                "period": 300,
                "region": "${AWS::Region}",
                "title": "DocumentDB Metrics"
              }
            }
          ]
        }

2. Logging Centralizado

2.1 Logger Configuration

go
// internal/logging/logger.go
package logging

import (
    "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

type Logger struct {
    *zap.Logger
    cwClient *cloudwatchlogs.Client
    logGroup string
    stream   string
}

func NewLogger(environment string) (*Logger, error) {
    // Configuración base de zap
    config := zap.NewProductionConfig()
    config.EncoderConfig.TimeKey = "timestamp"
    config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    
    baseLogger, err := config.Build()
    if err != nil {
        return nil, err
    }

    // Cliente CloudWatch Logs
    cwClient := cloudwatchlogs.NewFromConfig(cfg)

    return &Logger{
        Logger:   baseLogger,
        cwClient: cwClient,
        logGroup: "/application/" + environment,
        stream:   "api-" + time.Now().Format("2006-01-02"),
    }, nil
}

func (l *Logger) LogToCloudWatch(level zapcore.Level, msg string, fields ...zap.Field) {
    // Log localmente
    l.Log(level, msg, fields...)

    // Preparar entrada para CloudWatch
    logEvent := &cloudwatchlogs.PutLogEventsInput{
        LogGroupName:  &l.logGroup,
        LogStreamName: &l.stream,
        LogEvents: []cloudwatchlogs.InputLogEvent{
            {
                Message:   aws.String(formatLogMessage(msg, fields)),
                Timestamp: aws.Int64(time.Now().UnixNano() / 1000000),
            },
        },
    }

    // Enviar a CloudWatch de manera asíncrona
    go func() {
        if _, err := l.cwClient.PutLogEvents(context.Background(), logEvent); err != nil {
            l.Error("Failed to send logs to CloudWatch", zap.Error(err))
        }
    }()
}

func formatLogMessage(msg string, fields []zap.Field) string {
    // Implementar formato de log personalizado
    return fmt.Sprintf("%s %v", msg, fields)
}

3. Deployment Automatizado

3.1 Dockerfile Multi-stage

dockerfile
# Build stage
FROM golang:1.19-alpine AS builder

WORKDIR /app
COPY . .

RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/api/main.go

# Final stage
FROM alpine:3.14

WORKDIR /app
COPY --from=builder /app/main .
COPY config /app/config

# Instalar certificados CA
RUN apk --no-cache add ca-certificates

EXPOSE 8080
CMD ["./main"]

3.2 Deploy Script

bash
#!/bin/bash
# scripts/deploy.sh

set -e

# Variables
ENVIRONMENT=$1
REGION=$2
ECR_REPO="your-ecr-repo"
APP_NAME="go-api"

# Validar entorno
if [[ ! "$ENVIRONMENT" =~ ^(dev|stg|prod)$ ]]; then
    echo "Environment must be dev, stg, or prod"
    exit 1
fi

# Build y push de imagen
echo "Building Docker image..."
docker build -t ${APP_NAME}:${ENVIRONMENT} .

# Login a ECR
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ECR_REPO}

# Tag y push
docker tag ${APP_NAME}:${ENVIRONMENT} ${ECR_REPO}:${ENVIRONMENT}
docker push ${ECR_REPO}:${ENVIRONMENT}

# Actualizar ECS Task Definition
echo "Updating ECS task definition..."
aws ecs update-service \
    --cluster ${APP_NAME}-${ENVIRONMENT} \
    --service ${APP_NAME}-service \
    --force-new-deployment \
    --region ${REGION}

# Esperar por deployment
echo "Waiting for deployment to complete..."
aws ecs wait services-stable \
    --cluster ${APP_NAME}-${ENVIRONMENT} \
    --services ${APP_NAME}-service \
    --region ${REGION}

echo "Deployment completed successfully!"

3.3 Validación Post-Deploy

python
# scripts/validate_deployment.py
import boto3
import requests
import time
import sys

def validate_deployment(environment, region):
    # Configurar clientes AWS
    ecs = boto3.client('ecs', region_name=region)
    cloudwatch = boto3.client('cloudwatch', region_name=region)
    
    # Variables
    cluster_name = f"go-api-{environment}"
    service_name = "go-api-service"
    endpoint = f"https://api-{environment}.yourdomain.com"
    
    def check_ecs_status():
        response = ecs.describe_services(
            cluster=cluster_name,
            services=[service_name]
        )
        return response['services'][0]['runningCount'] == response['services'][0]['desiredCount']
    
    def check_api_health():
        try:
            response = requests.get(f"{endpoint}/health")
            return response.status_code == 200
        except:
            return False
    
    def check_metrics():
        response = cloudwatch.get_metric_data(
            MetricDataQueries=[
                {
                    'Id': 'errors',
                    'MetricStat': {
                        'Metric': {
                            'Namespace': 'Application/GoAPI',
                            'MetricName': 'Errors',
                            'Dimensions': [
                                {'Name': 'Environment', 'Value': environment}
                            ]
                        },
                        'Period': 300,
                        'Stat': 'Sum'
                    }
                }
            ],
            StartTime=time.time() - 300,
            EndTime=time.time()
        )
        return response['MetricDataResults'][0]['Values'][0] == 0
    
    # Validar deployment
    print("Validating deployment...")
    
    checks = [
        ("ECS Status", check_ecs_status),
        ("API Health", check_api_health),
        ("Metrics", check_metrics)
    ]
    
    for check_name, check_func in checks:
        print(f"Checking {check_name}...", end=" ")
        if check_func():
            print("OK")
        else:
            print("FAILED")
            return False
    
    return True

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: validate_deployment.py <environment> <region>")
        sys.exit(1)
    
    environment = sys.argv[1]
    region = sys.argv[2]
    
    if validate_deployment(environment, region):
        print("Deployment validation successful!")
        sys.exit(0)
    else:
        print("Deployment validation failed!")
        sys.exit(1)

Verificación Final

1. Verificar Monitoreo

  • [ ] Métricas registradas
  • [ ] Dashboard funcional
  • [ ] Alertas configuradas
  • [ ] Logs centralizados

2. Verificar Deployment

  • [ ] Build exitoso
  • [ ] Deploy completo
  • [ ] Validaciones pasando
  • [ ] Rollback funcional

3. Verificar Operación

  • [ ] Alta disponibilidad
  • [ ] Performance óptimo
  • [ ] Errores manejados
  • [ ] Backups configurados

Troubleshooting Final

Problemas de Monitoreo

  1. Verificar permisos IAM
  2. Revisar métricas
  3. Verificar logs

Problemas de Deploy

  1. Verificar build
  2. Revisar ECS
  3. Verificar red

Problemas de Operación

  1. Verificar recursos
  2. Revisar configuración
  3. Verificar seguridad

Puntos Importantes

  1. Monitoreo proactivo
  2. Deployment automatizado
  3. Validación post-deploy
  4. Operación resiliente

El sistema completo proporciona:

  1. Monitoreo completo con CloudWatch
  2. Logging centralizado
  3. Proceso de deploy automatizado
  4. Validaciones post-deploy

Puntos clave:

  • Monitoreo en tiempo real
  • Logs centralizados y estructurados
  • Deploy automatizado y seguro
  • Validaciones completas