1. Обзор
В этой лабораторной работе вы продолжите работу, начатую в предыдущей, и добавите службу создания миниатюр. Служба создания миниатюр представляет собой веб-контейнер, который берет большие изображения и создает из них миниатюры.
После загрузки изображения в Cloud Storage, через Cloud Pub/Sub отправляется уведомление веб-контейнеру Cloud Run, который затем изменяет размер изображений и сохраняет их обратно в другой сегмент Cloud Storage.

Что вы узнаете
- Cloud Run
- Облачное хранилище
- Облачная публикация/подписка
2. Настройка и требования
Настройка среды для самостоятельного обучения
- Войдите в консоль Google Cloud и создайте новый проект или используйте существующий. Если у вас еще нет учетной записи Gmail или Google Workspace, вам необходимо ее создать .



- Название проекта — это отображаемое имя участников данного проекта. Это строка символов, не используемая API Google, и вы можете изменить её в любое время.
- Идентификатор проекта должен быть уникальным для всех проектов Google Cloud и неизменяемым (его нельзя изменить после установки). Консоль Cloud автоматически генерирует уникальную строку; обычно вам неважно, какая она. В большинстве практических заданий вам потребуется указать идентификатор проекта (обычно он обозначается как
PROJECT_ID), поэтому, если он вам не нравится, сгенерируйте другой случайный идентификатор или попробуйте свой собственный и посмотрите, доступен ли он. Затем он "замораживается" после создания проекта. - Существует третье значение — номер проекта , который используется некоторыми API. Подробнее обо всех трех значениях можно узнать в документации .
- Далее вам потребуется включить оплату в консоли Cloud, чтобы использовать ресурсы/API Cloud. Выполнение этого практического задания не должно стоить дорого, если вообще что-либо. Чтобы отключить ресурсы и избежать дополнительных расходов после завершения этого урока, следуйте инструкциям по «очистке», приведенным в конце практического задания. Новые пользователи Google Cloud имеют право на бесплатную пробную версию стоимостью 300 долларов США .
Запустить Cloud Shell
Хотя Google Cloud можно управлять удаленно с ноутбука, в этом практическом занятии вы будете использовать Google Cloud Shell — среду командной строки, работающую в облаке.
В консоли GCP щелкните значок Cloud Shell на панели инструментов в правом верхнем углу:

Подготовка и подключение к среде займут всего несколько минут. После завершения вы должны увидеть что-то подобное:

Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог размером 5 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лаборатории можно выполнять с помощью обычного браузера.
3. Включите API.
В этой лабораторной работе вам потребуется Cloud Build для создания образов контейнеров и Cloud Run для развертывания контейнеров.
Включите оба API из Cloud Shell:
gcloud services enable cloudbuild.googleapis.com \ run.googleapis.com
Вы должны увидеть сообщение об успешном завершении операции:
Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.
4. Создайте еще один контейнер.
Миниатюры загруженных изображений будут храниться в другом хранилище. Давайте воспользуемся утилитой gsutil для создания второго хранилища.
Внутри Cloud Shell задайте переменную для уникального имени корзины. В Cloud Shell уже установлена переменная GOOGLE_CLOUD_PROJECT , содержащая ваш уникальный идентификатор проекта. Вы можете добавить её к имени корзины. Затем создайте публичную многорегиональную корзину в Европе с единым уровнем доступа:
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
В итоге у вас должен появиться новый публичный бакет:

5. Клонируйте код
Клонируйте код и перейдите в директорию, содержащую сервис:
git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop cd serverless-photosharing-workshop/services/thumbnails/nodejs
Структура файлов сервиса будет следующей:
services
|
├── thumbnails
|
├── nodejs
|
├── Dockerfile
├── index.js
├── package.json
В папке thumbnails/nodejs находятся 3 файла:
-
index.jsсодержит код Node.js. -
package.jsonопределены зависимости библиотеки. -
Dockerfileопределяет образ контейнера.
6. Изучите код
Для изучения кода вы можете использовать встроенный текстовый редактор, нажав кнопку « Open Editor в верхней части окна Cloud Shell:

Вы также можете открыть редактор в отдельном окне браузера, чтобы освободить больше места на экране.
Зависимости
В файле package.json определены необходимые зависимости библиотек:
{
"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"
}
}
Библиотека Cloud Storage используется для чтения и сохранения файлов изображений в Cloud Storage. Firestore используется для обновления метаданных изображений. Express — это веб-фреймворк на JavaScript/Node. Модуль body-parser используется для упрощения обработки входящих запросов. Bluebird используется для обработки промисов, а Imagemagick — это библиотека для работы с изображениями.
Dockerfile
Dockerfile определяет образ контейнера для приложения:
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" ]
В качестве базового образа используется Node 14, а для обработки изображений применяется библиотека imagemagick. Создаются временные каталоги для хранения исходных и миниатюрных файлов изображений. Затем перед запуском кода с помощью команды npm start устанавливаются необходимые для работы кода модули NPM.
index.js
Давайте разберем код по частям, чтобы лучше понять, что делает эта программа.
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());
Сначала мы подключаем необходимые зависимости и создаём наше веб-приложение Express, а также указываем, что хотим использовать парсер JSON-тела запроса, поскольку входящие запросы фактически представляют собой JSON-данные, отправляемые в наше приложение методом POST.
app.post('/', async (req, res) => {
try {
// ...
} catch (err) {
console.log(`Error: creating the thumbnail: ${err}`);
console.error(err);
res.status(500).send(err);
}
});
Мы получаем входящие данные по базовому URL-адресу / и оборачиваем наш код логикой обработки ошибок, чтобы лучше понимать, почему что-то может не работать в нашем коде, просматривая журналы, которые будут видны в интерфейсе Stackdriver Logging в веб-консоли 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}`);
На платформе Cloud Run сообщения Pub/Sub отправляются посредством HTTP POST-запросов в виде JSON-данных следующего вида:
{
"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"
}
Но что действительно интересно в этом JSON-документе, так это то, что содержится в атрибуте message.data , который представляет собой просто строку, но кодирует фактическую полезную нагрузку в Base 64. Именно поэтому наш код выше декодирует содержимое этого атрибута в формате Base 64. После декодирования этот атрибут data содержит другой JSON-документ, представляющий сведения о событии Cloud Storage, который, помимо прочих метаданных, указывает имя файла и имя корзины.
{
"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="
}
Нас интересуют названия изображения и хранилища, поскольку наш код будет получать это изображение из хранилища для обработки в виде миниатюры:
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}`);
Мы получаем имя выходного хранилища из переменной среды.
У нас есть исходный бакет, создание файла в котором запустило нашу службу Cloud Run, и целевой бакет, где мы будем хранить полученное изображение. Мы используем path API для работы с локальными файлами, поскольку библиотека imagemagick будет создавать миниатюру локально во временном каталоге /tmp . Мы await асинхронного вызова для загрузки загруженного файла изображения.
const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
srcPath: originalFile,
dstPath: thumbFile,
width: 400,
height: 400
});
console.log(`Created local thumbnail in ${thumbFile}`);
Модуль imagemagick не очень хорошо поддерживает async / await , поэтому мы оборачиваем его в промис JavaScript (предоставляемый модулем Bluebird). Затем мы вызываем созданную нами асинхронную функцию изменения размера/обрезки с параметрами для исходного и целевого файлов, а также размерами миниатюры, которую хотим создать.
await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);
После загрузки файла миниатюры в Cloud Storage мы также обновим метаданные в Cloud Firestore, добавив логический флаг, указывающий на то, что миниатюра для этого изображения действительно создана:
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`);
После завершения обработки запроса мы отвечаем на HTTP POST-запрос, подтверждая, что файл был корректно обработан.
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Started thumbnail generator on port ${PORT}`);
});
В конце нашего исходного файла находятся инструкции, позволяющие Express запустить наше веб-приложение на стандартном порту 8080.
7. Протестируйте локально.
Перед развертыванием в облаке протестируйте код локально, чтобы убедиться в его работоспособности.
В папке thumbnails/nodejs установите зависимости npm и запустите сервер:
npm install; npm start
Если всё прошло успешно, сервер должен запуститься на порту 8080:
Started thumbnail generator on port 8080
Для выхода используйте сочетание CTRL-C .
8. Создайте и опубликуйте образ контейнера.
Cloud Run запускает контейнеры, но сначала необходимо создать образ контейнера (определенный в Dockerfile ). Для создания образов контейнеров и их последующего размещения в Google Container Registry можно использовать Google Cloud Build.
В папке thumbnails/nodejs , где находится Dockerfile , выполните следующую команду для сборки образа контейнера:
gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
Через минуту-две сборка должна завершиться успешно:

В разделе «История» Cloud Build также должны отображаться успешные сборки:

Чтобы просмотреть подробную информацию о сборке, щелкните по идентификатору сборки; на вкладке «Артефакты сборки» вы увидите, что образ контейнера был загружен в Cloud Registry (GCR):

При желании вы можете ещё раз проверить, что образ контейнера работает локально в Cloud Shell:
docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service
Необходимо запустить сервер на порту 8080 внутри контейнера:
Started thumbnail generator on port 8080
Для выхода используйте сочетание CTRL-C .
9. Развертывание в облаке.
Перед развертыванием в Cloud Run установите регион Cloud Run в качестве одного из поддерживаемых регионов и платформу для managed :
gcloud config set run/region europe-west1 gcloud config set run/platform managed
Вы можете проверить, что конфигурация настроена:
gcloud config list ... [run] platform = managed region = europe-west1
Выполните следующую команду, чтобы развернуть образ контейнера в 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
Обратите внимание на флаг --no-allow-unauthenticated . Он делает службу Cloud Run внутренней службой, которая будет запускаться только определенными учетными записями службы.
Если развертывание прошло успешно, вы должны увидеть следующий результат:

Если вы перейдете в пользовательский интерфейс облачной консоли, вы также увидите сообщение об успешном развертывании сервиса:

10. Передача событий из облачного хранилища в Cloud Run через механизм публикации/подписки.
Сервис готов, но вам все еще необходимо отправлять события Cloud Storage в недавно созданный сервис Cloud Run. Cloud Storage может отправлять события создания файлов через Cloud Pub/Sub, но для этого необходимо выполнить несколько шагов.
Создайте тему Pub/Sub в качестве канала связи:
TOPIC_NAME=cloudstorage-cloudrun-topic gcloud pubsub topics create $TOPIC_NAME
Настройте уведомления Pub/Sub при сохранении файлов в хранилище:
BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES
Создайте учетную запись службы для подписки Pub/Sub, которую мы создадим позже:
SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name "Cloud Run Pub/Sub Invoker"
Предоставьте учетной записи службы разрешение на вызов службы 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
Если вы включили учетную запись службы Pub/Sub 8 апреля 2021 года или ранее, предоставьте этой учетной записи службы роль iam.serviceAccountTokenCreator :
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
Для распространения изменений в IAM может потребоваться несколько минут.
Наконец, создайте подписку Pub/Sub с использованием учетной записи службы:
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
Вы можете проверить, создана ли подписка. Перейдите в консоль в раздел Pub/Sub, выберите тему gcs-events , и внизу вы увидите подписку:

11. Проверьте работу сервиса.
Чтобы проверить работоспособность настройки, загрузите новое изображение в папку uploaded-pictures и убедитесь, что в папке thumbnails новые уменьшенные изображения отображаются должным образом.
Вы также можете проверить журналы, чтобы увидеть сообщения, отображаемые на разных этапах работы сервиса Cloud Run:

12. Уборка (необязательно)
Если вы не планируете продолжать выполнение остальных лабораторных работ из этой серии, вы можете освободить ресурсы, чтобы сэкономить средства и в целом ответственно относиться к облачным технологиям. Освободить ресурсы можно по отдельности следующим образом.
Удалите корзину:
gsutil rb gs://$BUCKET_THUMBNAILS
Удалить сервис:
gcloud run services delete $SERVICE_NAME -q
Удалите тему Pub/Sub:
gcloud pubsub topics delete $TOPIC_NAME
В качестве альтернативы вы можете удалить весь проект:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
13. Поздравляем!
Теперь всё на своих местах:
- Создал уведомление в облачном хранилище, которое отправляет сообщения Pub/Sub по теме при загрузке нового изображения.
- Были определены необходимые привязки IAM и учетные записи (в отличие от Cloud Functions, где все автоматизировано, здесь это настраивается вручную).
- Создана подписка, чтобы наш сервис Cloud Run получал сообщения Pub/Sub.
- При каждой загрузке нового изображения в хранилище его размер изменяется благодаря новой службе Cloud Run.
Что мы рассмотрели
- Cloud Run
- Облачное хранилище
- Облачная публикация/подписка