Cloud Storage에서 이미지 처리 가이드

이 가이드에서는 Cloud Run, Cloud Vision API, ImageMagick을 사용하여 Cloud Storage 버킷에 업로드되는 이미지 중 불쾌감을 주는 이미지를 감지하고 이를 흐리게 처리하는 방법을 설명합니다. 이 가이드는 Cloud Run에서 Pub/Sub 사용 가이드를 기반으로 합니다.

이 가이드에서는 기존 샘플 앱을 수정하는 방법을 살펴봅니다. 원할 경우 완료된 샘플을 다운로드할 수도 있습니다.

목표

  • Cloud Run에 비동기 데이터 처리 서비스를 작성, 빌드, 배포합니다.
  • Cloud Storage에 파일을 업로드하고 Pub/Sub 메시지를 만들어 서비스를 호출합니다.
  • Cloud Vision API를 사용하여 폭력적인 콘텐츠 또는 성인 콘텐츠를 감지합니다.
  • ImageMagick을 사용하여 불쾌감을 주는 이미지를 흐리게 처리합니다.
  • 살점을 뜯어먹는 좀비 이미지를 업로드하여 해당 서비스를 테스트합니다.

비용

이 가이드에서는 비용이 청구될 수 있는 다음과 같은 Cloud Platform 구성요소를 사용합니다.

가격 계산기를 사용하면 예상 사용량을 기준으로 예상 비용을 산출할 수 있습니다.

Cloud Platform 신규 사용자는 무료 체험판을 사용할 수 있습니다.

시작하기 전에

  1. Google Cloud 계정에 로그인합니다. Google Cloud를 처음 사용하는 경우 계정을 만들고 Google 제품의 실제 성능을 평가해 보세요. 신규 고객에게는 워크로드를 실행, 테스트, 배포하는 데 사용할 수 있는 $300의 무료 크레딧이 제공됩니다.
  2. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  3. Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다. 프로젝트에 결제가 사용 설정되어 있는지 확인하는 방법을 알아보세요.

  4. Google Cloud Console의 프로젝트 선택기 페이지에서 Google Cloud 프로젝트를 선택하거나 만듭니다.

    프로젝트 선택기로 이동

  5. Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다. 프로젝트에 결제가 사용 설정되어 있는지 확인하는 방법을 알아보세요.

  6. Cloud Run and Cloud Vision API를 사용 설정합니다.

    API 사용 설정

  7. Cloud SDK를 설치하고 초기화합니다.
  8. 구성요소를 업데이트합니다.
    gcloud components update
  9. Pub/Sub 사용 가이드를 따라 메시지를 처리할 Pub/Sub 주제, 안전한 푸시 구독, 초기 Cloud Run 서비스를 설정합니다.

gcloud 기본값 설정

Cloud Run 서비스의 기본값으로 gcloud를 구성하려면 다음 안내를 따르세요.

  1. 기본 프로젝트를 설정합니다.

    gcloud config set project PROJECT_ID

    PROJECT_ID를 이 튜토리얼용으로 만든 프로젝트 이름으로 바꿉니다.

  2. 선택한 리전에 맞게 gcloud를 구성합니다.

    gcloud config set run/region REGION

    REGION을 지원되는 Cloud Run 리전 중 원하는 리전으로 바꿉니다.

Cloud Run 위치

Cloud Run은 리전을 기반으로 합니다. 즉, Cloud Run 서비스를 실행하는 인프라가 특정 리전에 위치해 있으며 해당 리전 내의 모든 영역에서 중복으로 사용할 수 있도록 Google이 관리합니다.

Cloud Run 서비스를 실행하는 리전을 선택하는 데 있어 중요한 기준은 지연 시간, 가용성 또는 내구성 요구사항입니다. 일반적으로 사용자와 가장 가까운 리전을 선택할 수 있지만 Cloud Run 서비스에서 사용하는 다른 Google Cloud 제품 위치도 고려해야 합니다. 여러 위치에서 Google Cloud 제품을 함께 사용하면 서비스 지연 시간과 비용에 영향을 미칠 수 있습니다.

Cloud Run은 다음 리전에서 사용할 수 있습니다.

등급 1 가격 적용

  • asia-east1(타이완)
  • asia-northeast1(도쿄)
  • asia-northeast2(오사카)
  • europe-north1(핀란드) 리프 아이콘 낮은 CO2
  • europe-west1(벨기에) 리프 아이콘 낮은 CO2
  • europe-west4(네덜란드)
  • us-central1(아이오와) 리프 아이콘 낮은 CO2
  • us-east1(사우스캐롤라이나)
  • us-east4(북 버지니아)
  • us-west1(오리건) 리프 아이콘 낮은 CO2

등급 2 가격 적용

  • asia-east2(홍콩)
  • asia-northeast3(대한민국 서울)
  • asia-southeast1(싱가포르)
  • asia-southeast2 (자카르타)
  • asia-south1(인도 뭄바이)
  • asia-south2(인도 델리)
  • australia-southeast1(시드니)
  • australia-southeast2(멜버른)
  • europe-central2(폴란드 바르샤바)
  • europe-west2(영국 런던)
  • europe-west3(독일 프랑크푸르트)
  • europe-west6(스위스 취리히) 리프 아이콘 낮은 CO2
  • northamerica-northeast1(몬트리올) 리프 아이콘 낮은 CO2
  • northamerica-northeast2(토론토)
  • southamerica-east1(브라질 상파울루) 리프 아이콘 낮은 CO2
  • us-west2(로스앤젤레스)
  • us-west3(솔트레이크시티)
  • us-west4(라스베이거스)

Cloud Run 서비스를 이미 만들었다면 Cloud Console의 Cloud Run 대시보드에서 리전을 확인할 수 있습니다.

작업 순서 이해하기

이 가이드의 데이터 흐름은 다음 단계를 따릅니다.

  1. 사용자가 Cloud Storage 버킷에 이미지를 업로드합니다.
  2. Cloud Storage에서 Pub/Sub에 새 파일에 대한 메시지를 게시합니다.
  3. Pub/Sub가 메시지를 Cloud Run 서비스에 푸시합니다.
  4. Cloud Run 서비스에서 Pub/Sub 메시지에 참조된 이미지 파일을 검색합니다.
  5. Cloud Run 서비스에서 Cloud Vision API를 사용하여 이미지를 분석합니다.
  6. 폭력적인 콘텐츠 또는 성인 콘텐츠가 감지될 경우 Cloud Run은 ImageMagick을 사용하여 해당 이미지를 흐리게 처리합니다.
  7. Cloud Run 서비스는 흐리게 처리된 이미지를 다른 Cloud Storage 버킷에 업로드하여 사용합니다.

흐린 이미지는 다음 번 독자의 실습에 사용하도록 남겨둘 수 있습니다.

Cloud Storage 버킷 설정

  1. 이미지를 업로드할 Cloud Storage 버킷을 만듭니다. INPUT_BUCKET_NAME은 전역적으로 고유한 버킷 이름입니다.

    gsutil mb gs://INPUT_BUCKET_NAME

    Cloud Run 서비스는 이 버킷에서만 읽습니다.

  2. 흐리게 처리된 이미지를 수신할 또 다른 Cloud Storage 버킷을 만듭니다. BLURRED_BUCKET_NAME은 전역적으로 고유한 버킷 이름입니다.

    gsutil mb gs://BLURRED_BUCKET_NAME

    Cloud Run 서비스는 흐리게 처리된 이미지를 이 버킷에 업로드합니다. 별도의 버킷을 사용하면 처리된 이미지가 서비스를 다시 트리거하지 않습니다.

다음 단계에서는 INPUT_BUCKET_NAME으로의 파일 업로드 알림을 처리하는 서비스를 만들고 배포합니다. 새 서비스의 조기 호출을 방지하기 위해 서비스를 배포하고 테스트한 후 알림 전송을 사용 설정합니다.

Pub/Sub 가이드 샘플 코드 수정

이 가이드는 Pub/Sub 사용 가이드에 나온 코드 조합을 기반으로 합니다. 해당 가이드를 아직 완료하지 않았다면 먼저 완료하되 삭제 단계를 건너뛰고 이 섹션으로 돌아와 이미지 처리 동작을 추가합니다.

이미지 처리 코드 추가

이미지 처리 코드는 가독성과 테스트 용이성을 위해 요청 처리와 구분되어야 합니다. 이미지 처리 코드를 추가하려면 다음 안내를 따르세요.

  1. Pub/Sub 가이드 샘플 코드의 디렉터리로 변경합니다.

  2. Google Cloud 서비스, ImageMagick, 파일 시스템과 통합하기 위해 라이브러리를 비롯한 이미지 처리 종속 항목을 가져올 코드를 추가합니다.

    Node.js

    편집기에서 새 image.js 파일을 열고 다음을 복사합니다.
    const gm = require('gm').subClass({imageMagick: true});
    const fs = require('fs');
    const {promisify} = require('util');
    const path = require('path');
    const vision = require('@google-cloud/vision');
    
    const {Storage} = require('@google-cloud/storage');
    const storage = new Storage();
    const client = new vision.ImageAnnotatorClient();
    
    const {BLURRED_BUCKET_NAME} = process.env;

    Python

    편집기에서 새 image.py 파일을 열고 다음을 복사합니다.
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    편집기에서 새 imagemagick/imagemagick.go 파일을 열고 다음을 복사합니다.
    
    // Package imagemagick contains an example of using ImageMagick to process a
    // file uploaded to Cloud Storage.
    package imagemagick
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"os"
    	"os/exec"
    
    	"cloud.google.com/go/storage"
    	vision "cloud.google.com/go/vision/apiv1"
    	visionpb "google.golang.org/genproto/googleapis/cloud/vision/v1"
    )
    
    // Global API clients used across function invocations.
    var (
    	storageClient *storage.Client
    	visionClient  *vision.ImageAnnotatorClient
    )
    
    func init() {
    	// Declare a separate err variable to avoid shadowing the client variables.
    	var err error
    
    	storageClient, err = storage.NewClient(context.Background())
    	if err != nil {
    		log.Fatalf("storage.NewClient: %v", err)
    	}
    
    	visionClient, err = vision.NewImageAnnotatorClient(context.Background())
    	if err != nil {
    		log.Fatalf("vision.NewAnnotatorClient: %v", err)
    	}
    }
    

    자바

    편집기에서 새 src/main/java/com/example/cloudrun/ImageMagick.java 파일을 열고 다음을 복사합니다.
    import com.google.cloud.storage.Blob;
    import com.google.cloud.storage.BlobId;
    import com.google.cloud.storage.BlobInfo;
    import com.google.cloud.storage.Storage;
    import com.google.cloud.storage.StorageOptions;
    import com.google.cloud.vision.v1.AnnotateImageRequest;
    import com.google.cloud.vision.v1.AnnotateImageResponse;
    import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
    import com.google.cloud.vision.v1.Feature;
    import com.google.cloud.vision.v1.Feature.Type;
    import com.google.cloud.vision.v1.Image;
    import com.google.cloud.vision.v1.ImageAnnotatorClient;
    import com.google.cloud.vision.v1.ImageSource;
    import com.google.cloud.vision.v1.SafeSearchAnnotation;
    import com.google.gson.JsonObject;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    
    public class ImageMagick {
    
      private static final String BLURRED_BUCKET_NAME = System.getenv("BLURRED_BUCKET_NAME");
      private static Storage storage = StorageOptions.getDefaultInstance().getService();

  3. Pub/Sub 메시지를 이벤트 객체로 수신하고 이미지 처리를 제어하는 코드를 추가합니다.

    이벤트에는 업로드된 원본 이미지에 대한 데이터가 포함됩니다. 이 코드는 Cloud Vision 분석 결과에서 폭력적인 콘텐츠 또는 성인 콘텐츠를 감지하여 이를 흐리게 처리할지 여부를 결정합니다.

    Node.js

    // Blurs uploaded images that are flagged as Adult or Violence.
    exports.blurOffensiveImages = async event => {
      // This event represents the triggering Cloud Storage object.
      const object = event;
    
      const file = storage.bucket(object.bucket).file(object.name);
      const filePath = `gs://${object.bucket}/${object.name}`;
    
      console.log(`Analyzing ${file.name}.`);
    
      try {
        const [result] = await client.safeSearchDetection(filePath);
        const detections = result.safeSearchAnnotation || {};
    
        if (
          // Levels are defined in https://cloud.google.com/vision/docs/reference/rest/v1/AnnotateImageResponse#likelihood
          detections.adult === 'VERY_LIKELY' ||
          detections.violence === 'VERY_LIKELY'
        ) {
          console.log(`Detected ${file.name} as inappropriate.`);
          return blurImage(file, BLURRED_BUCKET_NAME);
        } else {
          console.log(`Detected ${file.name} as OK.`);
        }
      } catch (err) {
        console.error(`Failed to analyze ${file.name}.`, err);
        throw err;
      }
    };

    Python

    # Blurs uploaded images that are flagged as Adult or Violence.
    def blur_offensive_images(data):
        file_data = data
    
        file_name = file_data["name"]
        bucket_name = file_data["bucket"]
    
        blob = storage_client.bucket(bucket_name).get_blob(file_name)
        blob_uri = f"gs://{bucket_name}/{file_name}"
        blob_source = vision.Image(source=vision.ImageSource(image_uri=blob_uri))
    
        # Ignore already-blurred files
        if file_name.startswith("blurred-"):
            print(f"The image {file_name} is already blurred.")
            return
    
        print(f"Analyzing {file_name}.")
    
        result = vision_client.safe_search_detection(image=blob_source)
        detected = result.safe_search_annotation
    
        # Process image
        if detected.adult == 5 or detected.violence == 5:
            print(f"The image {file_name} was detected as inappropriate.")
            return __blur_image(blob)
        else:
            print(f"The image {file_name} was detected as OK.")
    
    

    Go

    
    // GCSEvent is the payload of a GCS event.
    type GCSEvent struct {
    	Bucket string `json:"bucket"`
    	Name   string `json:"name"`
    }
    
    // BlurOffensiveImages blurs offensive images uploaded to GCS.
    func BlurOffensiveImages(ctx context.Context, e GCSEvent) error {
    	outputBucket := os.Getenv("BLURRED_BUCKET_NAME")
    	if outputBucket == "" {
    		return errors.New("BLURRED_BUCKET_NAME must be set")
    	}
    
    	img := vision.NewImageFromURI(fmt.Sprintf("gs://%s/%s", e.Bucket, e.Name))
    
    	resp, err := visionClient.DetectSafeSearch(ctx, img, nil)
    	if err != nil {
    		return fmt.Errorf("AnnotateImage: %v", err)
    	}
    
    	if resp.GetAdult() == visionpb.Likelihood_VERY_LIKELY ||
    		resp.GetViolence() == visionpb.Likelihood_VERY_LIKELY {
    		return blur(ctx, e.Bucket, outputBucket, e.Name)
    	}
    	log.Printf("The image %q was detected as OK.", e.Name)
    	return nil
    }
    

    자바

    // Blurs uploaded images that are flagged as Adult or Violence.
    public static void blurOffensiveImages(JsonObject data) {
      String fileName = data.get("name").getAsString();
      String bucketName = data.get("bucket").getAsString();
      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, fileName).build();
      // Construct URI to GCS bucket and file.
      String gcsPath = String.format("gs://%s/%s", bucketName, fileName);
      System.out.println(String.format("Analyzing %s", fileName));
    
      // Construct request.
      List<AnnotateImageRequest> requests = new ArrayList<>();
      ImageSource imgSource = ImageSource.newBuilder().setImageUri(gcsPath).build();
      Image img = Image.newBuilder().setSource(imgSource).build();
      Feature feature = Feature.newBuilder().setType(Type.SAFE_SEARCH_DETECTION).build();
      AnnotateImageRequest request =
          AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(img).build();
      requests.add(request);
    
      // Send request to the Vision API.
      try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
        BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
        List<AnnotateImageResponse> responses = response.getResponsesList();
        for (AnnotateImageResponse res : responses) {
          if (res.hasError()) {
            System.out.println(String.format("Error: %s\n", res.getError().getMessage()));
            return;
          }
          // Get Safe Search Annotations
          SafeSearchAnnotation annotation = res.getSafeSearchAnnotation();
          if (annotation.getAdultValue() == 5 || annotation.getViolenceValue() == 5) {
            System.out.println(String.format("Detected %s as inappropriate.", fileName));
            blur(blobInfo);
          } else {
            System.out.println(String.format("Detected %s as OK.", fileName));
          }
        }
      } catch (Exception e) {
        System.out.println(String.format("Error with Vision API: %s", e.getMessage()));
      }
    }

  4. 위에서 만든 Cloud Storage 입력 버킷에서 참조된 이미지를 가져와 ImageMagick을 사용하여 이미지를 흐리게 처리한 다음 결과를 출력 버킷에 업로드합니다.

    Node.js

    // Blurs the given file using ImageMagick, and uploads it to another bucket.
    const blurImage = async (file, blurredBucketName) => {
      const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
    
      // Download file from bucket.
      try {
        await file.download({destination: tempLocalPath});
    
        console.log(`Downloaded ${file.name} to ${tempLocalPath}.`);
      } catch (err) {
        throw new Error(`File download failed: ${err}`);
      }
    
      await new Promise((resolve, reject) => {
        gm(tempLocalPath)
          .blur(0, 16)
          .write(tempLocalPath, (err, stdout) => {
            if (err) {
              console.error('Failed to blur image.', err);
              reject(err);
            } else {
              console.log(`Blurred image: ${file.name}`);
              resolve(stdout);
            }
          });
      });
    
      // Upload result to a different bucket, to avoid re-triggering this function.
      const blurredBucket = storage.bucket(blurredBucketName);
    
      // Upload the Blurred image back into the bucket.
      const gcsPath = `gs://${blurredBucketName}/${file.name}`;
      try {
        await blurredBucket.upload(tempLocalPath, {destination: file.name});
        console.log(`Uploaded blurred image to: ${gcsPath}`);
      } catch (err) {
        throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
      }
    
      // Delete the temporary file.
      const unlink = promisify(fs.unlink);
      return unlink(tempLocalPath);
    };

    Python

    # Blurs the given file using ImageMagick.
    def __blur_image(current_blob):
        file_name = current_blob.name
        _, temp_local_filename = tempfile.mkstemp()
    
        # Download file from bucket.
        current_blob.download_to_filename(temp_local_filename)
        print(f"Image {file_name} was downloaded to {temp_local_filename}.")
    
        # Blur the image using ImageMagick.
        with Image(filename=temp_local_filename) as image:
            image.resize(*image.size, blur=16, filter="hamming")
            image.save(filename=temp_local_filename)
    
        print(f"Image {file_name} was blurred.")
    
        # Upload result to a second bucket, to avoid re-triggering the function.
        # You could instead re-upload it to the same bucket + tell your function
        # to ignore files marked as blurred (e.g. those with a "blurred" prefix)
        blur_bucket_name = os.getenv("BLURRED_BUCKET_NAME")
        blur_bucket = storage_client.bucket(blur_bucket_name)
        new_blob = blur_bucket.blob(file_name)
        new_blob.upload_from_filename(temp_local_filename)
        print(f"Blurred image uploaded to: gs://{blur_bucket_name}/{file_name}")
    
        # Delete the temporary file.
        os.remove(temp_local_filename)
    
    

    Go

    
    // blur blurs the image stored at gs://inputBucket/name and stores the result in
    // gs://outputBucket/name.
    func blur(ctx context.Context, inputBucket, outputBucket, name string) error {
    	inputBlob := storageClient.Bucket(inputBucket).Object(name)
    	r, err := inputBlob.NewReader(ctx)
    	if err != nil {
    		return fmt.Errorf("NewReader: %v", err)
    	}
    
    	outputBlob := storageClient.Bucket(outputBucket).Object(name)
    	w := outputBlob.NewWriter(ctx)
    	defer w.Close()
    
    	// Use - as input and output to use stdin and stdout.
    	cmd := exec.Command("convert", "-", "-blur", "0x8", "-")
    	cmd.Stdin = r
    	cmd.Stdout = w
    
    	if err := cmd.Run(); err != nil {
    		return fmt.Errorf("cmd.Run: %v", err)
    	}
    
    	log.Printf("Blurred image uploaded to gs://%s/%s", outputBlob.BucketName(), outputBlob.ObjectName())
    
    	return nil
    }
    

    자바

      // Blurs the file described by blobInfo using ImageMagick,
      // and uploads it to the blurred bucket.
      public static void blur(BlobInfo blobInfo) throws IOException {
        String bucketName = blobInfo.getBucket();
        String fileName = blobInfo.getName();
        // Download image
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        Path download = Paths.get("/tmp/", fileName);
        blob.downloadTo(download);
    
        // Construct the command.
        List<String> args = new ArrayList<String>();
        args.add("convert");
        args.add(download.toString());
        args.add("-blur");
        args.add("0x8");
        Path upload = Paths.get("/tmp/", "blurred-" + fileName);
        args.add(upload.toString());
        try {
          ProcessBuilder pb = new ProcessBuilder(args);
          Process process = pb.start();
          process.waitFor();
        } catch (Exception e) {
          System.out.println(String.format("Error: %s", e.getMessage()));
        }
    
        // Upload image to blurred bucket.
        BlobId blurredBlobId = BlobId.of(BLURRED_BUCKET_NAME, fileName);
        BlobInfo blurredBlobInfo =
            BlobInfo.newBuilder(blurredBlobId).setContentType(blob.getContentType()).build();
        try {
          byte[] blurredFile = Files.readAllBytes(upload);
          Blob blurredBlob = storage.create(blurredBlobInfo, blurredFile);
          System.out.println(
              String.format("Blurred image uploaded to: gs://%s/%s", BLURRED_BUCKET_NAME, fileName));
        } catch (Exception e) {
          System.out.println(String.format("Error in upload: %s", e.getMessage()));
        }
    
        // Remove images from fileSystem
        Files.delete(download);
        Files.delete(upload);
      }
    }

Pub/Sub 샘플 코드에 이미지 처리 통합

기존 서비스를 수정하여 이미지 처리 코드를 통합하려면 다음 안내를 따르세요.

  1. Cloud Vision 및 Cloud Storage 클라이언트 라이브러리를 비롯하여 서비스의 새 종속 항목을 추가합니다.

    Node.js

    npm install --save gm @google-cloud/storage @google-cloud/vision

    Python

    requirements.txt가 다음과 같이 표시되도록 필요한 클라이언트 라이브러리를 추가합니다.
    Flask==2.0.1
    pytest==5.3.0; python_version > "3.0"
    pytest==4.6.6; python_version < "3.0"
    gunicorn==20.1.0
    google-cloud-vision==2.5.0
    google-cloud-storage==1.42.2
    Wand==0.6.7
    

    Go

    Go 샘플 애플리케이션은 go 모듈을 사용하며, 위의 imagemagick/imagemagick.go 가져오기 문에 추가된 새 종속 항목은 해당 항목이 필요한 다음 명령어를 통해 자동으로 다운로드됩니다.

    자바

    pom.xml<dependencyManagement>에 다음 종속 항목을 추가합니다.
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>1.2.8.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    pom.xml<dependencies>에 다음 종속 항목을 추가합니다.
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.8</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20210307</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.11.0</version>
    </dependency>
    

  2. FROM 문 아래의 Dockerfile을 수정하여 ImageMagick 시스템 패키지를 컨테이너에 추가합니다. '다단계' Dockerfile을 사용하는 경우 이를 최종 단계에 배치합니다.

    Debian/Ubuntu
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
    RUN set -ex; \
      apt-get -y update; \
      apt-get -y install imagemagick; \
      rm -rf /var/lib/apt/lists/*
    
    Alpine
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
    RUN apk add --no-cache imagemagick
    

    시스템 패키지 사용 가이드에서 Cloud Run 서비스의 시스템 패키지 작업에 대해 자세히 알아보세요.

  3. 기존의 Pub/Sub 메시지 처리 코드를 새 이미지 흐리게 처리 로직의 함수 호출로 바꿉니다.

    Node.js

    app.js 파일이 Express.js 앱을 정의하고 사용할 Pub/Sub 메시지를 준비합니다. 다음과 같이 변경하세요.

    • 코드를 추가하여 새 image.js 파일 가져오기
    • 경로에서 기존 'Hello World' 코드 삭제
    • Pub/Sub 메시지의 유효성을 검사하는 코드 추가
    • 새 이미지 처리 함수를 호출하는 코드 추가

      완료되면 코드는 다음과 같이 표시됩니다.

    
    const express = require('express');
    const app = express();
    
    // This middleware is available in Express v4.16.0 onwards
    app.use(express.json());
    
    const image = require('./image');
    
    app.post('/', async (req, res) => {
      if (!req.body) {
        const msg = 'no Pub/Sub message received';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
      if (!req.body.message || !req.body.message.data) {
        const msg = 'invalid Pub/Sub message format';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Decode the Pub/Sub message.
      const pubSubMessage = req.body.message;
      let data;
      try {
        data = Buffer.from(pubSubMessage.data, 'base64').toString().trim();
        data = JSON.parse(data);
      } catch (err) {
        const msg =
          'Invalid Pub/Sub message: data property is not valid base64 encoded JSON';
        console.error(`error: ${msg}: ${err}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Validate the message is a Cloud Storage event.
      if (!data.name || !data.bucket) {
        const msg =
          'invalid Cloud Storage notification: expected name and bucket properties';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      try {
        await image.blurOffensiveImages(data);
        res.status(204).send();
      } catch (err) {
        console.error(`error: Blurring image: ${err}`);
        res.status(500).send();
      }
    });

    Python

    main.py 파일이 Flask 앱을 정의하고 수신 Pub/Sub 메시지를 사용할 준비를 합니다. 다음과 같이 변경하세요.

    • 코드를 추가하여 새 image.py 파일 가져오기
    • 경로에서 기존 'Hello World' 코드 삭제
    • Pub/Sub 메시지의 유효성을 검사하는 코드 추가
    • 새 이미지 처리 함수를 호출하는 코드 추가

      완료되면 코드는 다음과 같이 표시됩니다.

    import base64
    import json
    import os
    
    from flask import Flask, request
    
    import image
    
    app = Flask(__name__)
    
    @app.route("/", methods=["POST"])
    def index():
        envelope = request.get_json()
        if not envelope:
            msg = "no Pub/Sub message received"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        if not isinstance(envelope, dict) or "message" not in envelope:
            msg = "invalid Pub/Sub message format"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        # Decode the Pub/Sub message.
        pubsub_message = envelope["message"]
    
        if isinstance(pubsub_message, dict) and "data" in pubsub_message:
            try:
                data = json.loads(base64.b64decode(pubsub_message["data"]).decode())
    
            except Exception as e:
                msg = (
                    "Invalid Pub/Sub message: "
                    "data property is not valid base64 encoded JSON"
                )
                print(f"error: {e}")
                return f"Bad Request: {msg}", 400
    
            # Validate the message is a Cloud Storage event.
            if not data["name"] or not data["bucket"]:
                msg = (
                    "Invalid Cloud Storage notification: "
                    "expected name and bucket properties"
                )
                print(f"error: {msg}")
                return f"Bad Request: {msg}", 400
    
            try:
                image.blur_offensive_images(data)
                return ("", 204)
    
            except Exception as e:
                print(f"error: {e}")
                return ("", 500)
    
        return ("", 500)

    Go

    main.go 파일이 HTTP 서비스를 정의하고 수신 Pub/Sub 메시지를 사용할 준비를 합니다. 다음과 같이 변경하세요.

    • 코드를 추가하여 새 imagemagick.go 파일 가져오기
    • 핸들러에서 기존 'Hello World' 코드 삭제
    • Pub/Sub 메시지의 유효성을 검사하는 코드 추가
    • 새 이미지 처리 함수를 호출하는 코드 추가

    
    // Sample image-processing is a Cloud Run service which performs asynchronous processing on images.
    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"os"
    
    	"github.com/GoogleCloudPlatform/golang-samples/run/image-processing/imagemagick"
    )
    
    func main() {
    	http.HandleFunc("/", HelloPubSub)
    	// Determine port for HTTP service.
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    	// Start HTTP server.
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // PubSubMessage is the payload of a Pub/Sub event.
    // See the documentation for more details:
    // https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
    type PubSubMessage struct {
    	Message struct {
    		Data []byte `json:"data,omitempty"`
    		ID   string `json:"id"`
    	} `json:"message"`
    	Subscription string `json:"subscription"`
    }
    
    // HelloPubSub receives and processes a Pub/Sub push message.
    func HelloPubSub(w http.ResponseWriter, r *http.Request) {
    	var m PubSubMessage
    	body, err := ioutil.ReadAll(r.Body)
    	if err != nil {
    		log.Printf("ioutil.ReadAll: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    	if err := json.Unmarshal(body, &m); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	var e imagemagick.GCSEvent
    	if err := json.Unmarshal(m.Message.Data, &e); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if e.Name == "" || e.Bucket == "" {
    		log.Printf("invalid GCSEvent: expected name and bucket")
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if err := imagemagick.BlurOffensiveImages(r.Context(), e); err != nil {
    		log.Printf("imagemagick.BlurOffensiveImages: %v", err)
    		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    	}
    }
    

    자바

    PubSubController.java 파일이 HTTP 요청을 처리하는 컨트롤러를 정의하고 수신 Pub/Sub 메시지를 사용할 준비를 합니다. 다음과 같이 변경하세요.

    • 새 가져오기 추가
    • 컨트롤러에서 기존 'Hello World' 코드 삭제
    • Pub/Sub 메시지의 유효성을 검사하는 코드 추가
    • 새 이미지 처리 함수를 호출하는 코드 추가

    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import java.util.Base64;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    // PubsubController consumes a Pub/Sub message.
    @RestController
    public class PubSubController {
      @RequestMapping(value = "/", method = RequestMethod.POST)
      public ResponseEntity<String> receiveMessage(@RequestBody Body body) {
        // Get PubSub message from request body.
        Body.Message message = body.getMessage();
        if (message == null) {
          String msg = "Bad Request: invalid Pub/Sub message format";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Decode the Pub/Sub message.
        String pubSubMessage = message.getData();
        JsonObject data;
        try {
          String decodedMessage = new String(Base64.getDecoder().decode(pubSubMessage));
          data = JsonParser.parseString(decodedMessage).getAsJsonObject();
        } catch (Exception e) {
          String msg = "Error: Invalid Pub/Sub message: data property is not valid base64 encoded JSON";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Validate the message is a Cloud Storage event.
        if (data.get("name") == null || data.get("bucket") == null) {
          String msg = "Error: Invalid Cloud Storage notification: expected name and bucket properties";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        try {
          ImageMagick.blurOffensiveImages(data);
        } catch (Exception e) {
          String msg = String.format("Error: Blurring image: %s", e.getMessage());
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return new ResponseEntity<>(HttpStatus.OK);
      }
    }

전체 샘플 다운로드

사용할 전체 이미지 처리 코드 샘플을 검색하려면 다음 안내를 따르세요.

  1. 샘플 앱 저장소를 로컬 머신에 클론합니다.

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

    자바

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

  2. Cloud Run 샘플 코드가 포함된 디렉터리로 변경합니다.

    Node.js

    cd nodejs-docs-samples/run/image-processing/

    Python

    cd python-docs-samples/run/image-processing/

    Go

    cd golang-samples/run/image-processing/

    자바

    cd java-docs-samples/run/image-processing/

코드 제공

코드 제공은 Cloud Build로 컨테이너 이미지를 빌드하고, Container Registry에 컨테이너 이미지를 업로드하고, 컨테이너 이미지를 Cloud Run에 배포하는 세 단계로 구성됩니다.

코드를 제공하려면 다음 안내를 따르세요.

  1. 컨테이너를 빌드하고 Container Registry에 게시합니다.

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 GCP 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 GCP 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    여기서 PROJECT_ID는 GCP 프로젝트 ID이고 pubsub는 서비스에 지정할 이름입니다.

    성공하면 ID, 생성 시간, 이미지 이름이 포함된 성공 메시지가 표시됩니다. 이미지는 Container Registry에 저장되며 원하는 경우 다시 사용할 수 있습니다.

    자바

    이 샘플은 Jib를 사용해서 일반적인 자바 도구로 Docker 이미지를 빌드합니다. Jib는 Dockerfile을 사용하거나 Docker를 설치할 필요 없이 컨테이너 빌드를 최적화합니다. Jib로 자바 컨테이너 빌드에 대해 자세히 알아보세요.

    1. Dockerfile을 사용하여 시스템 패키지가 설치된 기본 이미지를 구성하고 빌드하여 Jib의 기본 이미지를 재정의합니다.

      # Use eclipse-temurin for base image.
      # It's important to use JDK 8u191 or above that has container support enabled.
      # https://hub.docker.com/_/eclipse-temurin/
      # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
      FROM eclipse-temurin:11-jre
      
      # Install Imagemagick into the container image.
      # For more on system packages review the system packages tutorial.
      # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
      RUN set -ex; \
        apt-get -y update; \
        apt-get -y install imagemagick; \
        rm -rf /var/lib/apt/lists/*

      gcloud builds submit --tag gcr.io/PROJECT_ID/imagemagick

      여기서 PROJECT_ID는 GCP 프로젝트 ID입니다.

    2. gcloud 사용자 인증 정보 도우미를 사용하여 Docker가 Container Registry로 내보내도록 승인합니다.

      gcloud auth configure-docker

    3. Jib로 최종 컨테이너를 빌드하고 Container Registry에 게시합니다.

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.1.4</version>
        <configuration>
          <from>
            <image>gcr.io/PROJECT_ID/imagemagick</image>
          </from>
          <to>
            <image>gcr.io/PROJECT_ID/pubsub</image>
          </to>
        </configuration>
      </plugin>
      
      mvn compile jib:build \
        -Dimage=gcr.io/PROJECT_ID/pubsub \
        -Djib.from.image=gcr.io/PROJECT_ID/imagemagick

      여기서 PROJECT_ID는 GCP 프로젝트 ID입니다.

  2. 다음 명령어를 실행하여 Cloud Pub/Sub 사용 가이드에서 사용한 것과 이름이 동일한 서비스를 배포합니다.

    Node.js

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Python

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Go

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    자바

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M --no-allow-unauthenticated

    PROJECT_ID를 GCP 프로젝트 ID로 바꿉니다. pubsub는 컨테이너 이름이고 pubsub-tutorial은 서비스 이름입니다. 컨테이너 이미지는 이전에 gcloud 기본값 설정에서 구성한 서비스 및 리전(Cloud Run)에 배포됩니다.

    BLURRED_BUCKET_NAME을 앞서 만든 Cloud Storage 버킷으로 바꿔 흐리게 처리된 이미지를 수신하여 환경 변수를 설정합니다.

    --no-allow-unauthenticated 플래그는 서비스에 대해 인증되지 않은 액세스를 제한합니다. 서비스를 비공개로 유지하면 Cloud Run의 자동 Pub/Sub 통합을 사용해 요청을 인증할 수 있습니다. 구성 방법에 대한 자세한 내용은 Pub/Sub와 통합을 참조하세요. IAM 기반 인증에 대한 자세한 내용은 액세스 관리를 참조하세요.

    배포가 완료될 때까지 기다립니다. 이 작업은 30초 정도 걸릴 수 있습니다. 성공하면 명령줄에 서비스 URL이 표시됩니다.

Cloud Storage의 알림 사용 설정

파일(객체라고도 함)이 업로드되거나 변경될 때마다 Pub/Sub 주제에 메시지를 게시하도록 Cloud Storage를 구성합니다. 새 파일 업로드가 서비스를 호출할 수 있도록 이전에 만든 주제로 알림을 보냅니다.

gsutil notification create -t myRunTopic -f json gs://INPUT_BUCKET_NAME

gsutil 명령어는 Cloud SDK의 일부로 설치됩니다. myRunTopic은 이전 가이드에서 만든 주제입니다.

INPUT_BUCKET_NAME버킷을 만들 때 사용한 이름으로 바꿉니다.

스토리지 버킷 알림에 대한 자세한 내용은 객체 변경 알림을 참조하세요.

사용해 보기

  1. 살점을 뜯어먹는 좀비와 같은 불쾌감을 주는 이미지를 업로드합니다.

    gsutil cp zombie.jpg gs://INPUT_BUCKET_NAME

    여기서 INPUT_BUCKET_NAME은 이미지를 업로드하기 위해 앞서 만든 Cloud Storage 버킷입니다.

  2. 서비스 로그로 이동합니다.

    1. Google Cloud Console의 Cloud Run 페이지로 이동합니다.
    2. pubsub-tutorial 서비스를 클릭합니다.
    3. 로그 탭을 선택합니다. 로그가 나타나려면 시간이 약간 걸릴 수 있습니다. 즉시 표시되지 않으면 잠시 후 다시 확인하세요.
  3. Blurred image: zombie.png 메시지를 찾습니다.

  4. 앞서 만든 BLURRED_BUCKET_NAME Cloud Storage 버킷에서 흐리게 처리된 이미지를 확인할 수 있습니다. Google Cloud Console의 Cloud Storage 페이지에서 버킷을 찾습니다.

삭제

이 튜토리얼용으로 새 프로젝트를 만든 경우 이 프로젝트를 삭제합니다. 기존 프로젝트를 사용한 경우 이 가이드에 추가된 변경사항은 제외하고 보존하려면 가이드용으로 만든 리소스를 삭제합니다.

프로젝트 삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 가이드에서 만든 프로젝트를 삭제하는 것입니다.

프로젝트를 삭제하려면 다음 안내를 따르세요.

  1. Cloud Console에서 리소스 관리 페이지로 이동합니다.

    리소스 관리로 이동

  2. 프로젝트 목록에서 삭제할 프로젝트를 선택하고 삭제를 클릭합니다.
  3. 대화상자에서 프로젝트 ID를 입력한 후 종료를 클릭하여 프로젝트를 삭제합니다.

가이드 리소스 삭제

  1. 이 가이드에서 배포한 Cloud Run 서비스를 삭제합니다.

    gcloud run services delete SERVICE-NAME

    여기서 SERVICE-NAME은 선택한 서비스 이름입니다.

    Google Cloud Console에서 Cloud Run 서비스를 삭제할 수도 있습니다.

  2. 튜토리얼 설정 중에 추가한 gcloud 기본 리전 구성을 삭제합니다.

     gcloud config unset run/region
    
  3. 프로젝트 구성을 삭제합니다.

     gcloud config unset project
    
  4. 이 가이드에서 만든 다른 Google Cloud 리소스를 삭제합니다.

다음 단계

  • Cloud Storage를 통해 Cloud Run에 데이터를 보관하는 방법 자세히 알아보기
  • Cloud Vision API를 사용하여 유해성 콘텐츠 이외의 항목을 감지하는 방법 알아보기
  • Google Cloud에 대한 참조 아키텍처, 다이어그램, 튜토리얼, 권장사항 살펴보기. Cloud 아키텍처 센터 살펴보기