Desarrollo backend

Go y gRPC en Kubernetes: comunicación entre servicios de alto rendimiento con load balancing y observability

Ruslan Ismailov Publicado 22 min de lectura
G

1. Introducción: por qué gRPC supera a REST en la comunicación entre servicios dentro de Kubernetes en 2026

En 2026, gRPC se ha consolidado definitivamente como el estándar de facto para la comunicación entre servicios dentro de clústeres de Kubernetes. Mientras que REST API sigue siendo una opción cómoda para APIs públicas e interacción con navegadores, dentro de una arquitectura de microservicios gRPC ofrece ventajas fundamentales.

En primer lugar, gRPC utiliza HTTP/2, lo que implica multiplexación de solicitudes dentro de una sola conexión, compresión de cabeceras y serialización binaria mediante Protocol Buffers. En la práctica, esto se traduce en una reducción de latencia del 20–40% y una disminución del volumen de datos transmitidos de 3 a 10 veces en comparación con REST API basado en JSON.

En segundo lugar, el tipado estricto a través de archivos .proto garantiza la programación por contrato: cualquier violación de la API se detecta de inmediato en la fase de generación de código, no en tiempo de ejecución. Esto es crítico cuando un equipo de 10 o más desarrolladores trabaja en distintos microservicios.

En tercer lugar, gRPC soporta de forma nativa cuatro tipos de interacción: RPC unario, streaming del servidor, streaming del cliente y streaming bidireccional — capacidades que son extremadamente difíciles de implementar con REST API puro.

Por último, el ecosistema de Go cuenta con soporte de primera clase para gRPC a través de la biblioteca oficial google.golang.org/grpc, lo que hace que la combinación Go + gRPC + Kubernetes sea especialmente potente para sistemas de alta carga.

2. Configuración básica de servicios gRPC en Go: archivos proto, generación de código, estructura del proyecto

Comenzamos definiendo el servicio mediante Protocol Buffers. Crearemos un servicio típico para la gestión de pedidos en una arquitectura de microservicios.

Estructura del proyecto

order-service/
├── api/
│   └── proto/
│       └── order/
│           └── v1/
│               └── order.proto
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── server/
│   │   └── order_server.go
│   └── repository/
│       └── order_repo.go
├── gen/
│   └── go/
│       └── order/
│           └── v1/
├── Makefile
├── Dockerfile
└── go.mod

Archivo Proto

// api/proto/order/v1/order.proto
syntax = "proto3";

package order.v1;

option go_package = "github.com/myorg/order-service/gen/go/order/v1;orderv1";

import "google/protobuf/timestamp.proto";

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_PROCESSING = 2;
  ORDER_STATUS_COMPLETED = 3;
  ORDER_STATUS_CANCELLED = 4;
}

message Order {
  string id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  double total_amount = 5;
  google.protobuf.Timestamp created_at = 6;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

message CreateOrderRequest {
  string customer_id = 1;
  repeated OrderItem items = 2;
}

message CreateOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string order_id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

message ListOrdersRequest {
  string customer_id = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message ListOrdersResponse {
  repeated Order orders = 1;
  string next_page_token = 2;
}

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  // Streaming del servidor para seguimiento de cambios de estado
  rpc WatchOrderStatus(GetOrderRequest) returns (stream Order);
}

Generación de código mediante Makefile

# Makefile
PROTO_DIR := api/proto
GEN_DIR := gen/go

.PHONY: proto
proto:
	protoc \
		--proto_path=$(PROTO_DIR) \
		--go_out=$(GEN_DIR) \
		--go_opt=paths=source_relative \
		--go-grpc_out=$(GEN_DIR) \
		--go-grpc_opt=paths=source_relative \
		$(shell find $(PROTO_DIR) -name '*.proto')

.PHONY: run
run:
	go run ./cmd/server/...

.PHONY: test
test:
	go test -v -race ./...

Implementación del servidor gRPC en Go

// internal/server/order_server.go
package server

import (
	"context"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	orderv1 "github.com/myorg/order-service/gen/go/order/v1"
	"github.com/myorg/order-service/internal/repository"
)

type OrderServer struct {
	orderv1.UnimplementedOrderServiceServer
	repo repository.OrderRepository
}

func NewOrderServer(repo repository.OrderRepository) *OrderServer {
	return &OrderServer{repo: repo}
}

func (s *OrderServer) CreateOrder(
	ctx context.Context,
	req *orderv1.CreateOrderRequest,
) (*orderv1.CreateOrderResponse, error) {
	if req.CustomerId == "" {
		return nil, status.Error(codes.InvalidArgument, "customer_id is required")
	}
	if len(req.Items) == 0 {
		return nil, status.Error(codes.InvalidArgument, "at least one item is required")
	}

	order, err := s.repo.Create(ctx, req.CustomerId, req.Items)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to create order: %v", err)
	}

	return &orderv1.CreateOrderResponse{Order: order}, nil
}

func (s *OrderServer) WatchOrderStatus(
	req *orderv1.GetOrderRequest,
	stream orderv1.OrderService_WatchOrderStatusServer,
) error {
	ctx := stream.Context()
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
			order, err := s.repo.GetByID(ctx, req.OrderId)
			if err != nil {
				return status.Errorf(codes.Internal, "failed to get order: %v", err)
			}
			if err := stream.Send(order); err != nil {
				return err
			}
			if order.Status == orderv1.OrderStatus_ORDER_STATUS_COMPLETED ||
				order.Status == orderv1.OrderStatus_ORDER_STATUS_CANCELLED {
				return nil
			}
		}
	}
}

Punto de entrada del servidor

// cmd/server/main.go
package main

import (
	"fmt"
	"net"
	"os"
	"os/signal"
	"syscall"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	orderv1 "github.com/myorg/order-service/gen/go/order/v1"
	"github.com/myorg/order-service/internal/server"
)

func main() {
	port := os.Getenv("GRPC_PORT")
	if port == "" {
		port = "50051"
	}

	lis, err := net.Listen("tcp", fmt.Sprintf(":%s", port))
	if err != nil {
		panic(err)
	}

	grpcServer := grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			loggingInterceptor,
			recoveryInterceptor,
		),
	)

	orderSrv := server.NewOrderServer(/* inject dependencies */)
	orderv1.RegisterOrderServiceServer(grpcServer, orderSrv)

	// Reflexión gRPC para grpcurl y otras herramientas
	reflection.Register(grpcServer)

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)

	go func() {
		fmt.Printf("gRPC server listening on :%s\n", port)
		if err := grpcServer.Serve(lis); err != nil {
			panic(err)
		}
	}()

	<-quit
	fmt.Println("Shutting down gRPC server...")
	grpcServer.GracefulStop()
}

3. Particularidades de gRPC en Kubernetes: por qué el kube-proxy estándar funciona mal con gRPC

Aquí se esconde una de las trampas más comunes al desplegar servicios gRPC en Kubernetes. kube-proxy implementa el balanceo de carga a nivel L4 (TCP), utilizando iptables o IPVS. Para HTTP/1.1 esto funciona bien: cada solicitud es una nueva conexión TCP y el balanceador puede distribuirlas de forma equitativa.

gRPC, construido sobre HTTP/2, establece una única conexión TCP de larga duración y multiplexa todas las solicitudes a través de ella. Cuando un cliente gRPC se conecta a un ClusterIP Service, kube-proxy dirige esa única conexión a un Pod concreto y ya no balancea las llamadas RPC posteriores. Como resultado, un Pod recibe el 100% del tráfico mientras los demás permanecen inactivos.

El problema se ilustra claramente con este esquema: 3 réplicas de order-service, un ClusterIP Service, y todo el tráfico va únicamente al Pod #1.

Existen tres enfoques principales para resolver este problema:

  • Balanceo de carga en el cliente (client-side) — el cliente conoce todos los endpoints y balancea las solicitudes por sí mismo.
  • Proxy/sidecar — Envoy u otro proxy L7 intercepta el tráfico y lo balancea a nivel de frames HTTP/2.
  • Headless Service + DNS — el DNS devuelve las IPs de todos los Pods y el cliente se conecta a todos.

4. Balanceo de carga en el cliente para gRPC en Go

El cliente gRPC de Go tiene soporte integrado para el balanceo de carga mediante las interfaces resolver.Resolver y balancer.Balancer. El punto clave es que el cliente debe obtener la lista de todas las IPs de los Pods, no el ClusterIP del Service.

Uso de Headless Service y DNS resolver

Para el balanceo en el cliente creamos un Headless Service (con clusterIP: None). En este caso, el DNS devuelve registros A para cada Pod en lugar de un único ClusterIP.

# kubernetes/order-service-headless.yaml
apiVersion: v1
kind: Service
metadata:
  name: order-service-headless
  namespace: production
spec:
  clusterIP: None  # Esto convierte el Service en headless
  selector:
    app: order-service
  ports:
    - name: grpc
      port: 50051
      targetPort: 50051
      protocol: TCP

Cliente gRPC con balanceo round-robin

// pkg/client/order_client.go
package client

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/balancer/roundrobin"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/keepalive"

	orderv1 "github.com/myorg/order-service/gen/go/order/v1"
)

type OrderClient struct {
	client orderv1.OrderServiceClient
	conn   *grpc.ClientConn
}

func NewOrderClient(ctx context.Context, target string) (*OrderClient, error) {
	// target para headless service: "dns:///order-service-headless.production.svc.cluster.local:50051"
	conn, err := grpc.DialContext(
		ctx,
		target,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		// Activamos balanceo round-robin
		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
		// Keepalive para mantener conexiones activas con todos los endpoints
		grpc.WithKeepaliveParams(keepalive.ClientParameters{
			Time:                10 * time.Second,
			Timeout:             3 * time.Second,
			PermitWithoutStream: true,
		}),
		// Activamos wait-for-ready para reconexión automática
		grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to dial order service: %w", err)
	}

	return &OrderClient{
		client: orderv1.NewOrderServiceClient(conn),
		conn:   conn,
	}, nil
}

func (c *OrderClient) CreateOrder(
	ctx context.Context,
	customerID string,
	items []*orderv1.OrderItem,
) (*orderv1.Order, error) {
	resp, err := c.client.CreateOrder(ctx, &orderv1.CreateOrderRequest{
		CustomerId: customerID,
		Items:      items,
	})
	if err != nil {
		return nil, fmt.Errorf("CreateOrder RPC failed: %w", err)
	}
	return resp.Order, nil
}

func (c *OrderClient) Close() error {
	return c.conn.Close()
}

Resolver personalizado para Kubernetes

Para una gestión más flexible, se puede implementar un resolver personalizado que acceda directamente a la API de Endpoints de Kubernetes mediante client-go. Esto permite reaccionar de inmediato a los cambios en los Pods sin depender del TTL de DNS.

// pkg/resolver/k8s_resolver.go
package resolver

import (
	"context"
	"fmt"

	v1 "k8s.io/api/core/v1"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/cache"
	"google.golang.org/grpc/resolver"
)

const Scheme = "k8s"

type k8sResolverBuilder struct {
	clientset *kubernetes.Clientset
}

func NewBuilder(clientset *kubernetes.Clientset) resolver.Builder {
	return &k8sResolverBuilder{clientset: clientset}
}

func (b *k8sResolverBuilder) Build(
	target resolver.Target,
	cc resolver.ClientConn,
	opts resolver.BuildOptions,
) (resolver.Resolver, error) {
	r := &k8sResolver{
		cc:        cc,
		clientset: b.clientset,
		namespace: target.URL.Host,
		service:   target.URL.Path[1:], // eliminamos la barra inicial
		ctx:       context.Background(),
	}
	go r.watch()
	return r, nil
}

func (b *k8sResolverBuilder) Scheme() string { return Scheme }

type k8sResolver struct {
	cc        resolver.ClientConn
	clientset *kubernetes.Clientset
	namespace string
	service   string
	ctx       context.Context
}

func (r *k8sResolver) watch() {
	factory := informers.NewSharedInformerFactoryWithOptions(
		r.clientset,
		0,
		informers.WithNamespace(r.namespace),
	)
	endpointsInformer := factory.Core().V1().Endpoints().Informer()
	endpointsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    func(obj interface{}) { r.updateAddresses(obj) },
		UpdateFunc: func(_, obj interface{}) { r.updateAddresses(obj) },
		DeleteFunc: func(obj interface{}) { r.updateAddresses(obj) },
	})
	factory.Start(r.ctx.Done())
}

func (r *k8sResolver) updateAddresses(obj interface{}) {
	endpoints, ok := obj.(*v1.Endpoints)
	if !ok || endpoints.Name != r.service {
		return
	}

	var addrs []resolver.Address
	for _, subset := range endpoints.Subsets {
		for _, addr := range subset.Addresses {
			for _, port := range subset.Ports {
				addrs = append(addrs, resolver.Address{
					Addr: fmt.Sprintf("%s:%d", addr.IP, port.Port),
				})
			}
		}
	}

	r.cc.UpdateState(resolver.State{Addresses: addrs})
}

func (r *k8sResolver) ResolveNow(opts resolver.ResolveNowOptions) {}
func (r *k8sResolver) Close()                                     {}

5. Balanceo de carga en el servidor: Envoy como sidecar

Una alternativa al balanceo en el cliente es usar el proxy Envoy en modo sidecar. Envoy comprende HTTP/2 y gRPC a nivel L7, lo que permite balancear llamadas RPC individuales en lugar de conexiones TCP.

Deployment con Envoy sidecar

# kubernetes/order-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: myorg/order-service:latest
        ports:
        - containerPort: 50051
          name: grpc
        env:
        - name: GRPC_PORT
          value: "50051"
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
      - name: envoy
        image: envoyproxy/envoy:v1.28.0
        ports:
        - containerPort: 10000
          name: grpc-envoy
        - containerPort: 9901
          name: admin
        volumeMounts:
        - name: envoy-config
          mountPath: /etc/envoy
      volumes:
      - name: envoy-config
        configMap:
          name: envoy-config

Configuración de Envoy para gRPC

# kubernetes/envoy-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-config
  namespace: production
data:
  envoy.yaml: |
    static_resources:
      listeners:
      - name: grpc_listener
        address:
          socket_address:
            address: 0.0.0.0
            port_value: 10000
        filter_chains:
        - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              codec_type: AUTO
              stat_prefix: grpc_ingress
              http2_protocol_options: {}
              route_config:
                name: local_route
                virtual_hosts:
                - name: backend
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: order_service_cluster
                      timeout: 30s
                      retry_policy:
                        retry_on: "reset,connect-failure,retriable-status-codes"
                        num_retries: 3
                        retriable_status_codes: [503]
              http_filters:
              - name: envoy.filters.http.grpc_stats
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig
                  stats_for_all_methods: true
              - name: envoy.filters.http.router
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

      clusters:
      - name: order_service_cluster
        connect_timeout: 5s
        type: STRICT_DNS
        lb_policy: ROUND_ROBIN
        typed_extension_protocol_options:
          envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
            "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
            explicit_http_config:
              http2_protocol_options: {}
        load_assignment:
          cluster_name: order_service_cluster
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: order-service-headless.production.svc.cluster.local
                    port_value: 50051

    admin:
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 9901

6. Health checking y graceful shutdown

Kubernetes requiere endpoints de health check funcionales para gestionar correctamente el ciclo de vida de los Pods. El protocolo gRPC Health Checking es la forma estándar de implementarlo.

Implementación de health check gRPC en Go

// cmd/server/main.go (versión ampliada)
package main

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

	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/reflection"

	orderv1 "github.com/myorg/order-service/gen/go/order/v1"
	"github.com/myorg/order-service/internal/server"
)

func main() {
	grpcPort := getEnv("GRPC_PORT", "50051")
	httpPort := getEnv("HTTP_PORT", "8080") // para HTTP liveness probe

	lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort))
	if err != nil {
		panic(err)
	}

	grpcServer := grpc.NewServer()

	// Registramos el servicio de health check
	healthSrv := health.NewServer()
	grpc_health_v1.RegisterHealthServer(grpcServer, healthSrv)

	orderSrv := server.NewOrderServer()
	orderv1.RegisterOrderServiceServer(grpcServer, orderSrv)
	reflection.Register(grpcServer)

	// Marcamos el servicio como SERVING
	healthSrv.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_SERVING)
	healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)

	// Servidor HTTP para liveness/readiness probes de Kubernetes
	httpMux := http.NewServeMux()
	httpMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ok"))
	})
	httpMux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
		// Verificamos dependencias (DB, caché, etc.)
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ready"))
	})
	httpServer := &http.Server{
		Addr:    fmt.Sprintf(":%s", httpPort),
		Handler: httpMux,
	}

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)

	go func() {
		fmt.Printf("gRPC server on :%s\n", grpcPort)
		if err := grpcServer.Serve(lis); err != nil {
			panic(err)
		}
	}()

	go func() {
		fmt.Printf("HTTP health server on :%s\n", httpPort)
		if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			panic(err)
		}
	}()

	<-quit

	// Graceful shutdown
	// 1. Marcamos el servicio como NOT_SERVING para que no lleguen nuevas solicitudes
	healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
	healthSrv.SetServingStatus("order.v1.OrderService", grpc_health_v1.HealthCheckResponse_NOT_SERVING)

	// 2. Damos tiempo a Kubernetes para actualizar los Endpoints (terminationGracePeriodSeconds)
	time.Sleep(5 * time.Second)

	// 3. Finalizamos las solicitudes en curso
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	httpServer.Shutdown(ctx)
	grpcServer.GracefulStop()
}

func getEnv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

Kubernetes Deployment con probes

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: order-service
        image: myorg/order-service:latest
        ports:
        - containerPort: 50051
          name: grpc
        - containerPort: 8080
          name: http
        livenessProbe:
          httpGet:
            path: /healthz
            port: http
          initialDelaySeconds: 10
          periodSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
          failureThreshold: 3
        lifecycle:
          preStop:
            exec:
              # Retraso adicional antes de SIGTERM para un drain correcto
              command: ["/bin/sh", "-c", "sleep 5"]

7. Observability: métricas, trazado y logging

La observability es uno de los aspectos clave de los servicios gRPC listos para producción en Kubernetes. Veremos la integración con Prometheus y OpenTelemetry.

Métricas gRPC con Prometheus

// cmd/server/main.go — añadimos métricas de Prometheus
package main

import (
	"net/http"

	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/prometheus"
	prom "github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"google.golang.org/grpc"
)

func buildGRPCServer() *grpc.Server {
	reg := prom.NewRegistry()

	grpcMetrics := prometheus.NewServerMetrics(
		prometheus.WithServerHandlingTimeHistogram(
			prometheus.WithHistogramBuckets([]float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}),
		),
	)
	reg.MustRegister(grpcMetrics)

	grpcServer := grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			grpcMetrics.UnaryServerInterceptor(),
		),
		grpc.ChainStreamInterceptor(
			grpcMetrics.StreamServerInterceptor(),
		),
	)

	// Endpoint HTTP de Prometheus
	http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))

	return grpcServer
}

Trazado con OpenTelemetry

// pkg/telemetry/tracer.go
package telemetry

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

func InitTracer(ctx context.Context, serviceName, otlpEndpoint string) (func(), error) {
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint(otlpEndpoint),
		otlptracegrpc.WithInsecure(),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create OTLP exporter: %w", err)
	}

	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceName(serviceName),
			semconv.ServiceVersion("1.0.0"),
		),
	)
	if err != nil {
		return nil, err
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
	)

	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))

	return func() { tp.Shutdown(ctx) }, nil
}

// Integración de OTel con gRPC
// En main.go añadimos los interceptors:
// import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
//
// grpc.NewServer(
//     grpc.StatsHandler(otelgrpc.NewServerHandler()),
// )

ServiceMonitor para Prometheus Operator

# kubernetes/service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: order-service-monitor
  namespace: production
  labels:
    release: kube-prometheus-stack
spec:
  selector:
    matchLabels:
      app: order-service
  endpoints:
  - port: http
    path: /metrics
    interval: 15s
    scrapeTimeout: 10s

Logging estructurado

// internal/interceptors/logging.go
package interceptors

import (
	"context"
	"time"

	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func LoggingUnaryInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
	return func(
		ctx context.Context,
		req interface{},
		info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler,
	) (interface{}, error) {
		start := time.Now()
		resp, err := handler(ctx, req)
		duration := time.Since(start)

		code := codes.OK
		if err != nil {
			code = status.Code(err)
		}

		logger.Info("gRPC request",
			zap.String("method", info.FullMethod),
			zap.String("code", code.String()),
			zap.Duration("duration", duration),
			zap.Error(err),
		)
		return resp, err
	}
}

8. Seguridad: mTLS para gRPC en Kubernetes sin service mesh

La autenticación mutua mediante mTLS es el estándar de seguridad para gRPC en producción. Lo implementamos sin service mesh, usando cert-manager para la gestión de certificados.

Instalación de cert-manager y creación de la CA

# kubernetes/cert-manager-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-secret
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: order-service-tls
  namespace: production
spec:
  secretName: order-service-tls
  duration: 2160h  # 90 días
  renewBefore: 360h  # renovamos 15 días antes de la expiración
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  subject:
    organizations:
      - myorg
  dnsNames:
    - order-service.production.svc.cluster.local
    - order-service-headless.production.svc.cluster.local
  usages:
    - server auth
    - client auth  # Necesario para mTLS

Servidor gRPC con mTLS

// pkg/tls/config.go
package tlsconfig

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"os"

	"google.golang.org/grpc/credentials"
)

func NewServerTLSCredentials(certFile, keyFile, caFile string) (credentials.TransportCredentials, error) {
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, fmt.Errorf("load server keypair: %w", err)
	}

	caCert, err := os.ReadFile(caFile)
	if err != nil {
		return nil, fmt.Errorf("read CA cert: %w", err)
	}

	caPool := x509.NewCertPool()
	if !caPool.AppendCertsFromPEM(caCert) {
		return nil, fmt.Errorf("failed to add CA cert to pool")
	}

	tlsCfg := &tls.Config{
		Certificates: []tls.Certificate{cert},
		ClientCAs:    caPool,
		ClientAuth:   tls.RequireAndVerifyClientCert, // exigimos certificado de cliente
		MinVersion:   tls.VersionTLS13,
	}

	return credentials.NewTLS(tlsCfg), nil
}

func NewClientTLSCredentials(certFile, keyFile, caFile string) (credentials.TransportCredentials, error) {
	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return nil, fmt.Errorf("load client keypair: %w", err)
	}

	caCert, err := os.ReadFile(caFile)
	if err != nil {
		return nil, fmt.Errorf("read CA cert: %w", err)
	}

	caPool := x509.NewCertPool()
	if !caPool.AppendCertsFromPEM(caCert) {
		return nil, fmt.Errorf("failed to add CA cert to pool")
	}

	tlsCfg := &tls.Config{
		Certificates: []tls.Certificate{cert},
		RootCAs:      caPool,
		MinVersion:   tls.VersionTLS13,
	}

	return credentials.NewTLS(tlsCfg), nil
}

Montaje de secretos TLS en el Pod

# Complemento a deployment.yaml
spec:
  containers:
  - name: order-service
    env:
    - name: TLS_CERT_FILE
      value: /etc/tls/tls.crt
    - name: TLS_KEY_FILE
      value: /etc/tls/tls.key
    - name: TLS_CA_FILE
      value: /etc/tls/ca.crt
    volumeMounts:
    - name: tls-certs
      mountPath: /etc/tls
      readOnly: true
  volumes:
  - name: tls-certs
    secret:
      secretName: order-service-tls

9. Pruebas de servicios gRPC

Las pruebas completas de gRPC en Go incluyen pruebas unitarias con mocks, pruebas de integración con un servidor gRPC real y pruebas de contrato.

Mocks con mockery y pruebas del servidor

// internal/server/order_server_test.go
package server_test

import (
	"context"
	"net"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"

	orderv1 "github.com/myorg/order-service/gen/go/order/v1"
	"github.com/myorg/order-service/internal/mocks"
	"github.com/myorg/order-service/internal/server"
)

func setupTestServer(t *testing.T, mockRepo *mocks.OrderRepository) orderv1.OrderServiceClient {
	t.Helper()

	lis, err := net.Listen("tcp", "127.0.0.1:0")
	require.NoError(t, err)

	grpcSrv := grpc.NewServer()
	orderv1.RegisterOrderServiceServer(grpcSrv, server.NewOrderServer(mockRepo))

	go grpcSrv.Serve(lis)
	t.Cleanup(grpcSrv.Stop)

	conn, err := grpc.Dial(
		lis.Addr().String(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	require.NoError(t, err)
	t.Cleanup(func() { conn.Close() })

	return orderv1.NewOrderServiceClient(conn)
}

func TestCreateOrder_Success(t *testing.T) {
	mockRepo := mocks.NewOrderRepository(t)
	mockRepo.On("Create", mock.Anything, "customer-123", mock.Anything).
		Return(&orderv1.Order{
			Id:         "order-456",
			CustomerId: "customer-123",
			Status:     orderv1.OrderStatus_ORDER_STATUS_PENDING,
		}, nil)

	client := setupTestServer(t, mockRepo)

	resp, err := client.CreateOrder(context.Background(), &orderv1.CreateOrderRequest{
		CustomerId: "customer-123",
		Items: []*orderv1.OrderItem{
			{ProductId: "prod-1", Quantity: 2, Price: 99.99},
		},
	})

	require.NoError(t, err)
	assert.Equal(t, "order-456", resp.Order.Id)
	mockRepo.AssertExpectations(t)
}

func TestCreateOrder_ValidationError(t *testing.T) {
	mockRepo := mocks.NewOrderRepository(t)
	client := setupTestServer(t, mockRepo)

	_, err := client.CreateOrder(context.Background(), &orderv1.CreateOrderRequest{
		CustomerId: "", // customer_id vacío
		Items:      []*orderv1.OrderItem{},
	})

	require.Error(t, err)
	st, ok := status.FromError(err)
	require.True(t, ok)
	assert.Equal(t, codes.InvalidArgument, st.Code())
	mockRepo.AssertNotCalled(t, "Create")
}

Herramientas para pruebas manuales

Para las pruebas manuales de servicios gRPC, las siguientes herramientas son imprescindibles:

  • grpcurl — el curl para gRPC. Permite enviar solicitudes a un servidor con reflection habilitado: grpcurl -plaintext localhost:50051 order.v1.OrderService/GetOrder
  • grpcui — interfaz web para gRPC, similar a Postman.
  • evans — shell REPL interactivo para gRPC con soporte para TLS y metadatos.
  • ghz — herramienta de pruebas de carga para gRPC: ghz --insecure --proto order.proto --call order.v1.OrderService.GetOrder -d '{"order_id":"123"}' localhost:50051

Pruebas de contrato con protovalidate

// Usamos buf validate para validar contratos proto
// en order.proto añadimos:
// import "buf/validate/validate.proto";
//
// message CreateOrderRequest {
//   string customer_id = 1 [(buf.validate.field).string.min_len = 1];
//   repeated OrderItem items = 2 [(buf.validate.field).repeated.min_items = 1];
// }

// En el servidor usamos el interceptor:
func validationInterceptor() grpc.UnaryServerInterceptor {
	v, _ := protovalidate.New()
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		if msg, ok := req.(proto.Message); ok {
			if err := v.Validate(msg); err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "validation failed: %v", err)
			}
		}
		return handler(ctx, req)
	}
}

10. Conclusión: cuándo gRPC es la elección correcta y cuándo es mejor REST API

Tras esta inmersión profunda en gRPC para Go y Kubernetes, resumimos cuándo vale la pena invertir en gRPC y cuándo es más sencillo mantener REST API.

gRPC es la elección correcta cuando:

  • La comunicación entre servicios ocurre dentro del clúster de Kubernetes, donde los clientes son otros servicios y no navegadores.
  • El rendimiento es crítico: los sistemas de alta carga con miles de RPS por servicio obtendrán una ganancia notable gracias a la serialización binaria y HTTP/2.
  • Se necesita un contrato de API estricto: los archivos .proto como única fuente de verdad previenen cambios incompatibles.
  • Se requiere streaming: actualizaciones en tiempo real, event streaming, interacción bidireccional.
  • Entorno multilenguaje: los archivos .proto generan clientes en Go, Python, Java, Rust — sin duplicación de código.
  • La observability de serie es importante: códigos de estado gRPC, métricas estándar con Prometheus, integración nativa con OpenTelemetry.

REST API sigue siendo la mejor opción cuando:

  • La API es pública y es consumida por navegadores o clientes externos.
  • El equipo es pequeño y el overhead de los archivos proto y la generación de código no está justificado.
  • Se necesita depuración sencilla mediante curl sin herramientas especializadas.
  • El servicio se integra con sistemas legacy o APIs de terceros que esperan JSON/HTTP.
  • Se requiere soporte para webhooks u operaciones CRUD simples sin alta carga.

La estrategia óptima para la mayoría de equipos en 2026 es: gRPC para la comunicación interna entre servicios (tráfico east-west) y REST API o GraphQL para APIs públicas externas (tráfico north-south). Para los servicios gRPC en Kubernetes, es imprescindible configurar el balanceo de carga client-side o basado en proxy, una observability completa con Prometheus y OpenTelemetry, y mTLS mediante cert-manager — estos tres componentes transforman una integración gRPC básica en un sistema fiable y listo para producción.

Los pipelines de CI/CD para servicios gRPC en Go también deben adaptarse: añada fases de linting de archivos proto con buf, verificación de breaking changes (buf breaking), generación de código y ejecución de pruebas de integración con un servidor gRPC real — esto garantizará la estabilidad de los contratos en una arquitectura de microservicios en rápido crecimiento.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →