Home

게시글 업로드를 Presigned URL 기반으로 다시 구현했습니다.

2024/04/27 01:36 KST (2024/04/27 02:04 KST 수정됨)


이 블로그는 게시글을 업로드할 때 글을 26만자 단위로 끊어서 (지난 게시글에서 256KB 단위로 끊는다고 설명했는데, 잘못 설명한 부분입니다. 제가 글자 256*1024개랑 256*1024바이트를 헷갈렸네요.) 여러 차례에 걸쳐 업로드하게끔 짜여 있었습니다.

여기서 말하는 '글'은 마크다운 형식으로 된 포스트 원문을 이야기하는데, 만약 포스트에 이미지가 삽입되어 있다면 이미지도 Data URI 형태로 변환되어 한 문자열에 같이 들어가게 됩니다.

그러다보니 글을 적을 때마다 적게는 1메가, 많게는 10메가까지 되는 문자열을 서버로 보내게 되는데, 안타깝게도 지금까지의 구조로는 일정 용량을 넘어가는 파일을 서버로 보낼 수 없었습니다.

(Cloudfront, API Gateway는 생략한 도표.)

지금까지는 Lambda로 작성된 백엔드 코드가 클라이언트의 권한을 확인한 후 (현재 제 디스코드 계정을 사용해 인증하고 있습니다.) Lambda가 가진 S3 접근 역할을 사용해 S3에 접근하는 방식을 택했는데, 이 때 Lambda에 올릴 수 있는 페이로드가 문제가 됐었습니다.

S3에 파일을 올리는 PUT 요청 자체는 5GB로 비교적 넉넉한 제한을 가지고 있는데, Lambda 함수를 호출할 때 body에 담을 수 있는 페이로드가 6MB로 제한되어 있기 때문입니다.

따지고 보면 굉장히 아까운 일입니다. PUT 요청 자체는 5GB까지도 올릴 수 있는데 그 사이에 낀 Lambda가 6MB씩밖에 전달을 못 해서 꾸역꾸역 파일을 나눠서 보내야 한다니요!

그 과정에서 만약 클라이언트가 직접 S3에 접근할 수 있도록 Lambda에서 임시 AWS 권한같은 걸 발급해줄 수는 없나 하는 생각이 들게 되었습니다.

그럼 Lambda로 직접 큰 용량의 원문을 보낼 필요 없이, Lambda에는 '내가 이 경로에 글을 올리고 싶다'는 사실만 전해주면 그 경로에 해당하는 권한만 뚫린 짧은 유효기간의 인증키를 만들어서 보내주고, 나머지 긴 글 업로드는 그 키로 클라이언트에서 직접 하는 구조를 만들 수 있는 것입니다.

결론부터 말하자면 이미 그런 기능이 있었습니다.

Presigned URL

S3 접근 권한이 있는 사용자는 GeneratePresignedUrl이라는 API를 통해 'Presigned URL'(미리 인증된 URL)이란 걸 생성할 수 있습니다.

S3 접근할 때 쓰는 URL 뒤에 쿼리 파라미터로 X-Amz-Security-Token이라고 해서 짧은 시간동안 사용할 수 있는 인증키가 같이 붙어있는 형태인데, 이 토큰을 사용하면 권한이 없는 사용자도 URL이 유효한 시간동안 GET, PUT 등을 할 수 있는 식입니다.

그렇다면 Lambda는 클라이언트 인증까지만을 담당하고 실제 업로드는 Lambda가 돌려준 presigned URL을 이용해 진행하는 형태를 만들 수 있는 것입니다.

사용법

백엔드에서 presigned URL을 발급하는 법 및 프론트엔드측 js 코드에서 그를 사용하는 법을 제 코드에서 일부 가져와 기록합니다.

백엔드

python boto3 기준 아래와 같이 사용하면 됩니다.

import boto3

# ...

s3_client = boto3.client('s3')

post_url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params={
'Bucket': 'omgadev-blog', # 버킷 이름
'Key': 'privates/{}/index.md'.format(body['id']) # 오브젝트 키
},
ExpiresIn=30
)

# ...

return {
'statusCode': 200,
'body': json.dumps({
'url': post_url
})
}

프론트엔드

발급된 URL을 전달받은 프론트엔드에선 그냥 이 URL로 PUT 요청을 쏘기만 하면 됩니다.

// ...

fetch("/api/upload", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"id": document.getElementById("post_id").value,
"title": document.getElementById("title").value,
// ... 생략
})
}).then(
(response) => response.json()
).then(
// api가 POST에 대한 결과로 .md 파일을 업로드할 수 있는 url을 돌려줍니다.
// 여기에 그대로 PUT 쏘면 됩니다.

(json) => fetch(json['url'], {
method: "PUT",
body: editor.getMarkdown()
})
).then(

// ...

손도 깔끔. 직접 해 보니 잘 돌아가는 것 같아서 기분이 좋습니다 :D

CORS 주의!

아마존 버킷 URL은 당연히 프론트엔드와는 다른 도메인을 쓰고 있을 것이기 때문에 대부분 CORS에 걸립니다. 당황하실 필요 없이 버킷의 CORS 설정에 프론트엔드 도메인을 등록해주면 됩니다.

우선 S3 대시보드의 '권한' 탭에 들어가고

권한 탭 아래에서 CORS 섹션을 찾아 '편집' 버튼을 눌러줍니다. (따로 설정한 적이 없다면 비어 있을 것입니다.)

그리고 json 편집기가 나오면 아래와 같은 형식으로 내용을 채워주시면 됩니다.

[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD",
"PUT"
],
"AllowedOrigins": [
"https://omga.dev"
]
}
]

저는 프론트에서 PUT뿐 아니라 GET도 수행해야 하기 때문에 AllowedMethods에 GET, HEAD, PUT 세 개의 메소드를 넣었습니다. AllowedOrigins에 프론트엔드 주소를 넣으시면 됩니다.