Skip to main content

Cómo crear una API segura con gRPC

Escrito por
Headshot of Vitalis Ogbonna

Vitalis Ogbonna

hero build api grpc

25 de agosto de 2022

0 minutos de lectura

Una llamada a procedimiento remoto de Google (gRPC) es la versión de código abierto de Google del framework de llamadas a procedimientos remotos (RPC). Es un protocolo de comunicación que aprovecha las tecnologías HTTP/2 y Protocol Buffers (protobuf). gRPC permite que un cliente o servidor remoto se comunique con otro servidor con solo llamar a la función del servidor receptor como si fuera local. Esto facilita mucho la comunicación y la transferencia de grandes conjuntos de datos entre el cliente y el servidor en sistemas distribuidos.

Al igual que otros sistemas RPC, gRPC define un servicio. Especifica sus métodos y tipos de retorno mediante protobuf —un protocolo de Google para la serialización y deserialización—, lo que facilita la definición de servicios y la generación automática de bibliotecas cliente. gRPC usa este protocolo, actualmente en la versión 3, como lenguaje de definición de interfaces y conjunto de herramientas de serialización.

Para la mayoría de las aplicaciones modernas, gRPC es una excelente opción gracias a su compatibilidad con todo tipo de datos. Es ideal para grandes volúmenes de datos, como los datos de streaming, y puede ser excesivo para aplicaciones sencillas en las que la transferencia de grandes cantidades de datos no es una prioridad.

En este artículo aprenderás a usar gRPC mediante la comunicación entre un cliente y un servidor de dos aplicaciones Node.js. También destacaremos algunas medidas de seguridad para usar gRPC como mecanismo de comunicación en tus servicios.

Requisitos previos del tutorial

Para este tutorial, debes tener OpenSSL y Node.js (versión 4.0 o posterior) instalados en tu PC. Es fundamental tener conocimientos básicos de Node.js y JavaScript. También debes asegurarte de que tu entorno de trabajo tenga privilegios de administrador.

Configurar el proyecto de Node.js

Primero, para establecer la estructura de carpetas de la aplicación, crea una carpeta llamada event-app-node-grpc e inicializa un proyecto de Node.js con npm. Para ello, escribe los siguientes comandos:

#bash
$ mkdir event-app-node-grpc
$ cd event-app-node-grpc
$ npm init -y

Una vez inicializada la aplicación, crea la siguiente estructura de carpetas. Puedes consultar en GitHub el código completo que usamos en este tutorial:

Event-app-node-grpc
client
app.js
index.js
server
index.js
scripts
generate-certs.sh
events.proto
README.md

Instalar paquetes

En la terminal, ve al directorio raíz de la aplicación. Instala los siguientes paquetes con el comando npm install, como se muestra en el siguiente fragmento de código:

#bash
$ npm install express @grpc/grpc-js @grpc/proto-loader

Veamos los paquetes que acabas de instalar en el fragmento de código anterior:

  • Express es el servidor HTTP de tu aplicación.

  • @grpc/grpc-js es una biblioteca de gRPC para Node.js. Permite crear un servicio gRPC en el entorno de ejecución de Node.js.

  • @grpc/proto-loader es un paquete necesario para cargar archivos protobuf y usarlos con gRPC. Usa la versión 3 del paquete protobuf.js.

Después de instalar los paquetes anteriores, abre el archivo package.json y agrega las siguientes configuraciones a las etiquetas scripts, como se muestra en el fragmento de código:

#package.json

"scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },

Las configuraciones adicionales del fragmento de código anterior corresponden a la configuración del entorno de ejecución de la aplicación y a la generación del certificado SSL. Una vez agregadas, el archivo package.json actualizado debería verse como el siguiente fragmento de código:

#package.json(updated)

{
 "name": "event-app-node-grpc",
 "version": "1.0.0",
 "description": "A CRUD application to demonstrate the use of gRPC with NodeJS",
 "main": "server/index.js",
 "scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },
 "author": "(author name here)",
 "license": "ISC",
 "dependencies": {
   "@grpc/proto-loader": "^0.6.12",
   "express": "^4.18.1",
   "@grpc/grpc-js": "^1.6.12
 }
}

El fragmento de código anterior muestra el archivo package.json actualizado después de agregar a la etiqueta scripts los comandos para configurar el entorno de ejecución de la aplicación y generar el certificado SSL.

Definir el Protocol Buffer

En este tutorial se muestra cómo usar gRPC en una aplicación sencilla de seguimiento de eventos. La aplicación de demostración recibe detalles de eventos y los guarda en una base de datos en memoria, y permite actualizar, consultar y eliminar los datos de los eventos.

En las aplicaciones gRPC, la interfaz del servicio y las cargas útiles necesarias se definen en un archivo protobuf para permitir la comunicación entre distintas aplicaciones. Los archivos protobuf tienen la extensión .proto, como se muestra en el esquema de configuración del proyecto.

Ahora, en el directorio raíz de la aplicación, crea un archivo events.proto y agrega el siguiente código. Puedes consultar como referencia el esquema de estructura del proyecto que definimos antes.

#events.proto

syntax = "proto3";

service EventService {
   rpc GetAllEvents (Empty) returns (EventList) {}
   rpc GetEvent (EventId) returns (Event) {}
   rpc CreateEvent (Event) returns (Event) {}
   rpc UpdateEvent (Event) returns (Event) {}
   rpc DeleteEvent (EventId) returns (Empty) {}
}

message Empty {}

message Event {
   string id = 1;
   string name = 2;
   string description = 3;
   string location = 4;
   string duration = 5;
   int32 lucky_number = 6;
   string status = 7;
}

message EventList {
   repeated Event events = 1;
}

message EventId {
   string id = 1;
}

En los fragmentos de código de definición de proto anteriores, primero especificamos la versión de Protocol Buffer con la definición syntax = "proto3" y, después, definimos el servicio de protocolo.

Luego, en la descripción del servicio de eventos del protocolo, creamos un servicio llamado EventService. Después, creamos funciones rpc dentro de este servicio, junto con los parámetros necesarios y los valores de retorno esperados. Puedes definir tantos servicios como necesite tu aplicación, pero, para simplificar, definimos solo uno.

También definimos los tipos de datos para la función rpc en la definición de EventService y los valores de retorno mediante el sistema exclusivo de numeración de campos de gRPC. Este sistema especifica la cantidad de bytes que se usan durante la codificación. Consulta más detalles en la documentación oficial de protobuf.

Crear el servidor gRPC

Siguiendo la estructura de carpetas anterior, crea una carpeta server en el directorio raíz de la aplicación y, dentro de ella, crea un archivo index.js. Pega el siguiente fragmento de código en el archivo server/index.js que acabas de crear:

#server/index.js

const PROTO_PATH = "./events.proto";

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

En el fragmento de código anterior, importamos el archivo events.proto que definimos antes y lo asignamos a la variable PROTO_PATH. Luego, lo cargamos con el método loadSync de la biblioteca protoLoader. Después, guardamos las definiciones de proto en la variable eventsProto, que las contiene todas.

A continuación, agrega el siguiente fragmento de código justo después de la variable eventsProto en el archivo server/index.js que definimos antes.

const { randomUUID } = require("node:crypto");

const events = [
   {
       id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7",
       name: "Birthday Party",
       description: "27th Birthday in Paris",
       location: "Paris France",
       duration: "All Day",
       lucky_number: 27,
       status: "Pending"
   },
];

const server = new grpc.Server();

En el fragmento de código anterior, requerimos el paquete node:crypto y su función randomUUID, que sirve para generar cadenas únicas aleatorias para los ID de nuestros eventos. Como en este tutorial usamos una base de datos en memoria, la definiremos como un arreglo para almacenar la lista de eventos. Luego, configuraremos la instancia del servidor con una nueva llamada al método grpc.Server.

A continuación, registraremos los servicios de la aplicación. Para ello, agrega el siguiente fragmento de código justo después de la variable server del fragmento anterior:

server.addService(eventsProto.EventService.service, {

   getAllEvents: (_, callback) => {
       callback(null, { events });
   },

   getEvent: (call, callback) => {
       let event = events.find(n => n.id == call.request.id);

       if (event) {
           callback(null, event);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   createEvent: (call, callback) => {
       let event = call.request;

       event.id = randomUUID();
       events.push(event);
       callback(null, event);
   },

   updateEvent: (call, callback) => {
       let existingEvent = events.find(n => n.id == call.request.id);

       if (existingEvent) {
           existingEvent.name = call.request.name;
           existingEvent.description = call.request.description;
           existingEvent.location = call.request.location;
           existingEvent.duration = call.request.duration;
           existingEvent.lucky_number = call.request.lucky_number;
           existingEvent.status = call.request.status;
           callback(null, existingEvent);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   deleteEvent: (call, callback) => {
       let existingEventIndex = events.findIndex(
           n => n.id == call.request.id
       );

       if (existingEventIndex != -1) {
           events.splice(existingEventIndex, 1);
           callback(null, {});
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   }
});

En el fragmento de código anterior, llamamos al método addService en la instancia del servidor gRPC para registrar los servicios de la aplicación. En esencia, se trata de operaciones para crear, leer y actualizar eventos.

Para iniciar el servidor de la aplicación, pega el siguiente fragmento de código justo después del método addService del fragmento anterior.

server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
console.log(`Server listening at http://127.0.0.1:${port}`);
server.start();
});

Crear el cliente gRPC

Siguiendo la estructura de carpetas anterior, crea una carpeta client en el directorio raíz de la aplicación y, dentro de ella, crea dos archivos: index.js y app.js. Pega el siguiente fragmento de código en el archivo client/app.js.

#client/app.js

const PROTO_PATH = "../events.proto";
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("127.0.0.1:50051", grpc.credentials.createInsecure());
module.exports = client;

En el fragmento de código anterior, importamos las definiciones de proto que creamos antes y las cargamos con protoLoader. Conectamos el cliente grpc a la dirección IP de la aplicación del servidor y exportamos el servicio de eventos con el nombre de variable client. También agregamos un certificado SSL al cliente para autorizar y cifrar las comunicaciones entre el cliente y el servidor.

Luego, pega el siguiente fragmento de código en el archivo client/index.js:

#client/index.js

const client = require("./app");

const express = require("express");
const app = express();
app.disable('x-powered-by');
app.use(express.json());
app.use(express.urlencoded());

app.get("/", (req, res) => {
   client.getAllEvents(null, (err, data) => {
       if (!err) {
           res.status(200).send({
               data
           });
       }
   });
});

app.post("/createEvent", (req, res) => {

   let newEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.insert(newEvent, (err, data) => {
       if (err) throw err;
       res.status(200).send({
           data,
           message: 'Event created successfully'
       });
   });
});

app.post("/updateEvent", (req, res) => {
   let updateEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.update(updateEvent, (err, data) => {
       if (err) throw err;

       res.status(200).send({
           data,
           message: 'Event updated successfully'
       });
   });
});

app.delete("/deleteEvent", (req, res) => {
   client.remove({ id: req.body.eventId }, (err, _) => {
       if (err) throw err;

       res.status(200).send({
           message: 'Event deleted successfully'
       });
   });
});

const PORT = process.env.PORT || 50050;
app.listen(PORT, () => {
   console.log("Client Server listening to port %d", PORT);
});

En el fragmento de código anterior, importamos event-service desde el archivo client/app.js. Después, configuramos un servidor Express con endpoints sencillos para gestionar la creation, la update, la fetch y la delete de eventos mediante llamadas remotas a la aplicación del servidor con gRPC.

Probar las aplicaciones del servidor y del cliente

En este punto, podemos probar lo que hicimos para asegurarnos de que todo vaya bien.

El servidor

Ve al directorio raíz del proyecto desde la terminal y ejecuta los siguientes comandos:

$bash
$ npm run start

La aplicación del servidor debería estar disponible en http://localhost:50051:

% npm run start
> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at https://127.0.0.1:50051

El cliente

Abre una nueva ventana de terminal, ve a la carpeta client desde el directorio raíz de la aplicación y ejecuta los siguientes comandos:

$bash
$ node index

La aplicación debería estar disponible en http://localhost:50050:

$ event-app-node-grpc  cd client
$ event-app-node-grpc/client node index.js

Client Server listening to port 50050

Para probarla, ve a localhost:50050 en tu navegador o usa una herramienta para probar APIs, como Postman. Deberías ver el evento predeterminado que agregamos inicialmente a nuestro arreglo de eventos. La respuesta debería ser igual a la de la siguiente captura de pantalla:

Interfaz de Postman que muestra una solicitud GET a localhost con una respuesta JSON que contiene detalles de un evento de cumpleaños.

Autenticar y proteger la API gRPC

El protocolo gRPC admite varios mecanismos de autenticación, por lo que se adapta fácilmente a sistemas nuevos y existentes. Podemos implementar la autenticación en las comunicaciones entre clientes y servidores gRPC con mecanismos recomendados, como SSL y TLS, con o sin autenticación basada en tokens de Google. También podemos crear una autenticación personalizada extendiendo la función de autenticación integrada en gRPC.

De forma predeterminada, gRPC incluye los siguientes mecanismos de autenticación:

  • SSL y TLS para autenticar el servidor y cifrar los datos intercambiados entre el cliente y el servidor

  • ALTS (un protocolo de autenticación y transporte mutuo diseñado por Google) para proteger las comunicaciones RPC de las aplicaciones que se ejecutan en Google Cloud Platform (GCP)

  • Un mecanismo genérico de autenticación basada en tokens para adjuntar credenciales basadas en metadatos a las solicitudes y respuestas

En este tutorial implementaremos la autenticación con SSL, como mencionamos en la introducción, y luego modificaremos los archivos client/app.js y server/index.js para incorporar este cambio.

Generar un certificado SSL con OpenSSL

Primero, generemos un certificado SSL con OpenSSL. Para este proceso, necesitarás tener OpenSSL instalado y permiso para ejecutar scripts de Bash. Estos requisitos son fundamentales para evitar errores de permisos.

En la estructura de carpetas, crea una carpeta scripts y, dentro de ella, un archivo llamado generate-certs.sh. Pega el siguiente fragmento de código en ese archivo:

#scripts/generate-certs.sh

echo "Creating certs folder ..."
mkdir certs && cd certs

echo "Generating certificates ..."

openssl genrsa -passout pass:1111 -des3 -out ca.key 4096

openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Test/CN=localhost"

openssl genrsa -passout pass:1111 -des3 -out server.key 4096

openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Server/CN=localhost"

openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

openssl rsa -passin pass:1111 -in server.key -out server.key

openssl genrsa -passout pass:1111 -des3 -out client.key 4096

openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Client/CN=localhost"

openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

openssl rsa -passin pass:1111 -in client.key -out client.key

El código anterior genera los certificados SSL necesarios para establecer una conexión segura y cifrada entre las aplicaciones del servidor y del cliente. Al ejecutarse, crea una carpeta certs, genera los certificados SSL del servidor y del cliente con OpenSSL y los guarda en la carpeta certs. Para obtener más información sobre estas configuraciones y su funcionamiento, visita el sitio web de OpenSSL.

Generar un certificado SSL para la aplicación con npm

Ahora, usa el script para generar un certificado SSL para tu aplicación. Ejecuta los siguientes comandos en la terminal, desde el directorio raíz de la aplicación.

$bash
$ npm run generate:certs

Esto crea una carpeta certs que contiene los certificados SSL generados.

Ten en cuenta que se requieren algunos privilegios de administrador. Si aparece un error de permisos al ejecutar el script, usa los siguientes comandos para otorgarle el permiso de ejecución y vuelve a intentarlo.

$bash
$ cd scripts
$ chmod u+r+x generate-certs.sh
$ ./generate-certs.sh

Deberías ver el siguiente resultado en la terminal:

> event-app-node-grpc@1.0.0 generate:certs
> ./scripts/generate-certs.sh

Creating certs folder…
Generating certificates…
Generating RSA private key, 4096 bit long modulus
………………………………………………..++
……………..++

e is 65537 (0x10001)
Generating RSA private key, 4096 bit long modulus
…………………++
………………………………………….….….….….….………..…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Server/CN=localhost
Getting CA Private Key
writing RSA key
Generating RSA private key, 4096 bit long modulus
………………………………………………..….….….…………..++
.…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Client/ON=localhost
Getting CA Private Key
writing RSA key

Actualizar los archivos client/app.js y server/index.js

Hasta ahora, generamos los certificados SSL necesarios para autenticar nuestras API gRPC. Ahora modificaremos los archivos client/index.js y server/index.js para que funcionen con estos certificados.

En el archivo client/app.js actualizado que se muestra a continuación, incorporamos el módulo fs para leer los certificados generados. Luego, usamos esos certificados para crear credenciales SSL de gRPC y, por último, las aplicamos al servicio gRPC.

#client/app.js(updated)

const PROTO_PATH = "../events.proto";
const fs = require('fs');
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const credentials = grpc.credentials.createSsl(
   fs.readFileSync('../certs/ca.crt'),
   fs.readFileSync('../certs/client.key'),
   fs.readFileSync('../certs/client.crt')
);

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("localhost:50051",credentials);
module.exports = client;

También incorporamos el módulo fs en el archivo server/index.js actualizado que se muestra a continuación para leer los certificados generados. Luego, usamos esos certificados para crear credenciales SSL de gRPC y las aplicamos al servidor.

#server/index.js(updated)

const PROTO_PATH = "./events.proto";
const fs = require('fs');

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

const server = new grpc.Server();

let credentials = grpc.ServerCredentials.createSsl(
   fs.readFileSync('./certs/ca.crt'), [{
   cert_chain: fs.readFileSync('./certs/server.crt'),
   private_key: fs.readFileSync('./certs/server.key')
}], true);

----------

----------

server.bindAsync("0.0.0.0:50051", credentials, (error, port) => {
console.log(`Server listening at http://0.0.0.0:${port}`);
server.start();
});

Ejecutar las aplicaciones del servidor y del cliente

Implementamos correctamente una solución de gestión de eventos con las especificaciones de gRPC. Para probar los endpoints, inicia la aplicación desde la terminal siguiendo los pasos que se indican a continuación.

El servidor

Ve al directorio raíz del proyecto desde la terminal y ejecuta los siguientes comandos:

$bash
$ npm run start

Después de ejecutarlos, la aplicación del servidor debería estar disponible en http://0.0.0.0:50051:

event-app-node-grpc % npm run start

> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at http://0.0.0.0:50051

El cliente

Abre una nueva ventana de la terminal, ve a la carpeta client desde el directorio raíz de la aplicación y ejecuta los siguientes comandos:

$bash
$ node index

Después de ejecutarlos, la aplicación del cliente debería estar disponible en http://localhost:50050:

> event-app-node-grpc % cd client
> client % node index
Client Server listening to port 50050

Para probar la aplicación, ve a localhost:50050 en tu navegador o usa una herramienta para probar API, como Postman. Deberías ver el evento predeterminado que agregamos inicialmente a nuestro arreglo de eventos. La respuesta debería coincidir con la siguiente captura de pantalla:

Postman muestra una solicitud GET a localhost con una respuesta JSON que enumera un evento de cumpleaños pendiente en París.

Puedes probar otros endpoints agregados a la aplicación para asegurarte de que todo funcione como esperas.

¡Creaste una API segura con gRPC!

En este tutorial, creamos una API sencilla con gRPC y Node.js, y explicamos cómo funciona y cuáles son sus numerosas ventajas, como HTTP/2 y SSL/TLS para la autenticación y el cifrado de extremo a extremo, que mejoran la seguridad de las API.

A pesar de estas ventajas, gRPC también tiene puntos débiles: compatibilidad limitada con navegadores, un formato de datos difícil de leer para las personas, una curva de aprendizaje pronunciada y poca compatibilidad con el almacenamiento en caché en el borde. Sin embargo, gracias a su rendimiento incomparable y su naturaleza multilingüe, gRPC es la mejor opción para la comunicación entre microservicios internos. El protocolo gRPC es impresionante y, desde su lanzamiento inicial en agosto de 2016, ha logrado una adopción considerable en la industria. Sin duda, seguirá creciendo.

Hay muchas otras cosas que puedes hacer con gRPC. El ejemplo de este tutorial es apenas la punta del iceberg de todo lo que ofrece. Consulta la documentación para ampliar tus conocimientos sobre gRPC y mejorar los procesos de comunicación de tu aplicación, así como las estrategias para mantener la seguridad de gRPC.

Más recursos sobre API: