Cloud Run의 32MB 요청 제한을 극복하는 방법

Cloud Run은 Google Cloud에서 제공하는 멋진 서버리스 제품으로 컨테이너화된 웹 서비스를 실행하는 데 적합합니다. 자동 크기 조정, 롤링 업데이트, 자동 다시 시작, 0으로 크기 조정과 같은 많은 이점을 제공합니다. 클러스터를 프로비저닝하고 관리하는 번거로움 없이 이 모든 것이 가능합니다!

예를 들어 다음과 같은 디자인의 Python Flask Rest API를 호스트하기 위해 이 제품을 확실히 선택할 것입니다.


1- HTTP POST로 REST 끝점에 데이터 파일 업로드
2- 파일 처리
3- 클라이언트 라이브러리를 사용하여 BigQuery에 데이터 삽입

완벽하게 괜찮습니다 ... 32MB보다 큰 데이터 파일을 처리하고 싶지 않다면!

실제로 Cloud Run에서는 그렇게 큰 파일을 업로드할 수 없습니다. 대신 다음과 같은 오류 메시지가 표시됩니다.

413: Request entity too large



축하합니다. 열심히 하셨습니다size limit of Cloud Run inbound requests .
하지만 걱정하지 마세요. 아래의 개선된 디자인을 적용하면 서비스에 Cloud Run을 계속 사용할 수 있습니다.

Cloud Storage, 서명된 URL 및 PubSub 알림으로 디자인 개선



제한 사항을 해결하기 위해 Cloud Storage signed urls을 기반으로 솔루션을 설계할 수 있습니다.



이번에는 파일이 REST 끝점에 직접 업로드되지 않고 대신 클라우드 저장소에 업로드되어 32MB 제한을 우회합니다.

이 프로세스의 단점은 클라이언트가 하나가 아닌 두 개의 요청을 해야 한다는 것입니다. 따라서 완전히 새로운 순서는 다음과 같습니다.

1- 클라이언트가 업로드할 서명된 URL을 요청합니다.
2- Cloud Storage 클라이언트를 사용하는 웹 서비스는 서명된 URL을 생성하여 클라이언트에 반환합니다.
3- 클라이언트가 파일을 Cloud Storage 버킷에 직접 업로드합니다(서명된 URL에 HTTP PUT).
4- 파일 업로드가 끝나면 알림OBJECT_FINALIZE이 PubSub로 전송됩니다.
5- 알림이 구독을 통해 Cloud Run의 웹 서비스로 다시 푸시됩니다.
6- 웹 서비스는 파일을 다운로드하여 알림에 반응합니다.
7- 그런 다음 웹 서비스는 원래 디자인에서와 똑같은 방식으로 파일을 처리할 수 있습니다.
8- 마찬가지로 데이터가 BigQuery에 삽입됩니다.

이 디자인은 완전한 서버리스이며 단일 장애 지점 없이 깔끔하게 확장됩니다. 이제 구현 방법을 자세히 살펴 보겠습니다.

Cloud Run에서 서명된 URL 만들기



잡았다! 서명된 URL을 생성하려면 Cloud Run 서비스에 역할roles/iam.serviceAccountTokenCreator이 있어야 합니다. 실제로 문서화되지 않았으며 승인하지 않으면 더 많은 정보 없이 HTTP 오류 403이 발생합니다.

this blog post by Evan Peterson에서 제공하는 이 Python 코드는 로컬에서 비공개 키 파일을 요구하지 않고 Cloud Run 웹 서비스의 기본 서비스 계정을 사용하여 서명된 URL을 생성하는 방법을 보여줍니다(보안상의 이유로 절대 안 됩니다!).

from typing import Optional
from datetime import timedelta

from google import auth
from google.auth.transport import requests
from google.cloud.storage import Client


def make_signed_upload_url(
    bucket: str,
    blob: str,
    *,
    exp: Optional[timedelta] = None,
    content_type="application/octet-stream",
    min_size=1,
    max_size=int(1e6)
):
    """
    Compute a GCS signed upload URL without needing a private key file.
    Can only be called when a service account is used as the application
    default credentials, and when that service account has the proper IAM
    roles, like `roles/storage.objectCreator` for the bucket, and
    `roles/iam.serviceAccountTokenCreator`.
    Source: https://stackoverflow.com/a/64245028

    Parameters
    ----------
    bucket : str
        Name of the GCS bucket the signed URL will reference.
    blob : str
        Name of the GCS blob (in `bucket`) the signed URL will reference.
    exp : timedelta, optional
        Time from now when the signed url will expire.
    content_type : str, optional
        The required mime type of the data that is uploaded to the generated
        signed url.
    min_size : int, optional
        The minimum size the uploaded file can be, in bytes (inclusive).
        If the file is smaller than this, GCS will return a 400 code on upload.
    max_size : int, optional
        The maximum size the uploaded file can be, in bytes (inclusive).
        If the file is larger than this, GCS will return a 400 code on upload.
    """
    if exp is None:
        exp = timedelta(hours=1)
    credentials, project_id = auth.default()
    if credentials.token is None:
        # Perform a refresh request to populate the access token of the
        # current credentials.
        credentials.refresh(requests.Request())
    client = Client()
    bucket = client.get_bucket(bucket)
    blob = bucket.blob(blob)
    return blob.generate_signed_url(
        version="v4",
        expiration=exp,
        service_account_email=credentials.service_account_email,
        access_token=credentials.token,
        method="PUT",
        content_type=content_type,
        headers={"X-Goog-Content-Length-Range": f"{min_size},{max_size}"}
    )


테라포밍



코드형 인프라 없이 클라우드를 수행하는 강력한 방법은 없으며 Terraform은 클라우드 리소스를 관리하는 완벽한 도구입니다.



다음은 이 설계를 배포하기 위한 Terraform 조각입니다.

# Resources to handle big data files (>32 Mb)
# These files are uploaded to a special bucket with notifications

provider "google-beta" {
  project = <your GCP project name>
}

data "google_project" "default" {
  provider = google-beta
}

resource "google_storage_bucket" "bigframes_bucket" {
  project  = <your GCP project name>
  name     = "upload-big-files"
  location = "EU"

  cors {
    origin = ["*"]
    method = ["*"]
    response_header = [
      "Content-Type",
      "Access-Control-Allow-Origin",
      "X-Goog-Content-Length-Range"
    ]
    max_age_seconds = 3600
  }
}

resource "google_service_account" "default" {
  provider     = google-beta
  account_id   = "sa-webservice"
}

resource "google_storage_bucket_iam_member" "bigframes_admin" {
  bucket = google_storage_bucket.bigframes_bucket.name
  role   = "roles/storage.admin"
  member = "serviceAccount:${google_service_account.default.email}"
}

# required to generate a signed url
resource "google_service_account_iam_member" "tokencreator" {
  provider           = google-beta
  service_account_id = google_service_account.default.name
  role               = "roles/iam.serviceAccountTokenCreator"
  member             = "serviceAccount:${google_service_account.default.email}"
}

# upload topic for notifications
resource "google_pubsub_topic" "bigframes_topic" {
  provider = google-beta
  name     = "topic-bigframes"
}

# upload deadletter topic for failed notifications
resource "google_pubsub_topic" "bigframes_topic_deadletter" {
  provider = google-beta
  name     = "topic-bigframesdeadletter"
}

# add frame upload notifications on the bucket
resource "google_storage_notification" "bigframes_notification" {
  provider       = google-beta
  bucket         = google_storage_bucket.bigframes_bucket.name
  payload_format = "JSON_API_V1"
  topic          = google_pubsub_topic.bigframes_topic.id
  event_types    = ["OBJECT_FINALIZE"]
  depends_on = [google_pubsub_topic_iam_binding.bigframes_binding]
}

# required for storage notifications
# seriously, Google, this should be by default !
resource "google_pubsub_topic_iam_binding" "bigframes_binding" {
  topic   = google_pubsub_topic.bigframes_topic.id
  role    = "roles/pubsub.publisher"
  members = ["serviceAccount:service-${data.google_project.default.number}@gs-project-accounts.iam.gserviceaccount.com"]
}

# frame upload main sub
resource "google_pubsub_subscription" "bigframes_sub" {
  provider = google-beta
  name     = "sub-bigframes"
  topic    = google_pubsub_topic.bigframes_topic.id

  push_config {
    push_endpoint = <URL where pushed notification are POST-ed>
  }
  dead_letter_policy {
    dead_letter_topic = google_pubsub_topic.bigframes_topic_deadletter.id
  }
}

# frame upload deadletter subscription
resource "google_pubsub_subscription" "bigframes_sub_deadletter" {
  provider             = google-beta
  name                 = "sub-bigframesdeadletter"
  topic                = google_pubsub_topic.bigframes_topic_deadletter.id
  ack_deadline_seconds = 600

  push_config {
    push_endpoint = <URL where pushed notification are POST-ed>
  }
}



그냥 terraform deploy 그것!

업로드 방법



마지막 문제: 서명된 URL을 사용하여 Cloud Storage에 업로드하려면 PUT 요청에 추가 헤더를 설정해야 합니다.X-Goog-Content-Length-Range: <min size>,<max size>여기서 min sizemax size는 위의 min_size 메서드의 max_sizemake_signed_upload_url()와 일치합니다.

결론



이 디자인을 경험해 보셨습니까? 어떻게 개선하시겠습니까? 댓글로 알려주세요.


Excalidraw 및 GCP 아이콘 라이브러리로 만든 설계 스키마
joel herzogUnsplash님의 표지 사진

좋은 웹페이지 즐겨찾기