Pic-a-Daily: 실습 2—사진 썸네일 만들기

1. 개요

이 Codelab에서는 이전 실습을 기반으로 썸네일 서비스를 추가합니다. 썸네일 서비스는 큰 사진을 가져와 썸네일을 만드는 웹 컨테이너입니다.

사진이 Cloud Storage에 업로드되면 Cloud Pub/Sub를 통해 Cloud Run 웹 컨테이너로 알림이 전송되고, 웹 컨테이너는 이미지 크기를 조정한 후 Cloud Storage의 다른 버킷에 다시 저장합니다.

31fa4f8a294d90df.png

학습할 내용

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

2. 설정 및 요구사항

자습형 환경 설정

  1. Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
  • 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유해야 하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud Console은 고유한 문자열을 자동으로 생성합니다. 일반적으로 신경 쓰지 않아도 됩니다. 대부분의 Codelab에서는 프로젝트 ID를 참조해야 하며(일반적으로 PROJECT_ID로 식별됨), 마음에 들지 않는 경우 임의로 다시 생성하거나 직접 지정해서 사용할 수 있는지 확인하세요. 프로젝트가 생성되면 프로젝트 ID가 '고정'됩니다.
  • 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참조하세요.
  1. 다음으로 Cloud 리소스/API를 사용하려면 Cloud Console에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼을 마친 후 비용이 결제되지 않도록 리소스를 종료하려면 Codelab의 끝에 있는 '삭제' 안내를 따르세요. Google Cloud 새 사용자에게는 미화 $300 상당의 무료 체험판 프로그램에 참여할 수 있는 자격이 부여됩니다.

Cloud Shell 시작

Google Cloud를 노트북에서 원격으로 실행할 수 있지만, 이 Codelab에서는 Cloud에서 실행되는 명령줄 환경인 Google Cloud Shell을 사용합니다.

GCP 콘솔에서 오른쪽 상단 툴바의 Cloud Shell 아이콘을 클릭합니다.

bce75f34b2c53987.png

환경을 프로비저닝하고 연결하는 데 몇 분 정도 소요됩니다. 완료되면 다음과 같이 표시됩니다.

f6ef2b5f13479f3a.png

가상 머신에는 필요한 개발 도구가 모두 들어있습니다. 영구적인 5GB 홈 디렉토리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 실습의 모든 작업은 브라우저만으로 수행할 수 있습니다.

3. API 사용 설정

이 실습에서는 컨테이너 이미지를 빌드하는 데 Cloud Build가 필요하고 컨테이너를 배포하는 데 Cloud Run이 필요합니다.

Cloud Shell에서 두 API를 모두 사용 설정합니다.

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이 고유한 프로젝트 ID로 설정되어 있습니다. 이를 버킷 이름에 추가하면 됩니다. 그런 다음 균일한 수준의 액세스 권한을 사용하여 유럽에 공개 멀티 리전 버킷을 만듭니다.

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

결과적으로 다음과 같은 새 공개 버킷이 있어야 합니다.

8e75c8099938e972.png

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. 코드 살펴보기

코드를 탐색하려면 Cloud Shell 창 상단의 Open Editor 버튼을 클릭하여 내장된 텍스트 편집기를 사용합니다.

3d145fe299dd8b3e.png

전용 브라우저 창에서 편집기를 열어 화면 공간을 더 확보할 수도 있습니다.

종속 항목

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 웹 애플리케이션을 만듭니다. 또한 들어오는 요청은 실제로 애플리케이션에 대한 POST 요청을 통해 전송되는 JSON 페이로드일 뿐이므로 JSON 본문 파서를 사용하고 싶다고 표시합니다.

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

/ 기본 URL에서 이러한 수신 페이로드를 수신하고 Google Cloud 웹 콘솔의 Stackdriver Logging 인터페이스에서 표시되는 로그를 확인하여 코드에서 오류가 발생하는 이유에 관한 정보를 더 잘 파악할 수 있도록 오류 로직 처리로 코드를 래핑합니다.

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 메시지는 다음과 같은 형식의 JSON 페이로드로 HTTP POST 요청을 통해 전송됩니다.

{
  "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 속성에는 Cloud Storage 이벤트 세부정보를 나타내는 또 다른 JSON 문서가 포함되어 있으며, 이 문서에는 다른 메타데이터와 함께 파일 이름과 버킷 이름이 표시됩니다.

{
  "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 서비스가 트리거된 원본 버킷과 결과 이미지를 저장할 대상 버킷이 있습니다. imagemagick 라이브러리가 /tmp 임시 디렉터리에 썸네일을 로컬로 생성하므로 로컬 파일 처리를 위해 path 내장 API를 사용합니다. 업로드된 이미지 파일을 다운로드하기 위해 비동기 호출을 위해 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 친화적이지 않으므로 Bluebird 모듈에서 제공하는 JavaScript Promise 내에 래핑합니다. 그런 다음 소스 및 대상 파일의 매개변수와 생성하려는 썸네일의 크기를 사용하여 생성한 비동기 크기 조절 / 자르기 함수를 호출합니다.

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 Cloud Build를 사용하여 컨테이너 이미지를 빌드한 다음 Google Container Registry에 호스팅할 수 있습니다.

Dockerfile이 있는 thumbnails/nodejs 폴더에서 다음 명령어를 실행하여 컨테이너 이미지를 빌드합니다.

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

1~2분 후에 빌드가 성공해야 합니다.

b354b3a9a3631097.png

Cloud Build '기록' 섹션에도 성공한 빌드가 표시됩니다.

df00f198dd2bf6bf.png

빌드 ID를 클릭하여 세부정보 보기를 가져오면 '빌드 아티팩트' 탭에 컨테이너 이미지가 Cloud Registry (GCR)에 업로드된 것으로 표시됩니다.

a4577ce0744f73e2.png

원하는 경우 컨테이너 이미지가 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에 배포하기 전에 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 서비스가 특정 서비스 계정에서만 트리거되는 내부 서비스가 됩니다.

배포가 성공하면 다음과 같은 출력이 표시됩니다.

c0f28e7d6de0024.png

클라우드 콘솔 UI로 이동하면 서비스가 성공적으로 배포되었음을 확인할 수 있습니다.

9bfe48e3c8b597e5.png

10. Pub/Sub를 통한 Cloud Storage 이벤트에서 Cloud Run으로

서비스는 준비되었지만 새로 만든 Cloud Run 서비스에 Cloud Storage 이벤트를 만들어야 합니다. 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

2021년 4월 8일 이전에 Pub/Sub 서비스 계정을 사용 설정한 경우 Pub/Sub 서비스 계정에 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 주제를 선택하면 하단에 구독이 표시됩니다.

e8ab86dccb8d890.png

11. 서비스 테스트

설정이 작동하는지 테스트하려면 uploaded-pictures 버킷에 새 사진을 업로드하고 thumbnails 버킷에서 크기가 조절된 새 사진이 예상대로 표시되는지 확인합니다.

Cloud Run 서비스의 다양한 단계가 진행될 때 로깅 메시지가 표시되는지 로그를 다시 한번 확인할 수도 있습니다.

42c025e2d7d6ca3a.png

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 메시지를 전송하는 Cloud Storage의 알림을 만들었습니다.
  • 필수 IAM 바인딩 및 계정을 정의했습니다 (모든 것이 자동화된 Cloud Functions와 달리 여기서는 수동으로 구성됨).
  • Cloud Run 서비스가 Pub/Sub 메시지를 수신하도록 구독을 만들었습니다.
  • 새 사진이 버킷에 업로드될 때마다 새 Cloud Run 서비스 덕분에 사진의 크기가 조절됩니다.

학습한 내용

  • Cloud Run
  • Cloud Storage
  • Cloud Pub/Sub

다음 단계