[NodeJS] AWS Serverless 환경에서 Presigned URL로 S3에 파일 업로드 해보기 (CORS, 배포 이슈 수정)

오진서·2022년 7월 18일
5
post-thumbnail

이번 시간에는 NodeJS 환경에서 AWS LambdaAPI Gateway를 사용하여 presignedURL을 가져와보겠습니다.

Serverless?

우선 Serverless가 무엇인지에 대해 간략히 알아보겠습니다.

Serverless는 서버가 없는 것이 아닌, 개발자가 서버에 대한 존재에 대해 신경쓸 필요가 없다는 뜻입니다. 즉, 개발자는 서버에 대한 사양이라든가, 서버 갯수가 적당한가에 대해 신경쓸 필요없이 오로지 서비스 로직에만 집중할 수 있게 됩니다.

AWS의 LambdaAPI Gateway를 이용하면 별도의 서버를 띄우지 않고도 서버 기능을 수행시킴으로써 Serverless 아키텍처를 구현할 수 있게 됩니다.

프로세스를 간략히 설명하자면 수행할 서비스 로직을 Lambda함수에 작성하고, API Gateway로 Route 역할을 수행하도록 합니다.

아래 사진에서 API GatewayLambda로 이루어진 Serverless 아키텍처의 흐름의 예를 간략히 나타내 보았습니다.

위 흐름에서 사용자가 POST 요청으로 파일을 업로드를 한다 가정해보면, Lambda 트리거가 사용되어 PUT 요청으로 S3 Bucket에 객체를 저장하는 이벤트가 발생하게 됩니다. 그리고 또 한번 S3 트리거가 사용되어 Lambda 함수를 호출시킵니다.

이를 활용하면 파일을 S3 버킷에 업로드할 때마다 람다 함수를 통해 이미지 리사이징 작업을 하거나 압축 파일을 처리하는 등의 자동화 작업을 수행시킬 수 있습니다.


Serverless를 왜 적용할까?

그럼 저희 프로젝트에서 Serverless가 왜 필요한지 예로 들어보겠습니다.

초기 설계한 모델(로컬 서버 또는 EC2 환경)에서는 사용자가 이미지를 업로드하기 위해서는 서버를 24시간 구동하고 있어야 합니다. 사용자가 1명이든, 100명이든 서버를 구동하는 비용은 똑같이 내게 됩니다.

뿐만 아니라, API를 설계하는 데 있어 리소스들을 관리하기 위해 고려해야될 사항이 너무 많았습니다. (트래픽이 몰린다면?, 서버 확장에 대한 유지보수 작업)

반면, Serverless에서 Lambda 함수를 활용하면 작업을 수행할 때만 구동되고, 그 나머지 시간에는 휴먼 상태에 들어가게 됩니다. 다시 말해, 함수가 수행된 횟수 만큼 비용을 청구하는 방식이기 때문에 훨씬 자원 효율적이며, 비용 소모가 적습니다.

또한 Lambda 함수를 설정해두고, 서비스 코드를 실행시킬 트리거(특정 이벤트에 대한 모니터링)만 설정해주면 서버가 필요가 없어지게 됩니다. 결국 서버 운영에 대한 부담도 줄어들게 되는 것입니다.

*** 서버 확장성에 대해 덧붙여서 얘기하자면 전자의 서버 방식은 트래픽이 몰리면 Auto Scaling과 같은 복잡한 기술을 통해 확장 하는 반면, Lambda 함수는 함수의 호출 수만 늘어날 뿐입니다. 즉 저희는 호출한 만큼 비용을 더 지불할 뿐이고, 확장성 면에서 훨씬 뛰어나다 볼 수 있는 것입니다

이와 같은 Serverless 설계를 FaaS (Functions as a Service) 라고 부릅니다. 한 곳(서버)에 있는 서비스(API)들을 마이크로 서비스로 쪼개서 서비스(람다 함수)를 독립적으로 자유롭게 배포하고, 서비스가 실행된 횟수만큼 비용을 지불하는 것입니다.



Serverless Framework에서 Presigned URL 가져오기

LambdaAPI Gateway로 손수 인프라 구성을하는 작업은 다소.. 아니 매우 번거롭습니다.

다행히도 이를 돕기 위한 Serverless Framework가 있습니다. Serverless Framework를 사용하면 설정 파일 하나로 AWS 인프라 세팅부터 배포까지 한 번에 쉽게 끝낼 수 있습니다.

그럼 serverless 설치부터 시작해보겠습니다.


1-1) npm으로 serverless 프레임워크를 글로벌 옵션으로 설치해줍니다.

npm install serverless -g



1-2) 프로젝트 디렉토리로 와서 서비스 기반이 되는 템플릿 유형(aws-nodejs)을 선택해서 애플리케이션을 생성해보겠습니다.

sls create -t aws-nodejs

  • sls는 serverless의 약자이며, -t는 templete의 약자입니다.


1-3) 생성이 완료되면 프로젝트 폴더 안에 handler.jsserverless.yml 파일이 생깁니다.

project
├── handler.js
├── package.json
└── serverless.yml
  • serverless.yml : AWS CloudFormation 템플릿(AWS의 인프라)을 생성하는데 필요한 설정 파일입니다. 자세한 내용은 아래 링크에서 확인하실 수 있습니다.

  • handler.js : 람다 함수들을 정의하는 곳입니다.

Serverless Framework - Documentation (Serverless Framework Services)



serverless.yml

1-4) 그럼 serverless.yml파일을 작성해보겠습니다.

service: serverless-test # 계정의 고유한 이름 설정

frameworkVersion: "3" # 프레임워크 버전 설정

provider: # (1)
  name: aws
  runtime: nodejs12.x
  stage: "dev" # (2)
  region: "ap-northeast-2"

  iamRoleStatements: # (3)
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObject
      Resource: "arn:aws:s3:::${버킷 이름}/*"

functions: # (4)
  hello:
    handler: handler.generatePresignedUrl
    events:
      - http:
          path: presigned
          method: post
          cors: true # CORS 오류 임시 조치

(1) 클라우드 공급자(AWS)의 구성을 정의하는 곳입니다.
provider 속성은 밑에 있는 functions 속성에도 적용됩니다.

(2) stage는 배포 상태를, region은 배포할 지역을 의미합니다.

(3) AWS 람다 함수가 다른 AWS 서비스와 상호작용하기 위해서는 IAM Role을 통해 권한을 설정해주셔야 합니다. 이미 IAM 계정이 있으신 분들은

provider:
  name: aws
  role: ${IAM 리소스 이름}

으로 진행해주시면 됩니다.

**
저같은 경우 IAM Role을 새로 설정해주었는데 보시면 S3에 대한 권한으로 GetObject, PutObject 속성을 지정해주었습니다.GetObject 권한은 저희 프로젝트에서 추후 private 게시글의 이미지를 조회할 때 필요로하며, PutObject 권한은 이미지 저장 시 인증을 위해 설정해 주었습니다.
**

(4) handler.js에 작성된 generatePresignedUrl 함수(Lambda)를 events를 통해 API Gateway와 연결시켜주는 로직을 작성해줍니다. functions 속성 안에 여러 함수들을 담을 수 있으며, provider 속성을 상속 받습니다.

(5) (22-07-22 추가) postman으로는 presignedURL을 성공적으로 응답받았지만, 클라이언트에서 요청할 경우 클라이언트와 API Gateway의 도메인이 서로 달라서 CORS 오류가 발생했습니다. 개발 단계에서는 CORS 옵션을 주어서 모든 도메인이 접근할 수 있도록 API Gateway가 구성되도록 해줍니다.



handler.js

1-5) 다음으로 handler.js에서 presignedURL을 생성하는 람다 함수를 작성해보겠습니다.

const S3 = require("aws-sdk/clients/s3") // (1)
const config = require("./config.json")
const s3 = new S3();

module.exports.generatePresignedUrl = async (event) => {

  try {
    console.log({"event log":event}); // (2)
    
    let body = JSON.parse(event.body); // (3)
    let objectKey = body.objectKey;
    let s3Action = body.s3Action;
    let contentType = body.contentType;
    let expirationTime = 60; // (4)

    let params = {
      Bucket: config.BUCKET_NAME,
      Key: objectKey,
      Expires: expirationTime
    }

    if(s3Action === 'putObject'){ // (5)
      params.ContentType = contentType;
      params.Expires = 300
    }

    const signedUrl = s3.getSignedUrl(s3Action, params); // (6)

    return {
      statusCode: 200,
      headers: { // (7)
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Credentials': true,
      },
      body: JSON.stringify(signedUrl)
    }

  } catch (error) {
    console.log(error);

    return{
      statueCode:500
    }
  }
};

(1) aws-sdk : AWS 서비스용 Javascript 라이브러리입니다.

(2) 나중 람다 함수가 실행되면 CloudWatch에서 요청된 이벤트(request라고 생각하시면 됩니다) 로그를 확인할 수 있습니다.

(3) JSON 형식으로 파싱한 뒤 presigned URL을 생성할 때 필요한 값(버킷 이름, 키(파일 이름), 타입)들을 가져옵니다.

(4) presigned URL의 만료 시간을 지정합니다. (초 단위)

(5) putObject 요청은 S3에 데이터를 넣기위한 요청이므로 업로드할 파일의 타입을 지정해주고, 테스트를 위해 만료 시간을 연장시켜 주었습니다. 실제 서비스에서는 만료 시간을 훨씬 줄이거나, Lambda@Edge를 통해 일회성으로 사용하기도 합니다.

(6) 아까 이벤트 body로부터 받아온 값들과 s3Action(getObject or putObject)를 인자로 넣어 presignedURL을 동기적으로 가져옵니다. 세 번째 인자로 콜백함수를 넣으면 비동기적으로 가져올 수 있습니다.

(7) (22-07-22 추가) CORS 조치를 위해 응답 헤더 값을 추가하였습니다.



1-6) 이제 모든 구성을 끝 마쳤고, 아래 명령어로 서비스를 배포 해봅시다.

sls deploy

배포할 때는 serverless.yml 파일이 CloudFormation 템플릿으로 변환되고 CloudFormation 스택으로 배포됩니다. 아래 링크에서 자세한 배포과정을 확인하실 수 있습니다.

저는 아직 사용안해봤지만 serverless-offline 플러그인을 사용하면 실제 배포하지 않고도 로컬에서 테스트할 수 있다 합니다.

Serverless Framework - Documentation (Deploying to AWS)

** (22-07-22 추가)
만약 CORS 설정을 하고 서버리스 배포 중에 문제가 생긴다면 aws 홈페이지 가셔서 API Gateway의 CORS 설정을 수동으로 해준 것은 아닌지 확인 하시고 만약 설정하셨다면 options 메소드를 삭제해주시면 됩니다.

서버리스 배포 중 패키지 업로드 과정에서 용량 초과 에러가 나신 분들은 서버리스 작업 환경이 다른 서비스와 구분되어있는지도 확인하셔야 됩니다. 저 같은 경우 백엔드 서비스와 같은 위치에서 배포했다가 백엔드 관련된 모듈들을 죄다 업로드되서 발생한 문제였습니다.
**

배포가 완료되면 endpoint가 출력되는 모습을 보실 수 있습니다. 해당 주소를 복사해줍니다.



presignedURL로 S3에 파일 업로드 해보기

1-7) postman으로 이동해서 S3 버킷에 업로드하기 위한 presignedURL을 생성해보겠습니다.

아까 복사한 주소를 POST 요청으로 입력한 다음, JSON 형식으로 데이터를 작성하고 요청을 보내봅니다. 저는 images/ 폴더 안에 업로드하기 위해 objectKey에 접두사를 붙여주었습니다.



1-8) 성공적으로 presignedURL을 응답 받았습니다. 이제 S3에 파일을 업로드 하기위해 URL을 복사해줍니다.



1-9) 복사한 URL을 주소창에 입력해보면 아까 저희가 요청한 데이터를 바탕으로 header 정보가 자동으로 채워집니다.



1-10) 이제 이미지를 binary 형식으로 선택하고, 혹시나 만료 시간이 안지났는지 확인이 되었으면 PUT 요청으로 전송해봅니다.

만료 시간이 지나면 아래와 같은 AccessDenied 오류 코드가 출력됩니다.



1-11) 업로드가 성공적으로 되었는지 확인하기위해 AWS 홈페이지에서 S3 서비스로 이동해서 버킷을 열어봅니다.

방금 업로드한 파일을 확인했고, 객체 URL을 들어가보니 Redux 로고도 잘 출력되는 모습을 확인할 수 있습니다.

저 같은 경우 버킷을 생성할 때 버킷 정책을 public으로 설정해주었기 때문에 별도의 과정 없이 객체 URL을 접속할 수 있었는데, 혹시나 버킷을 private으로 설정해놓으신 분들은 getObject에 대한 presignedURL을 발급받고 그 URL을 브라우저에 입력하면 이미지가 잘 출력되실 겁니다.



오늘은 여기까지 하겠습니다! 서버 리스 환경에서 presignedURL을 발급받는 레퍼런스가 국내에는 아직 없는 것 같아 작성하게 되었는데 글솜씨가 미숙하지만 보시고 조금이나마 도움되셨으면 좋겠습니다 ㅎㅎ

참고

https://www.serverless.com/
서버리스 프레임워크 작동 방식에 대해 참고하였습니다.

https://velopert.com/3543
서버리스 개념에 대해 공부하는데 많은 도움이 되었습니다.

https://www.youtube.com/watch?v=fgG2HQWNelI
서버리스 프레임워크에서 코드로 구현하는 데 있어 참고하였습니다.

profile
안녕하세요

0개의 댓글