gRPC es un framework de llamadas a procedimientos remotos que usa HTTP/2 como transporte y Protocol Buffers para definir los servicios y serializar los mensajes, lo que lo hace más eficiente y estricto que una API REST con JSON. En este tutorial definirás un servicio gRPC, generarás el código en Go, implementarás un servidor con health checks estándar y apagado ordenado, ejecutarás dos instancias con systemd y las publicarás detrás de Nginx, que termina el TLS con un certificado de Let's Encrypt y reparte las llamadas entre ellas. Todo sobre Ubuntu 24.04.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
- Un usuario no root con privilegios
sudo. - Go instalado desde go.dev/dl en
/usr/local/go, con/usr/local/go/binen elPATH. El paquetegolang-gode Ubuntu 24.04 es demasiado antiguo para las versiones actuales degrpc-go. - Un dominio o subdominio con un registro A apuntando al servidor. En esta guía se usa
your_domain. - Los puertos 80 y 443 accesibles desde Internet.
Paso 1: Instalar protoc y los plugins de Go
protoc es el compilador de Protocol Buffers; los plugins protoc-gen-go y protoc-gen-go-grpc generan, respectivamente, los tipos de los mensajes y el código cliente y servidor de gRPC. Instala el compilador desde los repositorios de Ubuntu:
sudo apt update
sudo apt install protobuf-compiler
protoc --version
libprotoc 3.21.12
Instala los plugins y grpcurl, una herramienta de línea de comandos para llamar a servicios gRPC, con go install. Los binarios quedan en ~/go/bin, que debes añadir al PATH:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.profile
source ~/.profile
protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.6.2
Paso 2: Definir el servicio en Protocol Buffers
Crea el proyecto y el directorio del contrato. Versionar el paquete (greeter.v1) te permite publicar más adelante una v2 incompatible sin romper a los clientes actuales:
mkdir -p ~/greeter/proto/greeter/v1 && cd ~/greeter
go mod init example.com/greeter
nano proto/greeter/v1/greeter.proto
syntax = "proto3";
package greeter.v1;
option go_package = "example.com/greeter/proto/greeter/v1;greeterv1";
service Greeter {
// Llamada unaria: una petición, una respuesta
rpc SayHello(HelloRequest) returns (HelloReply);
// Streaming de servidor: una petición, varias respuestas
rpc SayHelloStream(HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Genera el código Go. paths=source_relative deja los archivos generados junto al .proto:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/greeter/v1/greeter.proto
ls proto/greeter/v1
greeter.pb.go greeter.proto greeter_grpc.pb.go
No edites los archivos .pb.go a mano: se regeneran cada vez que cambias el .proto.
Paso 3: Implementar el servidor
Crea el programa del servidor:
mkdir -p cmd/server
nano cmd/server/main.go
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
greeterv1 "example.com/greeter/proto/greeter/v1"
)
type greeterServer struct {
greeterv1.UnimplementedGreeterServer
instance string
}
func (s *greeterServer) SayHello(ctx context.Context, req *greeterv1.HelloRequest) (*greeterv1.HelloReply, error) {
if req.GetName() == "" {
return nil, status.Error(codes.InvalidArgument, "name es obligatorio")
}
return &greeterv1.HelloReply{Message: fmt.Sprintf("Hola, %s (instancia %s)", req.GetName(), s.instance)}, nil
}
func (s *greeterServer) SayHelloStream(req *greeterv1.HelloRequest, stream grpc.ServerStreamingServer[greeterv1.HelloReply]) error {
for i := 1; i <= 3; i++ {
msg := fmt.Sprintf("Hola %d/3, %s", i, req.GetName())
if err := stream.Send(&greeterv1.HelloReply{Message: msg}); err != nil {
return err
}
select {
case <-stream.Context().Done():
return stream.Context().Err()
case <-time.After(time.Second):
}
}
return nil
}
func main() {
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = "127.0.0.1:50051"
}
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("no se puede escuchar en %s: %v", addr, err)
}
srv := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Minute,
Time: 1 * time.Minute,
Timeout: 20 * time.Second,
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 20 * time.Second,
PermitWithoutStream: true,
}),
)
greeterv1.RegisterGreeterServer(srv, &greeterServer{instance: addr})
// Servicio estándar de salud (grpc.health.v1.Health)
healthSrv := health.NewServer()
healthpb.RegisterHealthServer(srv, healthSrv)
healthSrv.SetServingStatus("greeter.v1.Greeter", healthpb.HealthCheckResponse_SERVING)
// Reflexión: permite a grpcurl descubrir los servicios sin el .proto
reflection.Register(srv)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
log.Printf("gRPC escuchando en %s", addr)
if err := srv.Serve(lis); err != nil {
log.Fatalf("error del servidor: %v", err)
}
}()
<-ctx.Done()
log.Println("señal recibida, apagado ordenado")
healthSrv.Shutdown() // pasa todos los servicios a NOT_SERVING
done := make(chan struct{})
go func() {
srv.GracefulStop() // espera a que terminen las llamadas en curso
close(done)
}()
select {
case <-done:
case <-time.After(20 * time.Second):
log.Println("tiempo agotado, cerrando conexiones")
srv.Stop()
}
log.Println("servidor detenido")
}
Puntos clave del servidor:
- Incrustar
UnimplementedGreeterServermantiene la compatibilidad si añades métodos al.proto: los que no implementes devuelvenUnimplementeden lugar de romper la compilación. - Los errores se devuelven con
status.Errory un código gRPC (InvalidArgument,NotFound...), que el cliente recibe de forma estructurada. - El servicio de salud implementa el protocolo estándar
grpc.health.v1.Health, que entienden balanceadores, Kubernetes ygrpcurl. - Los parámetros de
keepalivecierran conexiones inactivas y detectan clientes caídos; la política de aplicación rechaza clientes que envíen pings con demasiada frecuencia. GracefulStopdeja de aceptar llamadas nuevas y espera a las que están en curso, con un límite de 20 segundos antes de forzar el cierre.
Descarga las dependencias y compila un binario estático:
go mod tidy
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o greeter-server ./cmd/server
Pruébalo en primer plano:
./greeter-server
2026/09/25 10:12:03 gRPC escuchando en 127.0.0.1:50051
Desde otra sesión SSH, lista los servicios con grpcurl. -plaintext indica que esta conexión local no usa TLS:
grpcurl -plaintext 127.0.0.1:50051 list
greeter.v1.Greeter
grpc.health.v1.Health
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
Llama al método unario:
grpcurl -plaintext -d '{"name": "CubePath"}' 127.0.0.1:50051 greeter.v1.Greeter/SayHello
{
"message": "Hola, CubePath (instancia 127.0.0.1:50051)"
}
Detén el servidor con Ctrl+C.
Paso 4: Ejecutar dos instancias con systemd
Instala el binario y crea un usuario de sistema para el servicio:
sudo install -m 0755 -o root -g root greeter-server /usr/local/bin/greeter-server
sudo useradd --system --no-create-home --shell /usr/sbin/nologin greeter
Una unidad plantilla de systemd (con @ en el nombre) permite arrancar varias instancias del mismo servicio. El texto después de @ llega a la unidad como %i, y aquí se usa como puerto:
sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=Servidor gRPC greeter (puerto %i)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=greeter
Group=greeter
Environment=LISTEN_ADDR=127.0.0.1:%i
ExecStart=/usr/local/bin/greeter-server
Restart=on-failure
RestartSec=2
KillSignal=SIGTERM
TimeoutStopSec=30
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
Arranca las instancias en los puertos 50051 y 50052:
sudo systemctl daemon-reload
sudo systemctl enable --now greeter@50051 greeter@50052
systemctl list-units 'greeter@*' --no-pager
UNIT LOAD ACTIVE SUB DESCRIPTION
[email protected] loaded active running Servidor gRPC greeter (puerto 50051)
[email protected] loaded active running Servidor gRPC greeter (puerto 50052)
Comprueba el estado de salud de cada instancia con el protocolo estándar:
grpcurl -plaintext -d '{"service": "greeter.v1.Greeter"}' 127.0.0.1:50052 grpc.health.v1.Health/Check
{
"status": "SERVING"
}
Este mismo comando, con código de salida distinto de cero si el servicio no responde SERVING, es el que puedes usar en tu sistema de monitorización.
Paso 5: Obtener el certificado TLS
Los clientes gRPC en Internet deben conectarse siempre por TLS. Nginx terminará el TLS con un certificado de Let's Encrypt. Instala Nginx y Certbot y abre el firewall:
sudo apt install nginx certbot python3-certbot-nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Crea un bloque para el puerto 80, que Certbot usa para validar el dominio y que redirige el resto del tráfico a HTTPS:
sudo nano /etc/nginx/sites-available/greeter
server {
listen 80;
listen [::]:80;
server_name your_domain;
location / {
return 301 https://$host$request_uri;
}
}
Actívalo y solicita el certificado. Con certonly, Certbot obtiene el certificado sin modificar la configuración de Nginx:
sudo ln -s /etc/nginx/sites-available/greeter /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --nginx -d your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at: /etc/letsencrypt/live/your_domain/privkey.pem
Para que Nginx cargue el certificado renovado sin intervención, añade un hook de despliegue:
sudo sh -c 'printf "#!/bin/sh\nsystemctl reload nginx\n" > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh'
sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Paso 6: Configurar Nginx como proxy gRPC
Nginx habla gRPC con el módulo ngx_http_grpc_module, incluido en el paquete de Ubuntu. La directiva grpc_pass reenvía cada llamada a un backend del upstream, repartiéndolas por turnos entre las dos instancias. Añade el bloque HTTPS al mismo archivo del sitio:
sudo nano /etc/nginx/sites-available/greeter
upstream greeter_grpc {
server 127.0.0.1:50051 max_fails=3 fail_timeout=10s;
server 127.0.0.1:50052 max_fails=3 fail_timeout=10s;
keepalive 16;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/greeter-access.log;
error_log /var/log/nginx/greeter-error.log;
location / {
grpc_pass grpc://greeter_grpc;
grpc_set_header X-Real-IP $remote_addr;
# Streams largos: evita que Nginx corte la llamada
grpc_read_timeout 1h;
grpc_send_timeout 1h;
# Si no puede conectar con un backend, prueba el otro
grpc_next_upstream error timeout;
}
}
http2en ellistenes obligatorio: gRPC no funciona sobre HTTP/1.1.grpc_read_timeoutpor defecto es de 60 segundos, demasiado poco para llamadas de streaming que duran más.max_failsyfail_timeoutretiran temporalmente un backend que no responde, ygrpc_next_upstreampasa la llamada al otro backend cuando la conexión falla o expira. Nginx no reintenta una llamada que ya llegó a enviar al backend, porque las peticiones gRPC sonPOSTy no se consideran idempotentes.
Valida y recarga Nginx:
sudo nginx -t
sudo systemctl reload nginx
Paso 7: Probar el servicio a través de TLS
Desde tu equipo (con grpcurl instalado) o desde el propio servidor, llama al servicio por el puerto 443. Sin -plaintext, grpcurl usa TLS y valida el certificado contra las CA del sistema:
grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHello
grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHello
{
"message": "Hola, CubePath (instancia 127.0.0.1:50051)"
}
{
"message": "Hola, CubePath (instancia 127.0.0.1:50052)"
}
Las respuestas alternan entre instancias, lo que confirma el reparto de carga. Prueba también el streaming de servidor, que devuelve un mensaje por segundo:
grpcurl -d '{"name": "CubePath"}' your_domain:443 greeter.v1.Greeter/SayHelloStream
{
"message": "Hola 1/3, CubePath"
}
{
"message": "Hola 2/3, CubePath"
}
{
"message": "Hola 3/3, CubePath"
}
Y la validación de errores:
grpcurl -d '{}' your_domain:443 greeter.v1.Greeter/SayHello
ERROR:
Code: InvalidArgument
Message: name es obligatorio
Advertenciala reflexión expone la lista completa de servicios y mensajes a cualquiera que llegue al puerto 443. Es cómoda para depurar, pero en un servicio público quita
reflection.Register(srv)y distribuye el archivo.protoa tus clientes, que pueden usarlo congrpcurl -proto.
Paso 8: Actualizar sin interrumpir el servicio
Con dos instancias puedes desplegar una nueva versión reiniciándolas de una en una. Mientras una se reinicia, Nginx envía las llamadas a la otra:
sudo install -m 0755 -o root -g root greeter-server /usr/local/bin/greeter-server.new
sudo mv /usr/local/bin/greeter-server.new /usr/local/bin/greeter-server
sudo systemctl restart greeter@50051
grpcurl -plaintext -d '{"service": "greeter.v1.Greeter"}' 127.0.0.1:50051 grpc.health.v1.Health/Check
sudo systemctl restart greeter@50052
Reinicia la segunda instancia solo después de que la primera responda SERVING. En el journal verás el apagado ordenado:
journalctl -u greeter@50051 -n 4 --no-pager
... greeter-server[7102]: señal recibida, apagado ordenado
... greeter-server[7102]: servidor detenido
... systemd[1]: Started [email protected] - Servidor gRPC greeter (puerto 50051).
... greeter-server[7240]: gRPC escuchando en 127.0.0.1:50051
Solución de problemas
grpcurl devuelve Failed to dial target host. Nginx no escucha en 443 o el firewall lo bloquea. Comprueba sudo ss -ltnp 'sport = :443' y sudo ufw status.
El cliente recibe Unavailable con un 502 de Nginx. Ninguna instancia responde. Revisa systemctl status 'greeter@*' y /var/log/nginx/greeter-error.log.
Error upstream sent too large http2 frame en el log de Nginx. El backend no habla HTTP/2 en texto plano: el upstream apunta a un puerto HTTP/1.1 o a un servidor gRPC con TLS. En el segundo caso usa grpc_pass grpcs:// en lugar de grpc://. Recuerda también que para gRPC se usa grpc_pass, no proxy_pass.
Los streams se cortan justo al minuto. Falta grpc_read_timeout en la location; el valor por defecto es de 60 segundos.
ENHANCE_YOUR_CALM y too_many_pings en los logs del cliente. El cliente envía pings de keepalive más a menudo que el MinTime de 20 segundos de la política del servidor. Sube el intervalo en el cliente o baja MinTime.
Conclusión
Tienes un servicio gRPC en Go definido con Protocol Buffers, con health checks estándar y apagado ordenado, ejecutándose en dos instancias gestionadas por systemd y publicado con TLS a través de Nginx, que reparte las llamadas y retira los backends caídos. Como siguientes pasos, puedes generar clientes en otros lenguajes a partir del mismo .proto, añadir autenticación con un interceptor que valide un token en los metadatos de cada llamada, o validar tus archivos .proto y detectar cambios incompatibles con la herramienta buf.
