Presigned Url을 활용한 S3 업로드/다운로드

1/28/2021

최근 회사 홈페이지에 비로그인한 불특정 다수로 부터 무언가를 제출 받아 특정 이메일로 포워딩 해주는 기능을 구현해야 할 일이 생겼었다. 처음 생각은 프론트에서 파일을 업로드하면 백엔드에서 Nodemailer를 이용해서 포워딩 해주는 방식이었다.

그런데 serverless-offline의 문제인지 백엔드로 넘어온 binary파일이 계속 이상하게 오염되는 문제가 있었다.

관련 이슈

결국 프론트에서 직접 s3에 파일을 업로드 하고 s3에 object가 추가로 생성될 때마다 람다를 통해 지정된 사용자에게 메일로 포워딩 하는 방식을 택하게 되었다.

버킷을 퍼블릭하게 열어버리면 별다른 작업이 필요 없지만 당연히 이렇게 문을 확짝 열어둘 수는 없는 노릇이니 업로드, 다운로드에 presigned-url을 이용하기로 했다.

presigned url을 이용한 절차를 간단히 써보면 아래와 같다.

  1. 클라이언트에서 업로드할 파일의 정보를 담아 백엔드에 s3 PutObject를 위한 signed url을 요청한다.
  2. 받아온 url로 파일으르 담아 post요청을 보낸다.
  3. S3 Object가 생성될 때마다 실행될 람다를 통해 메일을 포워드한다.

Signed Url 생성

우선 lambda를 이용하여 signed url을 생성하는 코드를 작성해보자.

import AWS from 'aws-sdk';
export async function getSignedUrl(res, req, next) {
const s3 = new AWS.S3({ region: 'my-region' });
const { author, title, contentType } = req.body;
const key = `${author}-${title}`;
try {
const signedUrl = s3.getSignedUrl("PutObject", {
Bucket: "BucketName", // Bucket name
Key: `${key}.pdf`, // S3에 저장될 object의 key
ContentType: contentType, // content type
Metadata: {
author,
title,
} // s3에 저장될 때 실행될 lambda에서 사용하기 위한 metadata
});
res.status(200).json(signedUrl);
} catch(e) {
next(e);
}
}

signed url을 요청할 때 post body에 필요한 데이터들을 담아서 보내면 람다에서 해당 데이터들을 조합하여 원하는 형태로 s3 object의 key를 생성한다. 생성된 url을 다시 클라이언트로 보내준다.

signed url을 생성할 때 옵션의 Expires property를 통해 url의 유효기간을 설정할 수 있다.

Metadata는 해당 object의 메타데이터를 저장하기 위한 값들인데 메일로 포워딩 할 때 실제 메일 본문에 포함되어야 할 데이터들을 메타데이터 형태로 받기 위해 설정해 두었다. 해당 메타데이터 각 key에 대한 실제 값들은 s3에 파일을 업로드 할 때 함께 request header에 담아 보내야 한다.

클라이언트에서 파일 업로드하기

여기서는 PutObject로 url을 설정했기 때문에 반드시 put method로 요청을 보내야 한다. 메타데이터는 request header에 아래와 같이 담아서 보내면 된다.

axios.put(signedUrl, file, {
headers: {
"x-amz-meta-author": "Paul Auster",
"x-amz-meta-title": "Moon Palace"
}
});

x-amz-meta라는 prefix를 붙여서 각각의 메타 데이터에 해당하는 값들을 담아 보내면 된다. url을 생성할 때 지정한 메타데이터와 일치해야 한다.

메일 포워딩

이제 s3 PutObject 이벤트에 람다를 붙여서 메일을 포워드 해보자

import AWS from 'aws-sdk';
export async function forwardMail(event) {
const s3 = new AWS.S3({
region: 'YOUR-REGION',
});
const Bucket = event.Records[0].s3.bucket.name;
const rawKey = event.Records[0].s3.object.key;
const Key = decodeURIComponent(rawKey.replace(/\+/g, " "));
const object = await s3.getObject({ Bucket, Key }).promise();
const { author, title } = object.Metadata;
}

이렇게 하면 object와 metadata를 가져올 수 있는데 이제 메일을 포워드 할 때 메타데이터를 이용해 메일 제목, 본문의 내용을 작성하고 파일을 첨부하여 보내면 된다.

그런데 여기서 귀찮아서 그냥 실제 object의 url을 메일에 삽입해서 보냈는데 이것도 조금 더 안전하게 하자면 getObject에 대한 signedUrl을 생성해서 메일에 담아 보내면 url이 valid한 기간 동안만 다운로드 가능하게 할 수 있다.

하나 삽질을 했던 포인트는 object의 키 값이 다소 이상한 방식으로 인코딩된다는 점인데 우선 공백이 죄다 +로 바뀌어 저장이 된다. 나머지는 일반적인 url encoding방식인데 따라서 decode하기 전에 먼저 +를 다시 공백으로 바꿔줘야 한다.


Powered by Gatsby