Blog de Amazon Web Services (AWS)

Picturesocial – Cómo usar DynamoDB en un API contenerizada

Por Jose Yapur – Sr. Developer Advocate, AWS

 

En el último artículo de Picturesocial aprendimos cómo usar un enfoque seguro para consumir servicios de AWS en las API que viven en Kubernetes, ahora es tiempo de añadir la base de datos para almacenar todos los objetos que usaremos en nuestra red social. Para ello elegimos Amazon DynamoDB debido a que es una base de datos moderna, que soporta alta transaccionalidad y simplifica el manejo de la información, pero además porque estamos usando estructuras de datos documentales que trabajan mejor en una base de datos no relacional que trabaja bajo llave-valor y documental como DynamoDB.

Les voy a ser sincero, cuando comencé a planificar Picturesocial había decidido usar DynamoDB pero no sabía muy bien cómo funcionaba ni cómo implementarlo y me costó algunos días poder llegar a ese momento donde dices “ahhhh, así era”, espero que este artículo haga que puedan decir lo mismo en mucho menos tiempo ❤️

Lo primero que haremos es descubrir qué es lo que nos gustaría almacenar en las API que hemos desarrollado hasta ahora, en un artículo anterior sobre Cómo analizar imágenes usando Inteligencia Artificial, creamos un API que devolvía las etiquetas de una foto usando Amazon Rekognition, si usamos esta API como base podremos identificar los atributos más importantes para nosotros, los cuales son: 1/ un Id para correlacionar la imagen con las etiquetas, 2/ el nombre de la imagen y su ubicación, 3/ El top 5 de las etiquetas detectadas por Amazon Rekognition, 4/ El usuario que compartió la imagen. Con esta información podemos convertir los atributos en un Modelo de Datos usando JSON. Este modelo de datos es el que usaremos para nuestra API Pictures y se debe ver de la siguiente forma:

{  
    "id": "string",  
    "imageName": "string",  
    "imageTags": ["string"],  
    "user": "string"
}

De todos esos campos, uno de ellos estará dedicado a ser la llave para obtener todo el objeto, en nuestro caso será el id, el cual generaremos de forma aleatoria desde el código, esa llave en DynamoDB se llama Primary Key. En nuestro caso no será una llave para ordenar la información (Sort Key) sino únicamente para separarla, así que esta Primary Key será del tipo Partition Key. Si quieres aprender más sobre este tema puedes ir a este link.

Cuando extrapolamos el modelo de datos a la realidad, hablamos de un Objeto de Datos como es el siguiente ejemplo, el conjunto de objetos de datos en DynamoDB se almacena en una Tabla.

{  
    "id": "c37ae810-4b6e-4da7-8b5d-1f720448f9a4",  
    "imageName": "picturesocialbucket/cat.jpg",  
    "imageTags": [    
        "Cat",    
        "Electronics",    
        "LCD Screen",    
        "Laptop",    
        "Pc"  ],  
    "user": "demo03"
 }

Ahora que tenemos nuestro Modelo de Datos, vamos a navegar las especificaciones de un CRUD API para que nuestros Picture API siga las mejores prácticas. CRUD viene de Crear (Create), Leer (Read), Actualizar (Update) y Eliminar (Delete), esas son las operaciones básicas que solemos hacer sobre un Objeto de Datos y deben siempre estar alineadas a métodos HTTP de la siguiente forma:

  • Crear → POST
  • Leer → GET
  • Actualizar → PUT
  • Eliminar → DELETE

No hay nada peor que un API mal documentada, métodos que no se alinean a las acciones o gente que usa espacios en vez de Tab para indentar su código (👀) Así, por más que no sepamos mucho sobre los métodos del API, podemos seguir los métodos HTTP y entender el Modelo de Datos. Puedes aprender más sobre los diferentes métodos HTTP y cómo se alinean a las API visitando la siguiente documentación.

Y ahora, con la antesala suficiente, ¡Vamos al código!

Pre-requisitos

  • Si esta es tu primera vez trabajando con AWS CLI o necesitas un refresh de como setear tus credenciales en el terminal te sugiero seguir este paso a paso: https://thinkwithwp.com/es/getting-started/guides/setup-environment/. Si no quieres instalar todo desde cero, en este mismo link podrás seguir los pasos para configurar Amazon Cloud9 que es un entorno de desarrollo virtual, que incluye casi todo el toolset que necesitas para este paso a .

Paso a Paso

  • Lo primero que haremos será crear la Tabla de DynamoDB necesaria para nuestra API, esta se llamará Pictures. Usaré el terminal con AWS CLI 2 instalado y con un perfil con permisos. Y ejecutamos el siguiente comando.
aws dynamodb create-table \
--table-name pictures \
--attribute-definitions \
AttributeName=id,AttributeType=S \
--key-schema \
AttributeName=id,KeyType=HASH \
--provisioned-throughput \
ReadCapacityUnits=5,WriteCapacityUnits=5 \
--table-class STANDARD
  • Cuando ejecutemos el comando obtendremos de respuesta un JSON con detalles de la tabla que acabamos de crear. Para salir de esa vista necesitamos escribir :q y presionar Enter.
{
    "TableDescription": {
    "AttributeDefinitions": [
    {
        "AttributeName": "id",
        "AttributeType": "S"
        }
        ],
        "TableName": "pictures",
        "KeySchema": [
        {
            "AttributeName": "id",
            "KeyType": "HASH"
        }
        ],
        "TableStatus": "CREATING",
        "CreationDateTime": "2022-08-30T11:53:01.937000-05:00",
        "ProvisionedThroughput": 
        {
            "NumberOfDecreasesToday": 0,
            "ReadCapacityUnits": 5,
            "WriteCapacityUnits": 5
        },
        "TableSizeBytes": 0,
        "ItemCount": 0,
        "TableArn": "arn:aws:dynamodb:us-east-1:378907872096:table/pictures",
        "TableId": "d95f5cf0-d2c1-48b8-b02c-4bdcb243b179",
        "TableClassSummary": 
        {
            "TableClass": "STANDARD"
        }
    }
}
  • Ahora la tabla está lista para el código. Pero primero, vamos a clonar el repositorio eligiendo la branch “Ep7”

git clone https://github.com/aws-samples/picture-social-sample.git -b ep7

  • Con el repositorio clonado, vamos a añadir los paquetes de AWS a nuestro proyecto posicionándonos en el directorio Pictures y ejecutando los siguientes comandos.

dotnet add package AWSSDK.SecurityToken
dotnet add package AWSSDK.Core
dotnet add package AWSSDK.DynamoDBv2
dotnet add package AWSSDK.Rekognition

  • Abrimos el repositorio clonado en VS Code y exploramos el archivo PictureTable.cs, que contiene las clases PictureTable y PictureTableRequest. La primera define el modelo de datos y la segunda el payload para crear nuevos elementos.
using Amazon.DynamoDBv2.DataModel;
[DynamoDBTable("pictures")]
public class PictureTable
{
    [DynamoDBHashKey]
    public string id { get; set; } = default!;
    [DynamoDBProperty("imagename")]
    public string ImageName { get; set; } = default!;
    [DynamoDBProperty("imagetags")]
    public string[] ImageTags { get; set; } = default!;
    [DynamoDBProperty("user")]
    public string User { get; set; } = default!;
}
  • Añadimos la definición de DynamoDBTable y el nombre de la tabla, arriba de la clase PictureTable, usando [DynamoDBTable(“pictures«)].Además, definimos qué atributo de la clase es la Llave por medio de la cabecera [DynamoDBHashKey]. Para los otros atributos solo establecemos el nombre como aparecerá en el documento JSON, de la siguiente forma: [DynamoDBProperty(«nameOfTheAttribute«)]
  • La segunda clase es más simple debido a que es solo el modelo de datos del payload, donde definimos la foto, el bucket y el nombre de usuario que usaremos para crear un nuevo elemento.
public class PictureTableRequest
{
    public string photo { get; set; }= default!;
    public string bucket { get; set; }= default!;
    public string user { get; set; }= default!;
}
  • Abrimos el Controlador Picture dentro de Controllers/PictureController.cs, ahi tendremos 3 métodos: Create, Read y Delete, usando los métodos HTTP que les corresponden.
  • Lo primero que haremos será declarar las variables de DynamoDB y el modelo de datos. Usando el constructor las inicializaremos, ten en cuenta que tomaremos el perfil activo en AWS CLI para consultar a la tabla.
  • Hemos omitido la cabecera de Authorize en el controlador debido a que en el siguiente episodio manejaremos la autenticación desde Amazon API Gateway.
public class PictureController : ControllerBase
{
    AmazonDynamoDBConfig clientConfig;
    AmazonDynamoDBClient client;
    DynamoDBContext context;
    PictureTable model;

    public PictureController()
    {
        this.clientConfig = new AmazonDynamoDBConfig();
        this.client = new AmazonDynamoDBClient(clientConfig);
        this.context = new DynamoDBContext(client);
        this.model = new PictureTable();
    }
  • Hemos explorado Amazon Rekognition en nuestro artículo anterior, por lo que ahora nos enfocaremos en los cambios necesarios para almacenar la información del análisis en DynamoDB.
  • Para ello declaramos el método Create como HttpPost y parseamos el request usando PictureTableRequest, la clase que exploramos al inicio.
  • El siguiente paso es crear un objeto del tipo PictureTable y asignárselo a nuestra variable global model. Este objeto tendrá la información que devolvió Amazon Rekognition como respuesta y finalmente guardar el modelo de forma asíncrona en DynamoDB usando SaveAsync(model).Como puedes ver, DynamoDB no agrega más que algún par de líneas custom a tu código.
[HttpPost]
public async Task<PictureTable> Create([FromBody]PictureTableRequest req)
{
    var rekognitionClient = new AmazonRekognitionClient(Amazon.RegionEndpoint.USEast1);
    var responseList = new List<String>();

    DetectLabelsRequest detectlabelsRequest = new DetectLabelsRequest()
    {
        Image = new Image()
        {
            S3Object = new S3Object()
            {
                Name = req.photo,
                Bucket = req.bucket
            },
        },
        MaxLabels = 5,
        MinConfidence = 80F
    };
    try
    {
        var detectLabelsResponse = await rekognitionClient.DetectLabelsAsync(detectlabelsRequest);
        foreach (Label label in detectLabelsResponse.Labels)
            responseList.Add(label.Name);
        if (responseList.Count() > 0)
        {
            var guid = Guid.NewGuid().ToString();
            model = new PictureTable{
                id = guid,
                ImageName = $"{req.bucket}/{req.photo}",
                ImageTags = responseList.ToArray(),
                User = req.user
            };
            await context.SaveAsync(model);
        }
        return model;
    }
    catch(Exception)
    {
        return model;
        throw;
    }    
}
  • Para el método Read usaremos pasos similares a los anteriores, pero usando HttpGet, donde usaremos la llave de la tabla para obtener los valores. En este caso estamos recibiendo por la URL el id y retornando el objeto de datos correspondiente por medio de LoadAsync<PictureTable>(id)
[HttpGet("{id}")]
public async Task<PictureTable> Read(string id)
{
    model = await context.LoadAsync<PictureTable>(id);
    return model;
}
  • Finalmente, el método Delete usando HTTP Delete, donde también usaremos el método LoadAsync para obtener el objeto de datos y finalmente el método DeleteAsync para eliminar el objeto de la base de datos.
[HttpDelete("{id}")]
public async Task<bool> Delete(string id)
{
    model = await context.LoadAsync<PictureTable>(id);
    if(model != null)
    {
        await context.DeleteAsync<PictureTable>(model);
        return true;
    }
    return false;
}
  • La API final debería verse así:
using Microsoft.AspNetCore.Mvc;
using Amazon.Rekognition;
using Amazon.Rekognition.Model;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

namespace pictures.Controllers;

[ApiController]
[Route("api/[controller]")]
public class PictureController : ControllerBase
{
    AmazonDynamoDBConfig clientConfig;
    AmazonDynamoDBClient client;
    DynamoDBContext context;
    PictureTable model;

    public PictureController()
    {
        this.clientConfig = new AmazonDynamoDBConfig();
        this.client = new AmazonDynamoDBClient(clientConfig);
        this.context = new DynamoDBContext(client);
        this.model = new PictureTable();
    }
    [HttpPost]
    public async Task<PictureTable> Create([FromBody]PictureTableRequest req)
    {
        var rekognitionClient = new AmazonRekognitionClient(Amazon.RegionEndpoint.USEast1);
        var responseList = new List<String>();

        DetectLabelsRequest detectlabelsRequest = new DetectLabelsRequest()
        {
            Image = new Image()
            {
                S3Object = new S3Object()
                {
                    Name = req.photo,
                    Bucket = req.bucket
                },
            },

        };
        try
        {
            var detectLabelsResponse = await rekognitionClient.DetectLabelsAsync(detectlabelsRequest);
            foreach (Label label in detectLabelsResponse.Labels)
                responseList.Add(label.Name);
            if (responseList.Count() > 0)
            {
                var guid = Guid.NewGuid().ToString();
                model = new PictureTable{
                    id = guid,
                    ImageName = $"{req.bucket}/{req.photo}",
                    ImageTags = responseList.ToArray(),
                    User = req.user
                };
                await context.SaveAsync(model);
            }
            return model;
        }
        catch(Exception)
        {
            return model;
            throw;
        }    
    }
    [HttpGet("{id}")]
    public async Task<PictureTable> Read(string id)
    {
        model = await context.LoadAsync<PictureTable>(id);
        return model;
    }

    [HttpDelete("{id}")]
    public async Task<bool> Delete(string id)
    {
        model = await context.LoadAsync<PictureTable>(id);
        if(model != null)
        {
            await context.DeleteAsync<PictureTable>(model);
            return true;
        }
        return false;
    }
}
  • Probemos el API ejecutando el siguiente comando dentro del directorio Pictures

dotnet run

  • Vamos a hacer uso de Swagger, que viene por defecto en las plantillas de Web API en .NET, ingresando a la siguiente ruta desde el navegador:

http://localhost:5075/swagger/index.html

  • ¡Podremos ver que el API se documentó automáticamente, así como los modelos de datos que utiliza!

  • Crearé una nueva Imagen por medio del método POST y creando un payload usando el modelo de PictureTableRequest, en mi caso usaré el nombre de una imagen y bucket que ya existe para hacer la prueba.
{
    "photo": "cat.jpg",
    "bucket": "picturesocialbucket",
    "user": "demo"
}
  • El resultado será el similar al siguiente:

  • Para las siguientes acciones podemos copiar el id del objeto y probarlo en el método Read.

  • Como puedes ver, obtenemos exactamente la misma información que obtuvimos en el método Crear, con la diferencia de que esta vez estamos obteniendo el objeto directamente de la Base de datos y no evaluando la imagen en Amazon Rekognition. Esto mejora significativamente el tiempo de respuesta de lectura vs escritura.
  • Ahora, solo nos queda actualizar la Politica de Amazon Identity and Access Management (IAM) que usa nuestro Amazon Elastic Kubernetes Service para que tambien soporte DynamoDB. Si no sabes como hacerlo, te sugiero revisar nuestro artículo sobre Cómo delegar el acceso a servicios de AWS desde Kubernetes. La nueva política permitirá que los Pods del API Pictures puedan acceder a DynamoDB además de Rekognition y S3.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1661888238004",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::picturesocialbucket"
    },
    {
      "Sid": "Stmt1661888728691",
      "Action": [
        "dynamodb:DeleteItem",
        "dynamodb:GetItem",
        "dynamodb:PartiQLDelete",
        "dynamodb:PartiQLInsert",
        "dynamodb:PartiQLSelect",
        "dynamodb:PartiQLUpdate",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:dynamodb:us-east-1:111122223333:table/pictures"
    },
    {
      "Sid": "Stmt1661888876887",
      "Action": [
        "rekognition:DetectLabels"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

Este es el final de este artículo, estamos teniendo un buen progreso en nuestro camino de creación de Picturesocial. Espero que hayas disfrutado este post y no dejes de seguir nuestra Picturesocial List para encontrar todo el contenido de esta serie. En el siguiente artículo aprenderemos sobre cómo exponer APIs a internet usando API Gateway y VPC Link.

¡Nos Leemos!


Sobre el autor

José Yapur es Senior Developer Advocate en AWS con experiencia en Arquitectura de Software y pasión por el desarrollo especialmente en .NET y PHP. Trabajó como Arquitecto de Soluciones por varios años, ayudando a empresas y personas en LATAM.