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에 이미 고유한 프로젝트 ID로 설정된 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

최종적으로 새 공개 버킷이 생성됩니다.

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 웹 프레임워크입니다. 본문 파서 모듈은 수신되는 요청을 쉽게 파싱하는 데 사용됩니다. Bluebird는 Promise 처리에 사용되고 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" ]

기본 이미지는 노드 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 본문 파서를 사용하려고 한다는 것을 표시합니다. 수신 요청은 실제로 POST 요청을 통해 애플리케이션에 전송된 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 메시지는 HTTP POST 요청을 통해 JSON 페이로드 형식의 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 속성에 포함되어 있다는 것입니다. 이 속성은 단지 문자열이지만 실제 페이로드를 Base64로 인코딩합니다. 따라서 위 코드가 이 속성의 Base64 콘텐츠를 디코딩합니다. 이 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 프로미스 내에서 래핑합니다. 그런 다음 소스 및 대상 파일의 매개변수와 생성하려는 썸네일 크기를 사용하여 만든 비동기 크기 조절 / 자르기 함수를 호출합니다.

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

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

9bfe48e3c8b597e5.png

10. Pub/Sub를 통해 Cloud Run에 대한 Cloud Storage 이벤트

서비스가 준비되었지만 여전히 새로 만든 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

다음 단계