Fotos diarias: Lab 2: Crea miniaturas de imágenes

1. Descripción general

En este codelab, compilarás sobre el lab anterior y agregarás un servicio de miniaturas. El servicio de miniaturas es un contenedor web que toma fotos grandes y crea miniaturas a partir de ellas.

Cuando la foto se sube a Cloud Storage, se envía una notificación a través de Cloud Pub/Sub a un contenedor web de Cloud Run, que, luego, cambia el tamaño de las imágenes y las guarda en otro bucket de Cloud Storage.

31fa4f8a294d90df.png

Qué aprenderás

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Configuración y requisitos

Configuración del entorno de autoaprendizaje

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una string de caracteres que no se utiliza en las API de Google y se puede actualizar en cualquier momento.
  • El ID del proyecto debe ser único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). Cloud Console genera automáticamente una string única, que, por lo general, no importa cuál sea. En la mayoría de los codelabs, debes hacer referencia al ID del proyecto (suele ser PROJECT_ID). Por lo tanto, si no te gusta, genera otro aleatorio o prueba con uno propio y comprueba si está disponible. Después de crear el proyecto, este ID se “congela” y no se puede cambiar.
  • Además, hay un tercer valor, el Número de proyecto, que usan algunas API. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en Cloud Console para usar las API o los recursos de Cloud. Ejecutar este codelab no debería costar mucho, tal vez nada. Si quieres cerrar los recursos para no se te facture más allá de este instructivo, sigue las instrucciones de “limpieza” que se encuentran al final del codelab. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de USD 300.

Inicia Cloud Shell

Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

En GCP Console, haga clic en el ícono de Cloud Shell en la barra de herramientas superior derecha:

bce75f34b2c53987.png

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

f6ef2b5f13479f3a.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Puedes realizar todo tu trabajo en este lab usando simplemente un navegador.

3. Habilita las APIs

En este lab, necesitarás Cloud Build para compilar imágenes de contenedor y Cloud Run para implementar el contenedor.

Habilita ambas APIs desde Cloud Shell:

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

Deberías ver que la operación finaliza correctamente:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. Crear otro bucket

Almacenarás las miniaturas de las imágenes subidas en otro bucket. Usemos gsutil para crear el segundo bucket.

Dentro de Cloud Shell, establece una variable para el nombre único del bucket. Cloud Shell ya tiene establecido GOOGLE_CLOUD_PROJECT en el ID único de tu proyecto. Puedes agregarlo al nombre del bucket. Luego, crea un bucket multirregional público en Europa con acceso uniforme:

BUCKET_THUMBNAILS=thumbnails-$GOOGLE_CLOUD_PROJECT
gsutil mb -l EU gs://$BUCKET_THUMBNAILS
gsutil uniformbucketlevelaccess set on gs://$BUCKET_THUMBNAILS
gsutil iam ch allUsers:objectViewer gs://$BUCKET_THUMBNAILS

Al final, deberías tener un nuevo bucket público:

8e75c8099938e972.png

5. Clona el código

Clona el código y ve al directorio que contiene el servicio:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
cd serverless-photosharing-workshop/services/thumbnails/nodejs

Tendrás el siguiente diseño de archivo para el servicio:

services
 |
 ├── thumbnails
      |
      ├── nodejs
           |
           ├── Dockerfile
           ├── index.js
           ├── package.json

Dentro de la carpeta thumbnails/nodejs, tienes 3 archivos:

  • index.js contiene el código de Node.js.
  • package.json define las dependencias de la biblioteca.
  • Dockerfile define la imagen de contenedor.

6. Cómo explorar el código

Para explorar el código, puedes usar el editor de texto integrado. Para ello, haz clic en el botón Open Editor en la parte superior de la ventana de Cloud Shell:

3d145fe299dd8b3e.png

También puedes abrir el editor en una ventana dedicada del navegador para tener más espacio en pantalla.

Dependencias

El archivo package.json define las dependencias de biblioteca necesarias:

{
  "name": "thumbnail_service",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "bluebird": "^3.7.2",
    "express": "^4.17.1",
    "imagemagick": "^0.1.3",
    "@google-cloud/firestore": "^4.9.9",
    "@google-cloud/storage": "^5.8.3"
  }
}

La biblioteca de Cloud Storage se usa para leer y guardar archivos de imagen en Cloud Storage. Firestore para actualizar los metadatos de las imágenes Express es un framework web de JavaScript / Node. El módulo body-parser se usa para analizar fácilmente las solicitudes entrantes. Bluebird se usa para administrar promesas, y Imagemagick es una biblioteca para manipular imágenes.

Dockerfile

Dockerfile define la imagen de contenedor para la aplicación:

FROM node:14-slim

# installing Imagemagick
RUN set -ex; \
  apt-get -y update; \
  apt-get -y install imagemagick; \
  rm -rf /var/lib/apt/lists/*; \
  mkdir /tmp/original; \
  mkdir /tmp/thumbnail;

WORKDIR /picadaily/services/thumbnails
COPY package*.json ./
RUN npm install --production
COPY . .
CMD [ "npm", "start" ]

La imagen base es el Nodo 14 y la biblioteca de imagemagick se usa para manipular la imagen. Algunos directorios temporales se crean para almacenar archivos de imágenes originales y en miniatura. Luego, se instalan los módulos de NPM que necesita nuestro código antes de iniciar el código con npm start.

index.js

Exploremos el código por partes para que podamos comprender mejor lo que hace este programa.

const express = require('express');
const imageMagick = require('imagemagick');
const Promise = require("bluebird");
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');

const app = express();
app.use(express.json());

Primero, requerimos las dependencias necesarias y creamos nuestra aplicación web Express, además de indicar que queremos usar el analizador de cuerpo JSON, ya que las solicitudes entrantes son, en realidad, cargas útiles JSON enviadas a través de una solicitud POST a nuestra aplicación.

app.post('/', async (req, res) => {
    try {
        // ...
    } catch (err) {
        console.log(`Error: creating the thumbnail: ${err}`);
        console.error(err);
        res.status(500).send(err);
    }
});

Recibimos esas cargas útiles entrantes en la URL base / y unimos nuestro código con un poco de manejo de lógica de errores para tener mejor información de por qué algo puede estar fallando en nuestro código. Para ello, analizamos los registros que serán visibles desde la interfaz de Stackdriver Logging en la consola web de Google Cloud.

const pubSubMessage = req.body;
console.log(`PubSub message: ${JSON.stringify(pubSubMessage)}`);

const fileEvent = JSON.parse(Buffer.from(pubSubMessage.message.data, 'base64').toString().trim());
console.log(`Received thumbnail request for file ${fileEvent.name} from bucket ${fileEvent.bucket}`);

En la plataforma de Cloud Run, los mensajes de Pub/Sub se envían a través de solicitudes HTTP POST, como cargas útiles JSON con el siguiente formato:

{
  "message": {
    "attributes": {
      "bucketId": "uploaded-pictures",
      "eventTime": "2020-02-27T09:22:43.255225Z",
      "eventType": "OBJECT_FINALIZE",
      "notificationConfig": "projects/_/buckets/uploaded-pictures/notificationConfigs/28",
      "objectGeneration": "1582795363255481",
      "objectId": "IMG_20200213_181159.jpg",
      "payloadFormat": "JSON_API_V1"
    },
    "data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN...FQUU9Igp9Cg==",
    "messageId": "1014308302773399",
    "message_id": "1014308302773399",
    "publishTime": "2020-02-27T09:22:43.973Z",
    "publish_time": "2020-02-27T09:22:43.973Z"
  },
  "subscription": "projects/serverless-picadaily/subscriptions/gcs-events-subscription"
}

Pero lo que es realmente interesante en este documento JSON es lo que contiene el atributo message.data, que es solo una cadena, pero que codifica la carga útil real en Base 64. Es por eso que nuestro código anterior decodifica el contenido Base 64 de este atributo. Una vez decodificado, ese atributo data contiene otro documento JSON que representa los detalles del evento de Cloud Storage, el cual, entre otros metadatos, indica el nombre del archivo y del bucket.

{
  "kind": "storage#object",
  "id": "uploaded-pictures/IMG_20200213_181159.jpg/1582795363255481",
  "selfLink": "https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg",
  "name": "IMG_20200213_181159.jpg",
  "bucket": "uploaded-pictures",
  "generation": "1582795363255481",
  "metageneration": "1",
  "contentType": "image/jpeg",
  "timeCreated": "2020-02-27T09:22:43.255Z",
  "updated": "2020-02-27T09:22:43.255Z",
  "storageClass": "STANDARD",
  "timeStorageClassUpdated": "2020-02-27T09:22:43.255Z",
  "size": "4944335",
  "md5Hash": "QzBIoPJBV2EvqB1EVk1riw==",
  "mediaLink": "https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg?generation=1582795363255481&alt=media",
  "crc32c": "hQ3uHg==",
  "etag": "CLmJhJu08ecCEAE="
}

Nos interesan los nombres de las imágenes y los buckets, ya que nuestro código recuperará esa imagen del bucket para el tratamiento de las miniaturas:

const bucket = storage.bucket(fileEvent.bucket);
const thumbBucket = storage.bucket(process.env.BUCKET_THUMBNAILS);

const originalFile = path.resolve('/tmp/original', fileEvent.name);
const thumbFile = path.resolve('/tmp/thumbnail', fileEvent.name);

await bucket.file(fileEvent.name).download({
    destination: originalFile
});
console.log(`Downloaded picture into ${originalFile}`);

Estamos recuperando el nombre del bucket de almacenamiento de salida de una variable de entorno.

Tenemos el bucket de origen cuya creación de archivos activó nuestro servicio de Cloud Run y el bucket de destino en el que almacenaremos la imagen resultante. Usamos la API integrada de path para controlar los archivos locales, ya que la biblioteca de imagemagick creará la miniatura de forma local en el directorio temporal /tmp. Ejecutamos await para una llamada asíncrona para descargar el archivo de imagen subido.

const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
        srcPath: originalFile,
        dstPath: thumbFile,
        width: 400,
        height: 400         
});
console.log(`Created local thumbnail in ${thumbFile}`);

El módulo de imagemagick no es muy compatible con async y await, por lo que lo unimos a una promesa de JavaScript (proporcionada por el módulo de Bluebird). Luego llamamos a la función de cambio de tamaño / recorte asíncrona que creamos con los parámetros para los archivos fuente y de destino, así como las dimensiones de la miniatura que queremos crear.

await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);

Una vez que se suba el archivo de la miniatura a Cloud Storage, también actualizaremos los metadatos en Cloud Firestore para agregar una marca booleana que indique que la miniatura de esta imagen en efecto se generó:

const pictureStore = new Firestore().collection('pictures');
const doc = pictureStore.doc(fileEvent.name);
await doc.set({
    thumbnail: true
}, {merge: true});
console.log(`Updated Firestore about thumbnail creation for ${fileEvent.name}`);

res.status(204).send(`${fileEvent.name} processed`);

Una vez finalizada nuestra solicitud, respondemos a la solicitud HTTP POST en la que se indica que el archivo se procesó correctamente.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started thumbnail generator on port ${PORT}`);
});

Al final del archivo fuente, tenemos las instrucciones para que Express inicie nuestra aplicación web en el puerto predeterminado 8080.

7. Realiza pruebas locales

Prueba el código de forma local para asegurarte de que funciona antes de implementarlo en la nube.

En la carpeta thumbnails/nodejs, instala las dependencias de npm y, luego, inicia el servidor:

npm install; npm start

Si todo salió bien, debería iniciar el servidor en el puerto 8080:

Started thumbnail generator on port 8080

Usa CTRL-C para salir.

8. Cómo compilar y publicar la imagen de contenedor

Cloud Run ejecuta contenedores, pero primero debes compilar la imagen del contenedor (definida en Dockerfile). Google Cloud Build se puede usar para compilar imágenes de contenedor y, luego, alojarse en Google Container Registry.

Dentro de la carpeta thumbnails/nodejs, donde se encuentra Dockerfile, ejecuta el siguiente comando para compilar la imagen del contenedor:

gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

Después de uno o dos minutos, la compilación debería completarse correctamente:

b354b3a9a3631097.png

El “historial” de Cloud Build La sección correspondiente también debería mostrar la compilación correcta:

df00f198dd2bf6bf.png

Haz clic en el ID de compilación para obtener la vista de detalles en los “artefactos de compilación” deberías ver que la imagen de contenedor se subió a Cloud Registry (GCR):

a4577ce0744f73e2.png

Si lo deseas, puedes volver a verificar que la imagen de contenedor se ejecute de manera local en Cloud Shell:

docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

Debe iniciar el servidor en el puerto 8080 en el contenedor:

Started thumbnail generator on port 8080

Usa CTRL-C para salir.

9. Implementa en Cloud Run

Antes de realizar implementaciones en Cloud Run, configura la región de Cloud Run en una de las regiones y la plataforma compatibles en managed:

gcloud config set run/region europe-west1
gcloud config set run/platform managed

Puedes verificar que la configuración esté establecida:

gcloud config list

...
[run]
platform = managed
region = europe-west1

Ejecuta el siguiente comando para implementar la imagen de contenedor en Cloud Run:

SERVICE_NAME=thumbnail-service
gcloud run deploy $SERVICE_NAME \
    --image gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service \
    --no-allow-unauthenticated \
    --update-env-vars BUCKET_THUMBNAILS=$BUCKET_THUMBNAILS

Observa la marca --no-allow-unauthenticated. Esto convierte al servicio de Cloud Run en un servicio interno que solo las cuentas de servicio específicas activarán.

Si la implementación se realiza correctamente, deberías ver el siguiente resultado:

c0f28e7d6de0024.png

Si te diriges a la IU de la consola de Cloud, también deberías ver que el servicio se implementó correctamente:

9bfe48e3c8b597e5.png

10. Eventos de Cloud Storage a Cloud Run a través de Pub/Sub

El servicio está listo, pero aún debes crear eventos de Cloud Storage en el servicio de Cloud Run recién creado. Cloud Storage puede enviar eventos de creación de archivos a través de Cloud Pub/Sub, pero debes seguir algunos pasos para que funcione.

Crea un tema de Pub/Sub como la canalización de comunicación:

TOPIC_NAME=cloudstorage-cloudrun-topic
gcloud pubsub topics create $TOPIC_NAME

Crea notificaciones de Pub/Sub cuando los archivos se almacenen en el bucket:

BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT
gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES

Crea una cuenta de servicio para la suscripción a Pub/Sub que crearemos más adelante:

SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
     --display-name "Cloud Run Pub/Sub Invoker"

Otorga permiso a la cuenta de servicio para invocar un servicio de Cloud Run:

SERVICE_NAME=thumbnail-service
gcloud run services add-iam-policy-binding $SERVICE_NAME \
   --member=serviceAccount:$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \
   --role=roles/run.invoker

Si habilitaste la cuenta de servicio de Pub/Sub el 8 de abril de 2021 o antes de esa fecha, otorga el rol iam.serviceAccountTokenCreator a la cuenta de servicio de Pub/Sub:

PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
     --member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
     --role=roles/iam.serviceAccountTokenCreator

Es posible que los cambios de IAM tarden unos minutos en propagarse.

Por último, crea una suscripción a Pub/Sub con la cuenta de servicio:

SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --format 'value(status.url)')
gcloud pubsub subscriptions create $TOPIC_NAME-subscription --topic $TOPIC_NAME \
   --push-endpoint=$SERVICE_URL \
   --push-auth-service-account=$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com

Puedes verificar que se cree una suscripción. En la consola, ve a Pub/Sub, selecciona el tema gcs-events y, en la parte inferior, deberías ver la suscripción:

e8ab86dccb8d890.png

11. Prueba el servicio

Para probar si la configuración funciona, sube una foto nueva al bucket uploaded-pictures y verifica en el bucket thumbnails que las imágenes nuevas con el tamaño nuevo aparezcan de la forma esperada.

También puedes volver a revisar los registros para ver que aparecen los mensajes de registro, ya que se realizan los distintos pasos del servicio de Cloud Run:

42c025e2d7d6ca3a.png

12. Limpieza (opcional)

Si no pretendes continuar con los otros labs de la serie, puedes liberar recursos para ahorrar costos y ser un buen ciudadano de la nube en general. Puedes limpiar los recursos de forma individual de la siguiente manera.

Borra el bucket:

gsutil rb gs://$BUCKET_THUMBNAILS

Borra el servicio:

gcloud run services delete $SERVICE_NAME -q

Borra el tema de Pub/Sub:

gcloud pubsub topics delete $TOPIC_NAME

También puedes borrar todo el proyecto:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. ¡Felicitaciones!

Ahora, todo está en su lugar:

  • Creaste una notificación en Cloud Storage que envía mensajes de Pub/Sub sobre un tema cuando se sube una imagen nueva.
  • Definieron las vinculaciones y cuentas de IAM necesarias (a diferencia de Cloud Functions, que es completamente automática, aquí se configura de forma manual).
  • Creaste una suscripción para que nuestro servicio de Cloud Run reciba los mensajes de Pub/Sub.
  • Cada vez que se sube una foto nueva al bucket, la foto cambia de tamaño gracias al nuevo servicio de Cloud Run.

Temas abordados

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Próximos pasos