Interactuar con servicios externos con los bindings de Dapr

Después de ver qué es posible integrarnos con servicios internos de nuestra propia aplicación a través de Dapr, es posible que te preguntes cómo puedo hacer lo mismo con servicios externos o de terceros. En este artículo te cuento qué son los bindings y cómo usarlos.

¿Qué son los bindings?

Otro de los bloques, llamado bindings, piensa en estos escenarios y alguno más, donde podemos interactuar tanto con información de entrada como de salida sin que nuestra aplicación sepa cómo se gestiona la comunicación. Existe un listado, cada vez más extenso, con los bindings disponibles a día de hoy y en que versión se encuentran: Alpha, Beta y Stable.

Hasta ahora no hemos tenido que configurar ningún componente, ya que para el primer artículo nos aprovechamos del state store que viene por defecto, y nos comunicaba con el Redis que el mismo Dapr generaba durante dapr init, y para el bloque service-to-Service comunication no hace falta configurar ninguno. Hoy sin embargo voy a enseñarte varias configuraciones y te mostraré como se hace para que el proceso de Dapr del proyecto las tenga en cuenta.

Bindings de entrada

Si bien es cierto que hay varios bindings que son de entrada y salida, vamos a separar estos dos apartados para mostrarte algunos ejemplos en una de las direcciones y que veas cómo se configuran.

En el caso de los de entrada vamos a ver varios:

Cron

Como te puedes imaginar, este binding lo que nos permite es ejecutar una llamada cada tiempo determinado, basándonos en una expresión de cron. La configuración del mismo sería como la siguiente:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: scheduled
spec:
  type: bindings.cron
  version: v1
  metadata:
  - name: schedule
    value: "30 * * * * *" # valid cron schedule

El código que invocará esta configuración será aquel que tenga como path /scheduled, el cual coincide con el nombre que le hemos dado al componente:

    [HttpPost("/scheduled")]
    public ActionResult scheduled()
    {
        _logger.LogInformation($"Scheduled fired at {DateTime.Now}");
        return Ok(); //ACK-ing an event
    }

Azure Storage queque

Una integración interesante puede ser con una cola de mensajería, donde recibimos mensajes directamente a un método de nuestro código donde ya solo tenemos que procesarlos pero no preocuparnos de saber cómo se recuperan, cada cuánto tiempo, eliminarlos, etcétera.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: azqueue
spec:
  type: bindings.azure.storagequeues
  version: v1
  metadata:
    - name: accountName
      value: "<AZ_STORAGE_NAME>"
    - name: accountKey
      value: "<AZ_STORAGE_ACCOUNT_KEY>"
    - name: queueName
      value: "<QUEUE_NAME>"
    - name: decodeBase64
      value: "true"

El código al cual llamará esta configuración cuando llegue algún mensaje a la cola será este:

    [HttpPost("/azqueue")]
    public ActionResult azqueue([FromBody] object message)
    {
        _logger.LogInformation($"New queue message:");
        _logger.LogInformation(message.ToString());
        return Ok(); //ACK-ing an event
    }

Twitter

Hasta hace bien poco una de las demo estrella con Dapr era usar este binding con uuna configuración como la siguiente:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: tweets
spec:
  type: bindings.twitter
  version: v1
  metadata:
  - name: consumerKey
    value: "<YOUR_CONSUMER_KEY>"
  - name: consumerSecret
    value: "<YOUR_CONSUMER_SECRET>"
  - name: accessToken
    value: "<YOUR_ACCESS_TOKEN>"
  - name: accessSecret
    value: "<YOUR_ACCESS_SECRET>"
  - name: query
    value: "batman"

Con su correspondiente método:

    [HttpPost("/tweets")]
    public ActionResult tweets([FromBody] object body)
    {
        _logger.LogInformation(body.ToString());
        return Ok(); //ACK-ing an event
    }

Sin embargo, actualmente este binding no está funcionando debido al cambio de versión de la API de Twitter, como bien investigó mi amigo Carlos Mendible, que además abrió un Issue en el repositorio de GitHub de los componentes para trabajar en ka compatibilidad con la versión 2.

Los importante de estos bindings de entrada es que veas que la dinámica es siempre la misma: archivo con la configuración y un método que utilice como path el nombre que le hemos dado al componente. Por último, si todo ha ido bien es necesario devolver un 200, return Ok(), para que Dapr entienda que la llamada ha sido satisfactoria.

Bindings de salida

Por otro lado, los bindings de salida se suelen invocar para notificar el resultado de una operación.

Http

Este nos permite invocar una llamada http. Para este ejemplo he utilizado la siguiente configuración:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: http-output
spec:
  type: bindings.http
  version: v1
  metadata:
  - name: url
    value: https://eokyzit656jjefn.m.pipedream.net

Sin emargo, al ser un binding de salida no esperamos que nada lo ejecute, sino que somos nosotros quienes lo ejecutamos. Por ello he creado un método como el siguiente, en un nuevon controlador llamado OutputControllers:

using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
namespace dapr_bindings_dotnet.Controllers;
[ApiController]
[Route("[controller]")]
public class OutputsController : ControllerBase
{
    private readonly ILogger<OutputsController> _logger;
    public OutputsController(ILogger<OutputsController> logger)
    {
        _logger = logger;
    }
    [HttpPost("/invoke-http-output")]
    public async Task<ActionResult> InvokeHttpOutput([FromBody] object body)
    {
        _logger.LogInformation(body.ToString());
        //Using Dapr SDK to invoke output binding
        var bindingName = "http-output";
        var bindingOperation = "post";
        using var _daprClient = new DaprClientBuilder().Build();
        await _daprClient.InvokeBindingAsync(bindingName, bindingOperation, body);
        return Ok();
    }
}

En el caso de los bindings de salida creamos un DaprClient y utilizamos el método InvokeBindingAsync, el cual recibe el nombre que le hemos dado al binding, el tipo de operación (pueden soportar varias), y el cuerpo del mensaje.

Cómo ejecutar Dapr con estos componentes

Hasta ahora, al no indicarle a Dapr lo contrario los componentes que se cargaban por defecto eran los generados en la carpeta ~/.dapr/components (o %UserProfile%\.dapr\components en Windows). Sin embargo, es habitual que estas configuraciones sean propias de los proyectos y es por ello que durante el arranque haya que decirle a Dapr cuál es la carpeta que tiene que inspeccionar para cargar los componentes de los que hace uso la aplicación.

Si estas usando Vs Code puedes modificar la tarea de dapr-debug de la siguiente manera:

        {
            //https://docs.dapr.io/developing-applications/ides/vscode/vscode-how-to-debug-multiple-dapr-apps/#daprd-parameter-table
            "appId": "dapr-bindings-dotnet",
            "appPort": 5444,
            "label": "dapr-debug",
            "type": "dapr",
            "dependsOn": "build",
            "componentsPath": "./components",
            "logLevel": "debug",
            "httpPort": 3500,
            "grpcPort": 5005
        },

Como ves, hay una opción que es componentsPath a la que puedes decir la ruta de la carpeta que quieres que cargue.

Del mismo modo puedes especificar este parámetro,–components-path, como parte del comando si es que lo ejecutas de manera manual:

dapr run --app-id "dapr-bindings-dotnet" --app-port "5444" --components-path "./components" --dapr-grpc-port "5005" --dapr-http-port "3500" --log-level "debug"

La forma de saber si se han cargado correctamente es fundamentalmente finándonos en los logs.

Por otro lado yo suelo fijarme en la extensión de Dapr de Visual Studio Code donde muestra todos los componentes cargados para una aplicación.

Extension de VS Code – Componentes cargados para la aplicación

El código con los ejemplos lo tienes en 0GiS0/dapr-bindings-dotnet.

¡Saludos!