Zdjęcia codziennie: moduł 2 – tworzenie miniatur zdjęć

1. Omówienie

W tym module z kodem utworzysz rozwinięcie poprzedniego modułu i dodasz usługę miniatur. Usługa miniatur to kontener internetowy, który wykonuje duże zdjęcia i tworzy z nich miniatury.

Gdy obraz jest przesyłany do Cloud Storage, Cloud Pub/Sub wysyła powiadomienie do kontenera internetowego Cloud Run, który następnie zmienia rozmiar obrazów i zapisuje je z powrotem w innym zasobniku w Cloud Storage.

31fa4f8a294d90df.png

Czego się nauczysz

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. Konfiguracja i wymagania

Samodzielne konfigurowanie środowiska

  1. Zaloguj się w konsoli Google Cloud i utwórz nowy projekt lub wykorzystaj już istniejący. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • Nazwa projektu jest wyświetlaną nazwą uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google i w każdej chwili możesz go zaktualizować.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i nie można go zmienić (nie można go zmienić po ustawieniu). Cloud Console automatycznie wygeneruje unikalny ciąg znaków. zwykle nieważne, co ona jest. W większości ćwiczeń w Codelabs musisz odwoływać się do identyfikatora projektu (który zwykle nazywa się PROJECT_ID), więc jeśli Ci się nie podoba, wygeneruj kolejny losowy kod lub wypróbuj swój własny i sprawdź, czy jest dostępny. Potem urządzenie jest „zawieszone”. po utworzeniu projektu.
  • Występuje trzecia wartość – numer projektu – używany przez niektóre interfejsy API. Więcej informacji o wszystkich 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby móc korzystać z zasobów i interfejsów API Cloud. Ukończenie tego ćwiczenia z programowania nie powinno kosztować zbyt wiele. Aby wyłączyć zasoby, aby nie naliczać opłat po zakończeniu tego samouczka, wykonaj czynności „wyczyść” znajdziesz na końcu tego ćwiczenia. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.

Uruchamianie Cloud Shell

Google Cloud można obsługiwać zdalnie z laptopa, ale w ramach tego ćwiczenia z programowania wykorzystasz Google Cloud Shell – środowisko wiersza poleceń działające w Cloud.

W konsoli GCP kliknij ikonę Cloud Shell na górnym pasku narzędzi:

bce75f34b2c53987.png

Uzyskanie dostępu do środowiska i połączenie się z nim powinno zająć tylko kilka chwil. Po zakończeniu powinno pojawić się coś takiego:

f6ef2b5f13479f3a.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, znacząco zwiększając wydajność sieci i uwierzytelnianie. Wszystkie zadania w tym module możesz wykonać w przeglądarce.

3. Włącz interfejsy API

W tym module potrzebujesz Cloud Build do utworzenia obrazów kontenerów i Cloud Run, aby wdrożyć kontener.

Włącz oba interfejsy API w Cloud Shell:

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

Powinna pojawić się operacja dokończenia:

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

4. Utwórz kolejny zasobnik

Miniatury przesłanych zdjęć zostaną zapisane w innym zasobniku. Użyjmy polecenia gsutil, aby utworzyć drugi zasobnik.

W Cloud Shell ustaw zmienną na potrzeby unikalnej nazwy zasobnika. W Cloud Shell wartość GOOGLE_CLOUD_PROJECT jest już ustawiona na Twój unikalny identyfikator projektu. Możesz to dodać do nazwy zasobnika. Następnie utwórz publiczny zasobnik dla wielu regionów w Europie z jednolitym dostępem:

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

W końcu powinieneś mieć nowy publiczny zasobnik:

8e75c8099938e972.png

5. Klonowanie kodu

Skopiuj kod i przejdź do katalogu zawierającego usługę:

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

Masz do dyspozycji taki układ plików dla usługi:

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

W folderze thumbnails/nodejs znajdują się 3 pliki:

  • index.js zawiera kod Node.js
  • package.json definiuje zależności biblioteki
  • Dockerfile definiuje obraz kontenera

6. Zapoznaj się z kodem

Aby zapoznać się z kodem, możesz użyć wbudowanego edytora tekstu. W tym celu kliknij przycisk Open Editor u góry okna Cloud Shell:

3d145fe299dd8b3e.png

Edytor możesz też otworzyć w specjalnym oknie przeglądarki, aby powiększyć obszar ekranu.

Zależności

Plik package.json określa potrzebne zależności biblioteki:

{
  "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"
  }
}

Biblioteka Cloud Storage służy do odczytywania i zapisywania plików graficznych w Cloud Storage. Firestore, aby zaktualizować metadane obrazu. Express to platforma internetowa JavaScript / węzłów. Moduł parsera treści służy do łatwego analizowania żądań przychodzących. Bluebird służy do obsługi obietnic, a Imagemagick to biblioteka do manipulowania obrazami.

Dockerfile

Dockerfile definiuje obraz kontenera aplikacji:

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" ]

Obrazem podstawowym jest Node 14, a biblioteka imagemagick jest używana do manipulacji obrazami. Niektóre katalogi tymczasowe są tworzone w celu przechowywania plików oryginalnych i miniatur. Następnie przed uruchomieniem kodu za pomocą npm start są instalowane moduły NPM wymagane przez nasz kod.

index.js

Przyjrzyjmy się fragmentom kodu, aby lepiej zrozumieć, do czego służy ten program.

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());

Najpierw wymagamy wymaganych zależności i utworzymy naszą ekspresową aplikację internetową oraz wskażemy, że chcemy użyć parsera treści JSON, ponieważ żądania przychodzące to w rzeczywistości tylko ładunki JSON wysyłane do naszej aplikacji za pomocą żądania POST.

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

Otrzymujemy te ładunki przychodzące z adresu URL / i dodamy do naszego kodu obsługę błędów logiczną. Dzięki temu w logach widocznych w interfejsie usługi Stackdriver Logging w konsoli internetowej Google Cloud uzyskasz lepsze informacje na temat możliwych przyczyn błędów w kodzie.

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}`);

Na platformie Cloud Run wiadomości Pub/Sub są wysyłane przez żądania HTTP POST jako ładunki JSON formularza:

{
  "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"
}

Najciekawszym momentem jest jednak to, co znajduje się w atrybucie message.data, który jest tylko ciągiem znaków, który koduje rzeczywisty ładunek w formacie Base 64. Dlatego powyższy kod dekoduje zawartość tego atrybutu w standardzie Base64. Ten atrybut data po zdekodowaniu zawiera kolejny dokument JSON reprezentujący szczegóły zdarzenia w Cloud Storage, który, między innymi metadane, zawiera nazwę pliku i zasobnika.

{
  "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="
}

Interesuje nas nazwa obrazu i zasobnika, ponieważ nasz kod pobierze ten obraz z zasobnika na miniaturę:

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}`);

Pobieramy nazwę wyjściowego zasobnika na dane ze zmiennej środowiskowej.

Mamy zasobnik źródłowy, którego utworzenie pliku uruchomiło usługę Cloud Run, oraz zasobnik docelowy, w którym zapiszemy wynikowy obraz. Do obsługi plików lokalnych używamy wbudowanego interfejsu API path, ponieważ biblioteka imagemagick utworzy miniaturę lokalnie w katalogu tymczasowym /tmp. Możemy await wykonać asynchroniczne wywołanie pobierania przesłanego pliku obrazu.

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

Moduł imagemagick nie jest zbyt przyjazny dla języka async / await, więc umieszczamy go w obiekcie JavaScriptu (dostarczanego w module Bluebird). Następnie wywołujemy utworzoną przez nas funkcję asynchroniczną zmiany rozmiaru / przycinania z parametrami pliku źródłowego i docelowego, a także wymiary miniatury, którą chcemy utworzyć.

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

Po przesłaniu pliku miniatury do Cloud Storage zaktualizujemy również metadane w Cloud Firestore, dodając flagę wartości logicznej, która wskazuje, że miniatura obrazu została wygenerowana:

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`);

Po rozpatrzeniu zgłoszenia odpowiadamy na żądanie HTTP POST informujące o prawidłowym przetworzeniu pliku.

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

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

Na końcu pliku źródłowego znajdziesz instrukcje, aby usługa Express uruchamiała aplikację internetową na domyślnym porcie 8080.

7. Przetestuj lokalnie

Przed wdrożeniem kodu w chmurze przetestuj go lokalnie, aby upewnić się, że działa.

W folderze thumbnails/nodejs zainstaluj zależności npm i uruchom serwer:

npm install; npm start

Jeśli wszystko poszło dobrze, serwer powinien uruchamiać się na porcie 8080:

Started thumbnail generator on port 8080

Użyj CTRL-C, aby wyjść.

8. Tworzenie i publikowanie obrazu kontenera

Cloud Run uruchamia kontenery, ale najpierw musisz utworzyć obraz kontenera (zdefiniowany w Dockerfile). Za pomocą Google Cloud Build możesz tworzyć obrazy kontenerów, a następnie hostować je w Google Container Registry.

W folderze thumbnails/nodejs, w którym znajduje się Dockerfile, uruchom to polecenie, aby utworzyć obraz kontenera:

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

Po kilku minutach kompilacja powinna się zakończyć:

b354b3a9a3631097.png

„Historia” Cloud Build powinny też pokazywać udaną kompilację:

df00f198dd2bf6bf.png

Kliknięcie identyfikatora kompilacji spowoduje wyświetlenie widoku szczegółów w sekcji „Artefakty kompilacji”. powinna pokazywać, że obraz kontenera został przesłany do Cloud Registry (GCR):

a4577ce0744f73e2.png

Jeśli chcesz, możesz dokładnie sprawdzić, czy obraz kontenera działa lokalnie w Cloud Shell:

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

Powinien uruchamiać serwer na porcie 8080 w kontenerze:

Started thumbnail generator on port 8080

Użyj CTRL-C, aby wyjść.

9. Wdrożenie w Cloud Run

Przed wdrożeniem w Cloud Run ustaw region Cloud Run na jeden z obsługiwanych regionów i platformy na managed:

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

Możesz sprawdzić, czy konfiguracja jest ustawiona:

gcloud config list

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

Uruchom to polecenie, aby wdrożyć obraz kontenera w 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

Zwróć uwagę na flagę --no-allow-unauthenticated. Dzięki temu usługa Cloud Run staje się usługą wewnętrzną, która będzie aktywowana tylko przez określone konta usługi.

Jeśli wdrożenie się powiedzie, powinny pojawić się następujące dane wyjściowe:

c0f28e7d6de0024.png

Jeśli otworzysz interfejs konsoli Cloud, zobaczysz też, że usługa została wdrożona:

9bfe48e3c8b597e5.png

10. Zdarzenia Cloud Storage przesyłane do Cloud Run przez Pub/Sub

Usługa jest gotowa, ale nadal musisz utworzyć zdarzenia Cloud Storage dla nowo utworzonej usługi Cloud Run. Cloud Storage może wysyłać zdarzenia tworzenia plików przez Cloud Pub/Sub, ale trzeba wykonać kilka czynności, aby to umożliwić.

Utwórz temat Pub/Sub jako potok komunikacji:

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

Utwórz powiadomienia Pub/Sub, gdy pliki będą przechowywane w zasobniku:

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

Utwórz konto usługi dla subskrypcji Pub/Sub, którą utworzymy później:

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

Przyznaj kontu usługi uprawnienia do wywoływania usługi 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

Jeśli konto usługi Pub/Sub zostało włączone 8 kwietnia 2021 r. lub wcześniej, przypisz rolę iam.serviceAccountTokenCreator do konta usługi 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

Zastosowanie zmian uprawnień może potrwać kilka minut.

Na koniec utwórz subskrypcję Pub/Sub za pomocą konta usługi:

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

Możesz sprawdzić, czy subskrypcja została utworzona. Otwórz Pub/Sub w konsoli, wybierz temat gcs-events. U dołu powinna się pojawić subskrypcja:

e8ab86dccb8d890.png

11. Testowanie usługi

Aby sprawdzić, czy konfiguracja działa, prześlij nowe zdjęcie do zasobnika uploaded-pictures i sprawdź w zasobniku thumbnails, czy nowe zdjęcia o zmienionych rozmiarach są wyświetlane zgodnie z oczekiwaniami.

Możesz też dokładnie sprawdzić logi, aby zobaczyć, czy pojawiają się komunikaty dotyczące logów w miarę postępów usługi Cloud Run:

42c025e2d7d6ca3a.png

12. Czyszczenie (opcjonalnie)

Jeśli nie chcesz przechodzić do innych modułów z tej serii, możesz zwolnić zasoby, aby zmniejszyć koszty i zachować dobre praktyki związane z chmurą. Zasoby możesz wyczyścić pojedynczo w ten sposób.

Usuń zasobnik:

gsutil rb gs://$BUCKET_THUMBNAILS

Usuń usługę:

gcloud run services delete $SERVICE_NAME -q

Usuń temat Pub/Sub:

gcloud pubsub topics delete $TOPIC_NAME

Możesz też usunąć cały projekt:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. Gratulacje!

Wszystko zostało przygotowane:

  • Utworzono w Cloud Storage powiadomienie, które wysyła wiadomości Pub/Sub na dany temat po przesłaniu nowego zdjęcia.
  • Zdefiniowano wymagane powiązania uprawnień i konta (w przeciwieństwie do Cloud Functions, w których wszystko jest zautomatyzowane, konfiguruje się je ręcznie tutaj).
  • Utworzono subskrypcję, aby nasza usługa Cloud Run odbierała wiadomości Pub/Sub.
  • Za każdym razem, gdy do zasobnika jest przesyłane nowe zdjęcie, jego rozmiar jest zmieniany dzięki nowej usłudze Cloud Run.

Omówione zagadnienia

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Następne kroki