1. Descripción general
En este codelab, continuarás con el lab anterior y agregarás un servicio de miniaturas. El servicio de miniaturas es un contenedor web que toma imágenes grandes y crea miniaturas a partir de ellas.
A medida que la imagen 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.

Qué aprenderás
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
2. Configuración y requisitos
Configuración del entorno de autoaprendizaje
- 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.



- 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.
- 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:

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:

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. Crea 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, configura una variable para el nombre único del bucket. Cloud Shell ya tiene GOOGLE_CLOUD_PROJECT establecido en tu ID del proyecto único. Puedes agregar ese valor al nombre del bucket. Luego, crea un bucket público multirregional en Europa con acceso uniforme a nivel del bucket:
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:

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 archivos para el servicio:
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
Dentro de la carpeta thumbnails/nodejs, tienes 3 archivos:
index.jscontiene el código de Node.jspackage.jsondefine las dependencias de la bibliotecaDockerfiledefine 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 que se encuentra en la parte superior de la ventana de Cloud Shell:

También puedes abrir el editor en una ventana del navegador exclusiva para tener más espacio en la 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 la imagen Express es un framework web de JavaScript / Node. El módulo body-parser se usa para analizar las solicitudes entrantes con facilidad. Bluebird se usa para controlar las 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 Node 14 y la biblioteca ImageMagick se usa para la manipulación de imágenes. Se crean algunos directorios temporales para almacenar archivos de imágenes originales y de miniaturas. Luego, se instalan los módulos de NPM que necesita nuestro código antes de iniciarlo con npm start.
index.js
Exploremos el código por partes para 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 de Express, además de indicar que queremos usar el analizador de cuerpo JSON, ya que las solicitudes entrantes son, en realidad, solo cargas útiles de 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 ajustamos nuestro código con cierta lógica de control de errores para tener mejor información sobre por qué algo puede fallar en nuestro código. Para ello, consultamos los registros que se verán 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"
}
Sin embargo, lo que realmente es interesante en este documento JSON es lo que se incluye en el atributo message.data, que es solo una cadena, pero que codifica la carga útil real en Base64. Por eso, el código anterior decodifica el contenido en Base64 de este atributo. Ese atributo data, una vez decodificado, contiene otro documento JSON que representa los detalles del evento de Cloud Storage, que, entre otros metadatos, indica el nombre del archivo y el nombre 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 la imagen y el bucket, ya que nuestro código recuperará esa imagen del bucket para su tratamiento de miniatura:
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}`);
Recuperamos 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 path para controlar los archivos locales, ya que la biblioteca de ImageMagick creará la miniatura de forma local en el directorio temporal /tmp. Hacemos 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 imagemagick no es muy compatible con async o await, por lo que lo incluimos en una promesa de JavaScript (proporcionada por el módulo Bluebird). Luego, llamamos a la función asíncrona de cambio de tamaño y recorte que creamos con los parámetros de los archivos de origen y 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 miniatura a Cloud Storage, también actualizaremos los metadatos en Cloud Firestore para agregar una marca booleana que indique que, de hecho, se generó la miniatura de esta imagen:
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 que finaliza nuestra solicitud, respondemos a la solicitud HTTP POST 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 de nuestro 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 funcione antes de implementarlo en la nube.
Dentro de 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 contenedores y, luego, alojarlas en Google Container Registry.
Dentro de la carpeta thumbnails/nodejs en la que 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 un minuto o dos, la compilación debería completarse correctamente:

La sección "Historial" de Cloud Build también debería mostrar la compilación correcta:

Si haces clic en el ID de compilación para obtener la vista de detalles, en la pestaña "Artefactos de compilación", deberías ver que la imagen del contenedor se subió a Cloud Registry (GCR):

Si lo deseas, puedes verificar que la imagen de contenedor se ejecute de forma local en Cloud Shell:
docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
Debería iniciar el servidor en el puerto 8080 del contenedor:
Started thumbnail generator on port 8080
Usa CTRL-C para salir.
9. Implementa en Cloud Run
Antes de implementar en Cloud Run, configura la región de Cloud Run en una de las regiones compatibles y la plataforma 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 se activará con cuentas de servicio específicas.
Si la implementación se realizó correctamente, deberías ver el siguiente resultado:

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

10. Eventos de Cloud Storage a Cloud Run a través de Pub/Sub
El servicio está listo, pero aún debes enviar eventos de Cloud Storage al 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 hay algunos pasos que debes seguir para que esto funcione.
Crea un tema de Pub/Sub como 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 almacenan 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 haya creado una suscripción. Ve a Pub/Sub en la consola, selecciona el tema gcs-events y, en la parte inferior, deberías ver la suscripción:

11. Prueba el servicio
Para probar si la configuración funciona, sube una imagen nueva al bucket uploaded-pictures y verifica en el bucket thumbnails que las imágenes redimensionadas nuevas aparezcan según lo esperado.
También puedes verificar los registros para ver los mensajes de registro a medida que se ejecutan los distintos pasos del servicio de Cloud Run:

12. Limpieza (opcional)
Si no quieres continuar con los otros labs de la serie, puedes limpiar los recursos para ahorrar costos y ser un buen ciudadano de la nube. 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
Como alternativa, puedes borrar todo el proyecto:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
13. ¡Felicitaciones!
Ahora todo está en su lugar:
- Se creó una notificación en Cloud Storage que envía mensajes de Pub/Sub en un tema cuando se sube una imagen nueva.
- Se definieron las vinculaciones y cuentas de IAM requeridas (a diferencia de Cloud Functions, donde todo se automatiza, aquí se configura de forma manual).
- Se creó una suscripción para que nuestro servicio de Cloud Run reciba los mensajes de Pub/Sub.
- Cada vez que se sube una imagen nueva al bucket, se cambia su tamaño gracias al nuevo servicio de Cloud Run.
Temas abordados
- Cloud Run
- Cloud Storage
- Cloud Pub/Sub
Próximos pasos
- Pic-a-daily: Lab 3—Running containers on a schedule (Pic-a-daily: Lab 3: Ejecuta contenedores de forma programada)