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

1. Przegląd

W tym laboratorium będziesz kontynuować pracę z poprzedniego laboratorium i dodasz usługę miniatur. Usługa miniatur to kontener internetowy, który pobiera duże obrazy i tworzy z nich miniatury.

Gdy obraz zostanie przesłany do Cloud Storage, powiadomienie jest wysyłane przez Cloud Pub/Sub do kontenera internetowego Cloud Run, który 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 użyj istniejącego. Jeśli nie masz jeszcze konta Gmail ani Google Workspace, musisz je utworzyć.

96a9c957bc475304.pngb9a10ebdf5b5a448.pnga1e3c01a38fa61c2.png

  • Nazwa projektu to nazwa wyświetlana dla uczestników tego projektu. Jest to ciąg znaków, który nie jest używany przez interfejsy API Google. Możesz go w każdej chwili 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 laboratoriów będziesz musiał odwoływać się do identyfikatora projektu (zwykle oznaczanego jako PROJECT_ID), więc 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 identyfikator jest „zamrożony”.
  • Istnieje też trzecia wartość – numer projektu, której 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 móc korzystać z zasobów i interfejsów API w chmurze. Wykonanie tego laboratorium nie powinno Cię nic kosztować. Aby wyłączyć zasoby i uniknąć naliczania opłat po zakończeniu tego samouczka, postępuj zgodnie z instrukcjami „czyszczenia” na końcu laboratorium. Nowi użytkownicy Google Cloud mogą skorzystać z programu bezpłatnego okresu próbnego o wartości 300 USD.

Uruchamianie Cloud Shell

Chociaż Google Cloud można obsługiwać zdalnie z laptopa, w tym laboratorium będziesz używać Google Cloud Shell, czyli środowiska wiersza poleceń działającego w chmurze.

W konsoli Google Cloud Platform 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 może zająć kilka chwil. Gdy to się uda, zobaczysz 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 laboratorium możesz wykonać za pomocą przeglądarki.

3. Włącz interfejsy API

W tym laboratorium 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 się zakończyć:

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

4. Utwórz kolejny zasobnik

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

W Cloud Shell ustaw zmienną dla unikalnej nazwy zasobnika. Cloud Shell ma już ustawioną zmienną GOOGLE_CLOUD_PROJECT na Twój unikalny identyfikator projektu. Możesz ją dołączyć 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 powinieneś mieć nowy zasobnik publiczny:

8e75c8099938e972.png

5. Sklonuj kod

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 taką strukturę plików:

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

W folderze thumbnails/nodejs masz 3 pliki:

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

6. Poznaj kod

Aby poznać kod, możesz użyć wbudowanego edytora tekstu, klikając przycisk Open Editor u góry okna Cloud Shell:

3d145fe299dd8b3e.png

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

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 obrazów w Cloud Storage. Firestore do aktualizowania metadanych 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. Tworzone są tymczasowe katalogi do przechowywania oryginalnych plików obrazów i miniatur. Następnie instalowane są moduły NPM potrzebne do działania kodu, a potem kod jest uruchamiany za pomocą polecenia npm start.

index.js

Przyjrzyjmy się kodowi po kolei, 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 potrzebnych zależności i tworzymy aplikację internetową Express, a także wskazujemy, że chcemy używać analizatora JSON, ponieważ przychodzące żądania to w rzeczywistości tylko ładunki JSON wysyłane za pomocą żądania POST do naszej aplikacji.

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 pod adresem URL / i otaczamy nasz kod logiką obsługi błędów, aby uzyskać więcej informacji o tym, dlaczego coś może się nie udać w naszym kodzie, sprawdzając logi, które będą widoczne w interfejsie Stackdriver Logging w konsoli 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 nasz kod powyżej dekoduje zawartość Base64 tego atrybutu. Atrybut data po zdekodowaniu zawiera kolejny dokument JSON, który reprezentuje szczegóły zdarzenia Cloud Storage, w tym między innymi 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 jego 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}`);

Pobieramy nazwę wyjściowego zasobnika z zmiennej środowiskowej.

Mamy zasobnik źródłowy, którego utworzenie pliku aktywował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. await oczekuje na asynchroniczne wywołanie, 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, dlatego otaczamy go obietnicą JavaScript (dostarczaną przez moduł Bluebird). Następnie wywołujemy utworzoną przez nas asynchroniczną funkcję zmiany rozmiaru / przycinania z parametrami plików źródłowych i docelowych oraz wymiarami 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 HTTP POST, ż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 mamy instrukcje, aby Express uruchomił naszą aplikację internetową na domyślnym porcie 8080.

7. Testowanie lokalne

Przed wdrożeniem w chmurze przetestuj kod lokalnie, aby sprawdzić, czy działa.

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

npm install; npm start

Jeśli wszystko pójdzie dobrze, serwer powinien się uruchomić na porcie 8080:

Started thumbnail generator on port 8080

Aby wyjść, użyj kombinacji klawiszy 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żna używać Google Cloud Build.

W folderze thumbnails/nodejs, w którym znajduje się Dockerfile, wykonaj 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” Cloud Build powinna się też wyświetlić informacja o pomyślnej kompilacji:

df00f198dd2bf6bf.png

Kliknij identyfikator kompilacji, aby wyświetlić szczegóły. Na karcie „Artefakty kompilacji” powinna się wyświetlić informacja, że obraz kontenera został 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 on uruchomić serwer na porcie 8080 w kontenerze:

Started thumbnail generator on port 8080

Aby wyjść, użyj kombinacji klawiszy 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

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 niej usługa Cloud Run będzie usługą wewnętrzną, która będzie aktywowana 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 otworzysz interfejs konsoli Cloud, zobaczysz też, że usługa została wdrożona:

9bfe48e3c8b597e5.png

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

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

Utwórz temat Pub/Sub jako potok komunikacyjny:

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

Utwórz powiadomienia 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 do niego rolę 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

Zastosowanie zmian IAM może potrwać kilka minut.

Na koniec utwórz subskrypcję Pub/Sub z kontem 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 się wyświetlić 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ć, czy pojawiają się w nich komunikaty, gdy usługa Cloud Run przechodzi przez różne etapy:

42c025e2d7d6ca3a.png

12. Zwalnianie miejsca (opcjonalne)

Jeśli nie zamierzasz kontynuować pracy z innymi laboratoriami z tej serii, możesz zwolnić miejsce, aby zaoszczędzić koszty i być dobrym obywatelem chmury. Możesz zwalniać miejsce na zasobach 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 jest już gotowe:

  • W Cloud Storage utworzono powiadomienie, które wysyła wiadomości Pub/Sub w temacie, gdy przesyłany jest nowy obraz.
  • Zdefiniowano wymagane powiązania i konta IAM (w przeciwieństwie do Cloud Functions, gdzie wszystko jest zautomatyzowane, tutaj konfiguracja jest ręczna).
  • Utworzono subskrypcję, aby nasza usługa Cloud Run otrzymywała wiadomości Pub/Sub.
  • Gdy nowy obraz zostanie przesłany do zasobnika, jego rozmiar zostanie zmieniony dzięki nowej usłudze Cloud Run.

Omówione zagadnienia

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

Następne kroki