Hace unas semanas se lanzaba la versión 1.10 de Dapr (16 de febrero de 2023) e incluía una novedad muy interesante, pluggable components.

Pluggable components

Dapr incluye una gran colección de componentes que podemos utilizar en nuestras aplicaciones, en el caso de que necesitaramos crear un componente privado seria algo complejo ya que deberiamos hacer un fork de los repositorios dapr y components-contrib y utilizando Go podríamos desarrollar nuestro componente a medida.

Dapr 1.10 incluye la posibilidad de crear e integrar nuestros propios componentes sin necesidad de modificar el código de Dapr a tráves de pluggable components (preview). Un componente pluggable es aquel que no está incluido en el runtime de Dapr.

La comunicación entre el componente y Dapr se realiza a tráves de gRPC y Unix Domain Sockets por lo que podemos utilizar cualquier lenguaje de programación que soporte gRPC para desarrollar nuestro componente.

Arquitectura registro y comunicación Dapr y un componente pluggable

A día de hoy podemos implementar 3 tipos de componentes. Dependiendo del tipo de componente, tendremos que implementar diferentes métodos:

Desarrollo

Para desarrollar el componente vamos a usar .NET 7 ya que soporta gRPC. Dapr ofrece una plantilla para .NET en el siguiente repositorio https://github.com/dapr/samples/tree/master/pluggable-components-dotnet-template.

El tipo de componente que vamos a desarrollar será de tipo Binding (output) por lo que la interfaz a implementar será la siguiente:

  • 1 método para la inicialización del componente initialization
  • 1 método para indicar que el componente está funcionando correctamente (liveness check -Ping-)
  • 1 método para ejecutar la lógica del componente
  • 1 método para indicar la lista de acciones disponibles en el componente

Una vez actualizada la plantilla a .NET 7 y actualizadas las referencias del proyecto:

  1. Registramos el gRPC service en la clase Program, en nuestro caso OutputBindingService
  2. En la clase Service implementamos nuestra clase OutputBindingService, al tratarse de un Outbinding debemos implementar los métodos:

Implementación - Init

    public override Task<OutputBindingInitResponse> Init(OutputBindingInitRequest request, ServerCallContext context)
    {
        logger.LogTrace("Init");

        LogMetadata(request.Metadata.Properties);

        var result = new OutputBindingInitResponse();

        return Task.FromResult(result);
    }

Implementación - Invoke

    /// <summary>
    /// Invoke remote systems with optional payloads.
    /// </summary>
    /// <param name="request">The request received from the client.</param>
    /// <param name="context">The context of the server-side call handler being invoked.</param>
    /// <returns>The response to send back to the client (wrapped by a task).</returns>
    public override Task<InvokeResponse> Invoke(InvokeRequest request, ServerCallContext context)
    {
        logger.LogTrace("Invoke");


        LogMetadata(request.Metadata);
        logger.LogTrace(request.Data.ToString());

        var result = new InvokeResponse()
        {
            ContentType = "application/json",
            Data = request.Data,
        };

        return Task.FromResult(result);
    }

Implementación - List operations

    /// ListOperations list system supported operations.
    /// </summary>
    /// <param name="request">The request received from the client.</param>
    /// <param name="context">The context of the server-side call handler being invoked.</param>
    /// <returns>The response to send back to the client (wrapped by a task).</returns>
    public override Task<ListOperationsResponse> ListOperations(ListOperationsRequest request, ServerCallContext context)
    {
        logger.LogTrace("List");

        var operations = new RepeatedField<string> { "read", "write" };
        var result = new ListOperationsResponse();
        result.Operations.AddRange(operations);

        return Task.FromResult(result);

    }

Implementación - Ping

    /// <summary>
    /// Ping the OutputBinding. Used for liveness porpuses.
    /// </summary>
    /// <param name="request">The request received from the client.</param>
    /// <param name="context">The context of the server-side call handler being invoked.</param>
    /// <returns>The response to send back to the client (wrapped by a task).</returns>
    public override Task<PingResponse> Ping(PingRequest request, ServerCallContext context)
    {
        logger.LogTrace("Ping");
        return Task.FromResult(new PingResponse());
    }

Compilamos y generamos la imagen Docker correspondiente, es importante que cuando generemos la imagen añadamos un usuario non-root:

RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

Despliegue del componente

Desplegar el nuestro componente pluggable es muy sencillo, tras generar la imagen con nuestra aplicación tan solo debemos generar un deployment y añadir las siguientes anotaciones a nivel de pod para indicarle a Dapr que registre nuestro componente:

dapr.io/pluggable-components: "dapr-pluggable-component"
dapr.io/app-id: "my-app"
dapr.io/enabled: "true"

Automaticamente Dapr Sidecar Injector creará otro contenedor para ejecutar el servicio de Dapr (Sidecar pattern) y modificará el deployment para compartir la carpeta /tmp/dapr-components-sockets entre los dos contenedores a traves de un volumeMounts.

Volume mounts entre containers

Comprobamos el correcto funcionamiento del componente ejecutando directamente las peticiones al contenedor Dapr de nuestro deployment. Para ello:

  1. Utilizamos port forward para poder conectarnos al contenedor Dapr de nuestro deployment:
kubectl port-forward deploy/mydapr-component  3500:3500
  1. Ejecutamos las peticiones a nuestro pluggable component:
curl -X POST -H 'Content-Type: application/json' http://localhost:3500/v1.0/bindings/prod-mystore -d '{ "data": 100, "operation": "write" }'
curl -X POST -H 'Content-Type: application/json' http://localhost:3500/v1.0/bindings/prod-mystore -d '{ "data": 200, "operation": "read" }'

Dapr pluggable component resultado ejecución

Creo que esta funcionalidad facilita en gran medida el poder desarrollar e integrar componentes privados, actualmente el único incoveniente es que es necesario desplegar el componente varias veces en el caso de que haya varias aplicaciones que lo utilicen.

References