풀스택 개발 공부로그

S3 signed URL image upload(Django 서버 부하줄이기) 2

|

클라이언트에서 장고 서버를 거쳐 S3에 이미지를 업로드하게되면 서버에 부하가 많이 걸릴것이라 판단하며 클라이언트에서 직접 S3로 올리고 그 이미지에 대한 메타정보만 장고의 데이터베이스에 저장하려한다.

Scalable User Uploads with Amazon S3 – The Smyth Group

alt text

The key point in the architecture is that uploads never pass through your service. Web or mobile clients simply need to get a signed upload URL from your service and upload files directly to that URL. The upload URL is signed so that unauthorized clients cannot write arbitrary files into your S3 bucket(s) and your service doesn’t have to deal with receiving, processing or storing the uploads.

하지만 인증된 사용자만 업로드할 수 있게 해야하는데 클라이언트에서 처리하는 내용은 쉽게 노출될 수 있다. 이를 보완하기 위한것이 pre-signed URL이다.

{‘url’: ‘https://haeyong-django-vuejs-s3.s3.amazonaws.com/‘,
 ‘fields’: 
{‘key’: ‘html.png’,
  ‘x-amz-algorithm’: ‘AWS4-HMAC-SHA256,
  ‘x-amz-credential’: ‘AKIAU3NMYFIM3LCWVDTX/20191211/ap-northeast-2/s3/aws4_request’,
  ‘x-amz-date’: 20191211T171546Z’,
  ‘policy’: ‘eyJleHBpcmF0aW9uIjogIjIwMjAtMDEtMjJUMDk6MTU6NDZaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiaGFleW9uZy1kamFuZ28tdnVlanMtczMifSwgeyJrZXkiOiAiMTIzNDUuanBnIn0sIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwgeyJ4LWFtei1jcmVkZW50aWFsIjogIkFLSUFVM05NWUZJTTNMQ1dWRFRYLzIwMTkxMjExL2FwLW5vcnRoZWFzdC0yL3MzL2F3czRfcmVxdWVzdCJ9LCB7IngtYW16LWRhdGUiOiAiMjAxOTEyMTFUMTcxNTQ2WiJ9XX0=‘,
  ‘x-amz-signature’: 53c371fc4a462f2fce4251ca742d62df5c06ef437b845baccb4f47b67a6d4bdf’}}

이런 response를 받는다.pre-signed URL을 Django 서버에서 생성한뒤 response를 받아

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <!-- Copy the 'url' value returned by S3Client.generate_presigned_post() -->
    <form action="https://haeyong-django-vuejs-s3.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
      <!-- Copy the 'fields' key:values returned by S3Client.generate_presigned_post() -->
      <input name="key" value="fromhtml.png" />
      <input name="x-amz-algorithm" value="AWS4-HMAC-SHA256" />
      <input name="x-amz-credential" value="AKIAU3NMYFIM3LCWVDTX/20191213/ap-northeast-2/s3/aws4_request" />
      <input name="x-amz-date" value="20191213T071320Z" />
      <input name="AWSAccessKeyId" value="AKIAU3NMYFIM3LCWVDTX" />
      <input name="policy" value="eyJleHBpcmF0aW9uIjogIjIwMTktMTItMTdUMTE6MTM6MjBaIiwgImNvbmRpdGlvbnMiOiBbeyJidWNrZXQiOiAiaGFleW9uZy1kamFuZ28tdnVlanMtczMifSwgeyJrZXkiOiAiZnJvbWh0bWwucG5nIn0sIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwgeyJ4LWFtei1jcmVkZW50aWFsIjogIkFLSUFVM05NWUZJTTNMQ1dWRFRYLzIwMTkxMjEzL2FwLW5vcnRoZWFzdC0yL3MzL2F3czRfcmVxdWVzdCJ9LCB7IngtYW16LWRhdGUiOiAiMjAxOTEyMTNUMDcxMzIwWiJ9XX0=" />
      <input name="x-amz-signature" value="4cd8084d6ae4a8dec0830e842c820fadea56fc55e397dce98b08b831ad119cee" />
    File:
      <input type="file"   name="file" /> <br />
      <input type="submit" name="submit" value="Upload to Amazon S3" />
    </form>
  </body>
</html>

이렇게 하니까 다음과 같은 오류가 났다.

<Error>
<link type="text/css" id="dark-mode" rel="stylesheet" href=""/>
<style type="text/css" id="dark-mode-custom-style"/>
<Code>InvalidRequest</Code>
<Message>
The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.
</Message>
<RequestId>1F41C19BB6E65F3D</RequestId>
<HostId>
dT2dfVAQtEz97HGjOTGAqbgaEkaveDv6aIk+Ho5sxD5ynygEZS8grYUBDxx3GcAIzzcyd/enCes=
</HostId>
</Error>

먼저 요청이 제대로 되지 않는 이유에 대한 가설을 2개를 세웠다.

  1. HTTP요청 형식이 잘못됐을 수 있다.
    Status Code:400 Bad Request
    
  2. 리턴된 pre-signed URL 값에 문제가 있을 수 있다.
<Message>
The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.
</Message>

해결방법

1번이 문제인 경우 AWS API 요청에 서명 - AWS 일반 참조 그래서 AWS API요청에 서명하는 방식을 찾아봤다. HTTP로 AWS에 요청을 전송할 때 AWS에서 보낸 사람을 식별하기 위해 요청에 서명을 한다. HTTP요청을 수동으로 생성하는 경우에만 HTTP 요청에 서명하는 방법을 알 필요가 있다. AWS CLI나 SKD를 사용하면 액세스 키를 사용하면 요청에 자동으로 서명을 한다. 어차피 Javscript로 보낼것이기 때문에 AWS javascript SDK를 사용한다. Amazon S3 클라이언트 측 암호화를 위한 AWS SDK 지원 - AWS 일반 참조

2번이 문제인 경우 pre-signed URL을 생성하는 과정을 다시 살펴본다. AWS SDK, CLI 및 Explorer 사용 - Amazon Simple Storage Service AWS에서 API로 통신할 때 인증하는 과정에서 서명을 해야하는데 이 서명에도 버전이 있다. 2버젼과 4버젼이 있는데 2버젼은 사용이 중지되어가고 있다. 그렇기 때문에 4버전을 사용해야한다. 필자는 파이썬을 사용했으므로 Python-Boto SDK에 대한 문서를 찾아봤다.

기본 config 파일인 boto 파일에 다음을 지정해야한다.

[s3] use-sigv4 = True

하지만 이것도 HTML을 통한 HTTP에는 효과가 없다.

결론 : 1번문제의 해결방법인 Javacript SDK를 사용하겠다.

Amplify JavaScript

Getting Started

S3 signed URL image upload(Django 서버 부하줄이기) 1

|

image upload flow 1

alt text

파일을 나의 웹 서버에 올리고 서버에서 다시 S3로 파일을 전송하여 저장하도록 한다면 서버에 부하가 걸릴것이다. 웹 클라이언트에서 바로 S3로 올리면 해결될거같다.

하지만 아무나 올릴 수 있으면 누군가 이를 악용하여 개인 저장소처럼 사용할 수 있으니 인증받은 사용자만 이미지를 업로드할 수 있어야한다. 인증받은 사용자는 제한된 범위안에서 저장소에 이미지를 업로드할 수 있다.

이미지를 효율적으로 업로드하기 위해 여러 블로그를 찾다가 좋은글을 발견했고 여기서 얻은 정보들을 토대로 공부를 했다. Software Architecture — Image Uploading - Joseph Gefroh - Medium

Image Metadata Record

We store the metadata of the image record in our database because we have to track it. There’s no sense uploading an image without a way to retrieve or manage it later. What use is uploading a photo intended to be used as a user avatar if we have no way to associate with the user record in question?

Why don’t we store the image URL? Storing the image URL is fragile, and the link is susceptible to breaking if the URL ever changes for any reason. Storing the metadata needed to construct the URL is a lot more robust — we can easily change things like the domain name, and build the appropriate URL without having any downtime.

이미지 파일을 업로드 하는 프로세스

  • Step 1: Client request an upload URL from the server (REQUEST)
  • Step 2: Client uploads the image data to the upload URL (UPLOAD)
  • Step 3: Client tells the server the upload is completed (CONFIRM)
  • Step 4: Server processes image in background (PROCESS)
  • Step 5: Client checks image processing status (CHECK)
  • Step 6: Server is done processing image, notifies client (FINALIZE)

Step 1: Client request an upload URL from the server (REQUEST) 바로 업로드하지 않고 업로드 url을 요청하는 이유는 서버에 직접적으로 요청하지 않기 위함이다. 서버는 URL을 만들어내는데 이것은 시간 제한이 걸려있고, 감시되고, 권한이 부여된것이다. 또한 서버가 제공한 URL은 3rd-party 서비스에 이미지를 업로드할 권한을 가진 쿼리 파라미터를 가지고있다.

권한에 대한 검증을 수행하고 나서 서버는 업로드할 이미지에 대한 record를 만들어야 하는데 그 안에는 다음과 같은 정보들이 포함되어야 한다. :

  • the name of the file
  • the type of the file
  • the url of the file
  • the status( requested, uploaded, processed)
  • the associations of the image (user, campaign)
  • the kind of association(banner, avatar)
  • write token, read token
  • any other audit data

Step 2: Client uploads the image data to the upload URL (UPLOAD)

URL에 이미지를 업로드한다.

Step 3: Client tells the server the upload is completed (CONFIRM)

이미지 업로드가 완료됐다는것을 서버에 이전에 받은 토큰과함께 전송한다.

Step 4: Server processes image in background (PROCESS)

토큰에 대한 유효성을 검사하고 업로드 요청을 확인한다. 무결성 검사와 최적화를 진행한다.

Step 5: Client checks image processing status (CHECK) Processing images takes some time, and you don’t want the client blocking a request. The client should check back occasionally to see if the processing is done. 이미지를 처리하는데 시간이 좀 걸린다. you don’t want the client blocking a request. 뭔소리지이게. 클라이언트를 이미지 처리가 끝났는지 주기적으로 체크해야한다.

Step 6: Server is done processing image, notifies client (FINALIZE) 업로드가 완료되고난 뒤 이미지 URL을 반환한다. 이미지가 보호되고 있다면 이미지에 접근하는 URL은 나의 Server를 가리킨다. 그리고 클라이언트의 어떤 요청에 대해서도 read token을 제공해야한다.


10 Dec 2019 2:10 PM #programming/upload

CORS

|

CORS

CORS 란? · juicylog HTTP요청은 기본적으로 Cross-site http requests가 가능하다. 하지만 자바스크립트(XMLHttpRequests)로 다른 웹페이지에 접근할 때는 Same Origin Policy로 인해 요청이 불가능하다.

즉, 자바스크립트 내에서 발생하는 요청은 프로토콜, 호스트, 포트가 같아야 요청이 가능하다. 같은 서버에 있는 주소로만 ajax요청이 가능하다.

하지만 외부 호출이 잦은 서비스가 많이 생기면서 오히려 불편한 정책이 되었다. 그래서 웹 브라우저에서 외부 도메인 서버와 통신하기 위한 방식을 표준화한 스펙이 CORS이다. 서버와 클라이언트가 헤더의 규칙으로 요청이나 응답을 어떻게 반응할지 결정하는 스펙이다.

참고

XML HttpRequest는 HTTP를 통해 쉽게 데이터를 주고받는 오브젝트를 제공한다. Ajax로 실행되는 HTTP통신도 XML HttpRequest 규격을 이용한다.

Ajax Asynchronous JavaScript and XML 원래 의미는 비동기적으로 Javascript와 XML을 이용하여 정보를 교환하는 방식 하지만 꼭 XML이 아니라도 json, yaml, text 등 모든 형태의 정보를 포함

HTTP CORS(Cross Origin Resource Sharing)란? - armadillo’s blog CORS는 다음과 같은 4개의 타입으로 구분된다. 브라우저가 요청 내용을 분석한 뒤 한가지 방식을 선택해 서버에 요청하기 때문에 개발자는 목적에 맞는 방식을 선택하고 조건에 맞춰 코딩해야 한다.

DNS

|

도메인의 구조

blog.example.com

  • blog : sub
  • example : second-level
  • com : top-level 각 레벨별로 전담하는 서버가 따로있다. top level의 서버는 second-level의 도메인의 서버 주소들을 각각 알고있고 second-level의 서버는 sub 도메인 서버의 주소들을 알고있다.

DNS가 돌아가는 모습은 크게 3가지

  1. 도매인 구매 IP(93.143.232.123)에 도메인 네임을 등록하고 싶다면 도메인을 구입해야한다. 우선 두가지 기관에 대해 알아야 한다.
    • 등록대행자 : 도메인을 구입을 대행 ex) 가비아
    • 등록소 : top level domain( .com .net 등)을 관리하는 기관
  2. 도매인 등록 도메인에 아이피를 연결하기 위해서는 도메인 네임 서버가 필요하다. 등록대행자가 example.com에 대한 정보(도메인 네임과 IP주소)와 네임서버의 주소를 등록소에 저장한다.

  3. 클라이언트가 도메인 주소로 아이피 주소를 알아내기 통신업체에서 도메인 서버의 주소를 알려준다. 아직 그 주소로 가도 도메인 네임의 아이피 주소를 모른다. 하지만 루트 네임서버에게 물어본다. 그러면 top level 도메인을 관리하는 기관의 서버주소를 알려준다. 거기에 가면 example.com의 아이피가 적혀있는 서버의 주소를 알 수 있다.

도메인 서버의 기능 2가지

  1. 도메인에 대한 IP를 기억
  2. 클라이언트가 요청하면 IP를 알려주는 기능

레코드와 CNAME

도메인 이름과 IP를 묶은 정보 한 건을 레코드라고 한다.

  • A : 도메인 주소에 대한 IP 주소
  • CNAME : 원래 도메인 주소에 대한 별명 예를들어 A타입으로 example.com 으로 접속하면 192.0.0.1 로 맵핑해주듯 CNAME은 www.example.com 를 example.com 로 맵핑해준다.

cname이 있음으로해서 생기는 장점

여러 도메인의 주소가 하나의 아이피를 가리키는 상황이다. 여러 도메인의 주소가 하나의 a타입 레코드를 바라보도록 하면 아이피가 변경될 상황에서 a타입의 아이피만 변경해주면 나머지 도메인주소의 아이피도 한번에 바꾸는 효과가 생긴다.

Docker

|