[Next.js] AWS Cognito 사용해 파일 업로드 트래픽 최적화

Peter·2022년 8월 5일
3

Next

목록 보기
1/1

파일 업로드 패턴

어플리케이션을 개발하는 과정에서 스태틱한 파일을 처리하는 로직을 구현하는 것은 프론트엔드, 백엔드 입장에서 생각보다 매우 귀찮은 일입니다. 파일의 종류와 크기에 따라서 여러가지 예외의 경우를 처리해야 하는 경우도 있고 사용자 입장에서 진행되는 시간이 지루하게 느낄 수 있으므로 클라이언트에서 여러가지 효과를 통해서 사용성을 높여주기도 합니다.

이번 글은 얼마전에 끝난 프로젝트를 진행하면서 고안한, 스태틱한 파일들을 전송하는 패턴을 변경, 최적화하게된 내용을 공유하기 위한 글입니다. 진행한 프로젝트에서 제가 만든 어플리케이션은 한개에 10Mb 가 넘는 사진들이 200장 정도, 별도의 처리과정을 거쳐나온 2~8Gb 정도의 큰 파일들을 업로드하고 다운로드하는 작업이 빈번한 어플리케이션으로 사용자의 편의성과 요금절약(?)을 위해 최적화 패턴을 도입해야할 필요성이 있었습니다.

기존 패턴

  1. 이번 프로젝트 전에 사용했던 패턴은 위 사진과 같이 client에서 서버로 파일을 보내면
  2. 서버에서 s3같은 파일들을 위해 별도로 마련한 공간에 전송을 하고
  3. 응답받은 주소를 데이터베이스에 다른 정보들과 함께 저장하는 패턴입니다

RESTFUL API 로 전송

프론트엔드

const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData();
    formData.append("file", file);
    formData.append("data", { 
      name: "data-file" 
    });

    try {
      const res = await axios.post("/api/upload", formData, {
        headers: {
          "Content-Type": "multipart/form-data",
        },
      });
    } catch (err) {
    	console.log("Error: ", err);
    }
  };
  • 클라이언트에서 파일을 전송하기 위해서 new FormData를 사용해 post 메소드로 전송할 형태로 만들어줍니다
  • 정해진 api에 post 메소드로 formData를 담아 전송합니다

백엔드

// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set the region 
AWS.config.update({region: 'REGION'});

// Create S3 service object
var s3 = new AWS.S3({apiVersion: '2006-03-01'});

// call S3 to retrieve upload file to specified bucket
var uploadParams = {Bucket: process.argv[2], Key: '', Body: ''};
var file = process.argv[3];

// Configure the file stream and obtain the upload parameters
var fs = require('fs');
var fileStream = fs.createReadStream(file);
fileStream.on('error', function(err) {
  console.log('File Error', err);
});
uploadParams.Body = fileStream;
var path = require('path');
uploadParams.Key = path.basename(file);

// call S3 to retrieve upload file to specified bucket
s3.upload (uploadParams, function (err, data) {
  if (err) {
    console.log("Error", err);
  } if (data) {
    console.log("Upload Success", data.Location);
  }
});
// 출처: aws-documentation https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v2/developer-guide/s3-example-creating-buckets.html
  • 클라이언트에서 전송받은 파일을 fs를 사용해 스트림화하고 s3에 업로드하게 됩니다
  • 업로드가 완료되면 클라이언트에 성공인지 실패인지의 응답도 하게 됩니다

기존 패턴이 가진 문제점

  • 위 사진을 보시면 파일 전송이 클라이언트 -> 서버, 서버 -> 스토리지로 두번 트래픽이 발생합니다.
  • 보통 4Gb 정도되는 파일을 전송하는데 8Gb의 트래픽이 발생하고 트래픽은 시간과 비용에 직결된 문제입니다.
  • 사용자는 서버에서 응답을 받기전까지 클라이언트에서 대기해야 합니다.

최적화 패턴

  • 위 문제점을 해결하기 위해선 위 사진같이 클라이언트에서 S3로 직접 파일을 전송하면 됩니다.
  • 버킷의 디렉토리 또한 사용자가 직접 설정하므로 S3로 직접 업로드를 하고 완료가 되면 디렉토리 주소와 기타 문자열 데이터와 함께 서버로 전송시켜주면 됩니다.
  • 위와 같은 패턴을 사용한다면 사용자가 업로드하기 원하는 파일의 크기 만큼의 트래픽이 발생하기 때문에 소요시간과 트래픽을 절반정도로 줄일 수 있습니다.

s3와 직접 통신

  • 클라이언트에서 s3와 통신하기 위해선 aws에서 제공하는 s3 sdk를 이용해야 합니다.
  • 보통 아무나 s3에 업로드 하는 것을 방지하기 위해 권한을 설정하는데 이 권한을 가지고 s3에 접근하기 위해선 IAM을 통해 s3접근 권한이 부여된 Credential을 sdk에 설정하면 s3와 직접 통신이 가능해집니다.

그렇지만 이 패턴을 사용할 수 없는 이유

  • 권한이 설정된 s3 를 사용하기 위해선 sdk에 크레덴셜을 설정해줘야 하는데 accessKey, accessSecretKey 정보를 클라이언트안에서 작성해줘야 합니다.
  • .env에 값을 설정하더라도 환경변수는 말그대로 환경에 따라 달라지는 변수에 대응하는 것이지 보안을 지켜주는 것은 아닙니다.
  • 위 사진은 브라우저 Sources에서 캡쳐한 결과로 우리가 작성해서 넣은 크레덴셜이 그대로 노출되어 있는 것을 확인할 수 있습니다.
  • 악의적 사용자는 이 키를 탈취해 클라이언트 밖에서 권한이 설정된 s3에 접근할 수 있게됩니다.
  • 보안상 이유로 인해 최적화할 수 있는 패턴을 사용할 수가 없었습니다.

AWS Cognito

IAM

AWS Identity and Access Management(IAM)은 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹 서비스입니다. IAM을 사용하여 리소스를 사용하도록 인증(로그인) 및 권한 부여(권한 있음)된 대상을 제어합니다.

  • 위에서 권한이 설정된 s3에 접근하기 위해 필요하다고 언급했던 IAM은 사실 aws에 속해있는 리소스에 엑세스하기 위한 '증명서' 입니다.
  • aws 관리자 계정을 통해 선택적으로 여러 각 리소스에 접근할 수 있도록 IAM을 만드는 것이 가능합니다.
  • IAM 자체가 accessKey와 accessSecretKey를 가지고 있는 것이 아닙니다.
  • IAM은 계정에 할당이 가능해 aws 관리자 아래로 생성된 계정에 부여하곤 합니다.
  • S3 접근권한을 담은 IAM 을 특정 계정에 할당하게 되고 그 계정에 대한 accessKey와 accessSecretKey를 sdk에 입력해서 사용하게 되는 것입니다.

Cognito?

Amazon Cognito를 사용하면 웹과 모바일 앱에 빠르고 손쉽게 사용자 가입, 로그인 및 액세스 제어 기능을 추가할 수 있습니다. Amazon Cognito에서는 수백만의 사용자로 확장할 수 있고, Apple, Facebook, Google 및 Amazon과 같은 소셜 자격 증명 공급자와 엔터프라이즈 자격 증명 공급자(SAML 2.0 및 OpenID Connect 사용)를 통한 로그인을 지원합니다. - aws cognito 소개글

  • 코그니토는 aws에서 제공하는 사용자 자격증명 서비스입니다.
  • 회원가입, 로그인관련 기능들을 손쉽게 설정이 가능합니다. => 토큰처리, 로그인유지 등의 번거로운 일을 해결하기 위한 서비스입니다.

Cognito가 주는 Credential

  • 우리가 주목해야할 코그니토의 기능중 하나는 "AWS 리소스에 대한 엑세스 제어" 입니다
  • S3나 기타 aws 리소스에 접근 가능한 IAM을 cognito는 유저들에게 할당이 가능합니다
  • 코그니토는 user pool과 identity pool 두가지 pool을 가지고 있으며 두가지 기능을 활용해서 로그인한 사용자에게, 보안이 보장되면서 권한이 설정된 S3에 접근할 수 있는 크레덴셜을 제공합니다.

User Pool

A user pool is a user directory in Amazon Cognito. With a user pool, your users can sign in to your web or mobile app through Amazon Cognito. Your users can also sign in through social identity providers like Google, Facebook, Amazon, or Apple, and through SAML identity providers. Whether your users sign in directly or through a third party, all members of the user pool have a directory profile that you can access through a Software Development Kit (SDK).

  • user pool은 전통적인 아이디, 패스워드 로그인 방법부터 Oauth 로그인까지 광범위하게 로그인 로직을 대신 처리해줍니다.
  • 코그니토를 통해 회원가입/로그인한 사용자는 user pool에 담겨 있습니다.
  • user pool에선 2중보안, 비밀번호 패턴 등의 회원가입/로그인에 대한 세부적인 설정이 가능합니다.
  • 개인적으로는 빈번하게 설정해야하는 값이 바뀌는 oauth를 우리대신 대응해주니 너무 간편했습니다.

Identity Pool

Amazon Cognito identity pools (federated identities) enable you to create unique identities for your users and federate them with identity providers. With an identity pool, you can obtain temporary, limited-privilege AWS credentials to access other AWS services. Amazon Cognito identity pools support the following identity providers:

  • Public providers: Login with Amazon (identity pools), Facebook (identity pools), Google (identity pools), Sign in with Apple (identity pools).
  • Amazon Cognito user pools
  • Open ID Connect providers (identity pools)
  • SAML identity providers (identity pools)
  • Developer authenticated identities (identity pools)
  • Identity Pool은 바라보고 있는 User Pool에 속한각 유저가 독립적인 아이덴티티를 가지게 해서 정해진 aws credential을 할당해줍니다.
  • Identity Pool에서 주는 임시 크레덴셜을 통해 권한이 설정된 s3에 보안상 안전하게 접근이 가능하게 됩니다.

과정

유저가 로그인 과정을 거쳐 코그니토로부터 크레덴셜을 가져오게 되는 로직을 간단하게 보여드리겠습니다.
코그니토는 소셜로그인 기능을 제공하지만 여기선 편의상 아이디/패스워드 로그인 방식을 전제로 진행했습니다.

1. 코그니토에 로그인

  • 로그인을 요청할 ClientId를 특정해 authParameters에 아이디와 비밀번호를 담아 요청합니다.

2. 코그니토로부터 토큰 응답

  • 코그니토에 로그인 요청한 정보가 올바르다면 userpool은 요청에 대해 토큰들을 응답해줍니다
  • 여기서 토큰은 크레덴셜 정보가 아니라 userpool 에 해당하는 사용자임을 증명하는 토큰들로써 로그인 유지 기능등을 수행하기 위한 토큰입니다
    • AccessToken => 일반적인 토큰으로 사용자 식별을 위한 토큰
    • RefreshToken => AccessToken이 만료되면 RefreshToken을 통해 새로운 AccessToken을 받아옵니다
    • IdToken => 이 토큰이 아이덴티티에서 크레덴셜을 가져오기 위한 토큰입니다.

3. Identity Pool에 요청

  • Identity pool에, 바라보고 있는 Identity pool 아이디와 id 토큰 값을 담아 요청합니다.

4. Identity를 응답

  • Identity Pool은 토큰값을 보고 user를 특정해 Identity 값을 만든 뒤 Identity 값을 보내줍니다
  • Identity 값이란 개인에 특정된 하나뿐인 값입니다. (사원증? 정도로 보면 좋겠네요)

5. Identity 값과 토큰을 가지고 요청

  • Identity Pool로부터 받은 Identity 값과 토큰을 다시 요청합니다

6. 드디어 Credential을 응답

  • 그럼 Identity Pool에서 Credential을 보내줍니다
  • 여기서 크레덴셜은 일반적으로 aws 계정 크레덴셜과는 다르게 Expiration 값이 존재합니다. 만료기간이 특정되어 있는 임시 값이라는걸 알 수 있습니다
  • 이 크레덴셜엔 미리 지정해둔 권한이 있습니다 여기서는 S3 엑세스 권한입니다.

7. 응답받은 크레덴셜로 s3 sdk에 요청

  • 코그니토로부터 받은 크레덴셜로 sdk를 설정하는 로직을 구현해주면 로그인 이후에 s3에 접근하는것이 가능해집니다.

결론

  • 스태틱한 파일을 어떻게 처리 할것인지에 대한 고민은 현재 진행중인것 같습니다
  • 위와 같은 패턴의 한계점은 s3를 사용하지만 코그니토를 사용하지 않으면 구현이 불가능한 내용입니다 (코그니토는 50,000명 이상부터 유료)
  • 조사해본 결과 Cloudflare 같은 경우 스토리지에 요청을 보내면 업로드할 주소를 클라이언트에 리턴해 직접 업로드하도록 하는 방법을 채택했습니다.
  • aws cloud 환경을 사용하는 프로덕션이라면 위와 같은 패턴을 사용해 파일 전송에 따른 트래픽을 절반정도로 감축시킬 수 있습니다.

appendix

GraphQL

  • 그래프큐엘을 사용하지 않는 큰 이유중에 하나는 파일 처리에 관한 내용 때문입니다
  • 불가능하지 않지만 복잡한 처리를 해줘야 하기 때문에 다른 좋은 기능을 가지고 있더라도 사용하지 않는 다는 글들을 많이 볼 수 있습니다
  • 위와 같은 패턴을 사용한다면 GraphQL에 파일처리를 구현하지 않습니다. 따라서 그래프큐엘의 큰 담점을 해소할 수 있는 부분이기도 합니다.
  • 회사에서도 이 점을 염두해두고 GraphQL 전환을 고려하고 있습니다.
profile
컴퓨터가 좋아

2개의 댓글

comment-user-thumbnail
2022년 8월 13일

S3업로드 시 presigned URL 을 고려 안한 이유가 있을까요?

1개의 답글