Go y gRPC en Kubernetes: comunicación entre servicios de alto rendimiento con load balancing y observability
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í →