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

1. Przegląd

W tym module rozwijasz poprzedni moduł i dodajesz usługę miniatur. Usługa miniatur to kontener internetowy, który pobiera duże zdjęcia i tworzy z nich miniatury.

Gdy obraz zostanie przesłany do Cloud Storage, za pomocą Cloud Pub/Sub do kontenera internetowego Cloud Run zostanie wysłane powiadomienie. Kontener zmieni rozmiar obrazów i zapisze 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 użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • Nazwa projektu to wyświetlana nazwa uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google. Możesz go w dowolnym momencie zaktualizować.
  • Identyfikator projektu musi być unikalny we wszystkich projektach Google Cloud i jest niezmienny (nie można go zmienić po ustawieniu). Konsola Cloud automatycznie generuje unikalny ciąg znaków. Zwykle nie musisz się nim przejmować. W większości modułów z kodem musisz odwoływać się do identyfikatora projektu (zwykle oznaczanego jako PROJECT_ID). Jeśli Ci się nie podoba, wygeneruj inny losowy identyfikator lub spróbuj użyć własnego i sprawdź, czy jest dostępny. Po utworzeniu projektu jest on „zamrażany”.
  • Istnieje też trzecia wartość, czyli numer projektu, którego używają niektóre interfejsy API. Więcej informacji o tych 3 wartościach znajdziesz w dokumentacji.
  1. Następnie musisz włączyć płatności w konsoli Cloud, aby korzystać z zasobów i interfejsów API Google Cloud. Ukończenie tego laboratorium nie powinno wiązać się z dużymi kosztami, a nawet z żadnymi. Aby wyłączyć zasoby i uniknąć naliczenia opłat po zakończeniu tego samouczka, postępuj zgodnie z instrukcjami „czyszczenia” na końcu ćwiczenia. Nowi użytkownicy Google Cloud mogą skorzystać z bezpłatnego okresu próbnego, w którym mają do dyspozycji środki w wysokości 300 USD.

Uruchamianie Cloud Shell

Z Google Cloud można korzystać zdalnie na laptopie, ale w tym module praktycznym będziesz używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

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

bce75f34b2c53987.png

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

f6ef2b5f13479f3a.png

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Wszystkie zadania w tym module możesz wykonać w przeglądarce.

3. Włącz interfejsy API

W tym module będziesz potrzebować Cloud Build do tworzenia obrazów kontenerów i Cloud Run do wdrażania kontenera.

Włącz oba interfejsy API w Cloud Shell:

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

Operacja powinna zakończyć się powodzeniem:

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

4. Tworzenie kolejnego zasobnika

Miniatury przesłanych zdjęć będziesz przechowywać w innym zasobniku. Użyjmy polecenia gsutil, aby utworzyć drugi zasobnik.

W Cloud Shell ustaw zmienną dla unikalnej nazwy zasobnika. W Cloud Shell zmienna GOOGLE_CLOUD_PROJECT jest już ustawiona na Twój unikalny identyfikator projektu. Możesz dodać go do nazwy zasobnika. Następnie utwórz publiczny zasobnik z wieloma regionami w Europie z jednolitym poziomem dostępu:

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

Na koniec powinna pojawić się nowa publiczna pula:

8e75c8099938e972.png

5. Klonowanie kodu

Sklonuj 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

Usługa będzie miała ten układ pliku:

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

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

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

6. Poznaj kod

Aby przejrzeć kod, możesz użyć wbudowanego edytora tekstu. W tym celu kliknij przycisk Open Editor u góry okna Cloud Shell:

3d145fe299dd8b3e.png

Możesz też otworzyć edytor w osobnym oknie przeglądarki, aby zyskać więcej miejsca na ekranie.

Zależności

Plik package.json określa wymagane 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 obrazów w Cloud Storage. Firestore, aby zaktualizować metadane obrazu. Express to platforma internetowa JavaScript / Node. Moduł body-parser służy do łatwego analizowania przychodzących żądań. Bluebird służy do obsługi obietnic, a Imagemagick to biblioteka do manipulowania obrazami.

Dockerfile

Dockerfile określa 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 bazowym jest Node 14, a do manipulowania obrazami używana jest biblioteka ImageMagick. Niektóre katalogi tymczasowe są tworzone w celu przechowywania oryginalnych plików obrazów i plików miniatur. Następnie instalowane są moduły NPM potrzebne do działania naszego kodu, a potem kod jest uruchamiany za pomocą polecenia npm start.

index.js

Przyjrzyjmy się poszczególnym częściom kodu, aby lepiej zrozumieć, co robi 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 niezbędnych zależności i tworzymy aplikację internetową Express, a także wskazujemy, że chcemy używać analizatora treści JSON, ponieważ przychodzące żądania 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 przychodzące ładunki na adresie URL / i otaczamy nasz kod logiką obsługi błędów, aby uzyskać lepsze informacje o tym, dlaczego coś może się nie udać w naszym kodzie. W tym celu sprawdzamy logi, które będą widoczne w interfejsie Stackdriver Logging w konsoli internetowej 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}`);

Na platformie Cloud Run wiadomości Pub/Sub są wysyłane za pomocą żądań HTTP POST jako ładunki JSON w formacie:

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

Jednak to, co jest naprawdę interesujące w tym dokumencie JSON, to zawartość atrybutu message.data, który jest po prostu ciągiem znaków, ale koduje rzeczywisty ładunek w formacie Base64. Dlatego w naszym kodzie powyżej dekodujemy zawartość tego atrybutu w formacie Base64. Ten atrybut data po zdekodowaniu zawiera kolejny dokument JSON, który reprezentuje szczegóły zdarzenia Cloud Storage, w tym nazwę pliku i nazwę 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="
}

Interesują nas nazwy obrazu i zasobnika, ponieważ nasz kod pobierze ten obraz z zasobnika w celu utworzenia miniatury:

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

Nazwę wyjściowego zasobnika na dane pobieramy ze zmiennej środowiskowej.

Mamy zasobnik źródłowy, w którym utworzenie pliku wywołało naszą usługę Cloud Run, oraz zasobnik docelowy, w którym będziemy przechowywać wynikowy obraz. Do obsługi plików lokalnych używamy wbudowanego interfejsu API path, ponieważ biblioteka ImageMagick utworzy miniaturę lokalnie w katalogu tymczasowym /tmp. Wysyłamy await w przypadku wywołania asynchronicznego, aby pobrać przesłany plik 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 async / await, więc umieszczamy go w obietnicy JavaScript (zapewnianej przez moduł Bluebird). Następnie wywołujemy utworzoną przez nas asynchroniczną funkcję zmiany rozmiaru lub przycinania, podając parametry plików źródłowego i docelowego oraz wymiary miniatury, którą chcemy utworzyć.

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

Gdy plik miniatury zostanie przesłany do Cloud Storage, zaktualizujemy też metadane w Cloud Firestore, aby dodać flagę logiczną wskazującą, że miniatura tego 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 zakończeniu żądania odpowiadamy na żądanie POST HTTP, że plik został prawidłowo przetworzony.

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

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

Na końcu pliku źródłowego znajdują się instrukcje, które powodują, że Express uruchamia aplikację internetową na domyślnym porcie 8080.

7. Testowanie lokalne

Przetestuj kod lokalnie, aby sprawdzić, czy działa, zanim wdrożysz go w chmurze.

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

npm install; npm start

Jeśli wszystko przebiegło pomyślnie, serwer powinien zostać uruchomiony na porcie 8080:

Started thumbnail generator on port 8080

Aby wyjść, użyj CTRL-C.

8. Tworzenie i publikowanie obrazu kontenera

Cloud Run uruchamia kontenery, ale najpierw musisz utworzyć obraz kontenera (zdefiniowany w Dockerfile). Do tworzenia obrazów kontenerów i hostowania ich w Google Container Registry możesz użyć Google Cloud Build.

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

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

Po minucie lub dwóch kompilacja powinna się zakończyć:

b354b3a9a3631097.png

W sekcji „Historia” kompilacji Cloud Build powinna być widoczna udana kompilacja:

df00f198dd2bf6bf.png

Kliknij identyfikator kompilacji, aby wyświetlić szczegóły. Na karcie „Artefakty kompilacji” powinien być widoczny obraz kontenera przesłany do Cloud Registry (GCR):

a4577ce0744f73e2.png

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

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

Powinien uruchomić serwer na porcie 8080 w kontenerze:

Started thumbnail generator on port 8080

Aby wyjść, użyj CTRL-C.

9. Wdrożenie w Cloud Run

Przed wdrożeniem w Cloud Run ustaw region Cloud Run na jeden z obsługiwanych regionów, a platformę 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

Aby wdrożyć obraz kontenera w Cloud Run, uruchom to polecenie:

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 wywoływana tylko przez określone konta usługi.

Jeśli wdrożenie się powiedzie, powinny się wyświetlić te dane wyjściowe:

c0f28e7d6de0024.png

Jeśli przejdziesz do interfejsu konsoli Google Cloud, zobaczysz, że usługa została wdrożona:

9bfe48e3c8b597e5.png

10. Zdarzenia Cloud Storage do Cloud Run za pomocą Pub/Sub

Usługa jest gotowa, ale nadal musisz kierować zdarzenia Cloud Storage do nowo utworzonej usługi Cloud Run. Cloud Storage może wysyłać zdarzenia utworzenia pliku za pomocą Cloud Pub/Sub, ale aby to działało, musisz wykonać kilka czynności.

Utwórz temat Pub/Sub jako potok komunikacyjny:

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

Tworzenie powiadomień Pub/Sub, gdy pliki są 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 na potrzeby 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

Rozpowszechnienie zmian w IAM 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. W konsoli otwórz Pub/Sub, wybierz temat gcs-events, a u dołu powinna być widoczna subskrypcja:

e8ab86dccb8d890.png

11. Testowanie usługi

Aby sprawdzić, czy konfiguracja działa, prześlij nowy obraz do zasobnika uploaded-pictures i sprawdź, czy w zasobniku thumbnails pojawiają się nowe obrazy o zmienionym rozmiarze.

Możesz też sprawdzić logi, aby zobaczyć pojawiające się wiadomości logowania w trakcie wykonywania poszczególnych kroków usługi Cloud Run:

42c025e2d7d6ca3a.png

12. Zwalnianie miejsca (opcjonalnie)

Jeśli nie zamierzasz kontynuować pracy z innymi ćwiczeniami z tej serii, możesz usunąć zasoby, aby zaoszczędzić pieniądze i być dobrym użytkownikiem chmury. Możesz zwolnić miejsce, czyszcząc poszczególne zasoby 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 jest już na miejscu:

  • utworzono powiadomienie w Cloud Storage, które wysyła wiadomości Pub/Sub w temacie, gdy przesyłane jest nowe zdjęcie;
  • Określ wymagane powiązania i konta IAM (w przeciwieństwie do Cloud Functions, gdzie wszystko jest zautomatyzowane, tutaj konfiguracja jest przeprowadzana ręcznie).
  • Utworzyliśmy subskrypcję, aby usługa Cloud Run otrzymywała wiadomości Pub/Sub.
  • Za każdym razem, gdy do zasobnika zostanie przesłany nowy obraz, zostanie on przeskalowany dzięki nowej usłudze Cloud Run.

Omówione zagadnienia

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Następne kroki