카테고리 없음

Multi Tenancy 아키텍처에서 Serverless로 Cron Job 수행하기

딤섬뮨 2024. 4. 10. 20:07
728x90

회사를 들어온 지 2주 남짓 지났나, 회사에서 운영하는 앱에 있던 하나의 서비스를 똑 떼서 다른 앱에도 붙일 수 있게 멀티 태넌시(SaaS)를 구축하는 업무를 맡았다. 그중 일정 데이터를 산출하는 통계작업을 serverless로 크론으로 돌리면서 설계부터 많은 우여곡절과 고민을 하였다. 정리를 해본다.

 

일단 멀티 태넌트가 수행하는 요구사항은 독립적이지만, 정 반대로 크론 잡은 모든 db에 일괄적으로 시행을 해야 해서 머리가 복잡했다.


요구 사항

  • 멀티태넌시 아키텍처를 가진 시스템에서의 매일 정오 시행 되는 크론 작업
  • 모든 태넌트들의 DB에 시행
  • 멀티 태넌트는 논리적 DB 로 분리
  • 서버리스(serverless)로 시행

아키텍처 고민

  1. 하나의 cron 서비스에서 모든 태넌트의 db 관리
    1. 비즈니스 로직에 멀티 태넌시 아키텍처의 종속성이 생기며 관리가 어려움
    2. 각 db 커낵션을 맺어주고 관리하는 부분의 추가적인 유지보수가 든다.
    3. ex. 만약 하나의 db라도 이슈가 생긴다면 application 자체가 다운된다.

2. 각 Tenant 마다 Lambda 함수 생성 (선택한 아키텍처)

→ 생각해 보면, 단순히 환경 변수에서 DATABASE_URL 만 변경해서 같은 함수를 실행하면 된다

  • 실제 크론 작업 예상 도식도
  • 인프라에서 태넌시 별로 코드를 관리하므로 비즈니스 로직과 멀티 태넌시 아키텍처가 종속 x
  • 각 람다 함수별로 독립적이라 서로 영향 x
  • 단,  환경 별로 관리하기 위한 별도의 코드가 필요해짐

Serverless 프레임 워크란?

serverless framework는 aws lambda 함수와 함께 필요한 aws인프라 리소스를 개발 및 배포하는데 도움을 준다.

functions와 event로 구성된 이벤트 기반 서버리스 아키텍처를 구축해 준다.

간단히 말해, aws lambda에 내 코드를 올리는 프레임 워크라고 보면 쉽다

서버리스는 서버가 없다. 즉, 실행할 함수와 실행할 방법(이벤트)만 알면 되는데 이걸 서버리스 프레임 워크에 정의해 준다.

 

Functions?

serverless 애플리케이션 코드는 AWS Lambda 함수에서 배포되고 실행된다.

 

Events?

함수는 이벤트에 의해 트리거 된다.

이벤트는 다양한 aws 리소스에서 발생한다.

  • API Gateway URL에 대한 HTTP 요청(예: REST API의 경우)
  • S3 버킷에 업로드된 새 파일(예: 이미지 업로드용)
  • CloudWatch 일정(예: 5분마다 실행)
  • SNS 주제의 메시지
  • CloudWatch 알림
  • 그 외 다수..

Lambda cold vs warm start

cold start : 소스 코드를. zip으로 내부의 s3에 다운로드하거나 ECR에 업로드하여 지정된 실행 환경으로 컨테이너를 실행시킨다.

람다 시스템은 실행환경을 곧바로 해제하지 않고 함수가 실행된 이후 일정 기간 동안 컨테이너를 유지한다.

컨테이너가 실행중일 때 함수 호출 시 Cold Start 과정을 생략하고 곧바로 실행 파일 실행(다시 안 올려도 되니깐 시간이 빨라짐)

Lambda 동작 순서

람다 코드 작성 & 배포 -> load code as. zip -> download to container -> bootstrap code -> run code

AWS Lambda 동작 순서 (feat. Warm start vs Cold start) (velog.io)


 

어떻게 같은 코드를 사용하지만, 환경 변수를 나누어 관리하나?

 

serverless에서 제공하는 stage 옵션을 사용하면 실행하는 환경 별로 관리가 가능하다

stage별로 관리할 환경 변수는 config.js라는 파일을 만들어서 관리할 수 있다.

구체적인 구현 사항에 대해 잘 나온 블로그가 있었다.

module.exports.DATABASE_CONFIG = (serverless) => ({
    global: {
      DATABASE_URL: "url"
    },
    doctor: {
      DATABASE_URL: "url2"
    },
  });

config.js를 각 stage를 key값으로 명시한다.

service: nest-lambda-serverless-4
plugins:
  - serverless-jetpack
  - serverless-offline

custom:
  STAGE: ${self:provider.stage}
  DB_CONFIG: ${file(./config/config.js):DATABASE_CONFIG} // config.js 에서 가져올 데이터 베이스 접속정보

provider:
  name: aws
  architecture: arm64
  runtime: nodejs18.x
  stage: ${opt:stage,'global'}
  region: ap-northeast-2
  environment:
    STAGE: ${self:provider.stage}
    DATABASE_URL: ${self:custom.DB_CONFIG.${self:custom.STAGE}.DATABASE_URL}
  iam:
    //iam role 구성
  ecr:
    images:
      nest-lambda:
        path: .
  
  vpc:
    securityGroupIds:
	    // rds 와 같은 vpc로 구성

    subnetIds:
    

functions:
  api:
    image:
      name: nest-lambda
      command:
        - dist/lambda.handler
    events: // 크론 작업
      - schedule:
          rate: rate(2 minutes)

serverless에서 사용한다.

 

config.js에 바로 db url이나 username, password를 포함하여 올리면 보안에 문제가 생기기 때문에 고민을 하고 있었는데 파트장님이 AWS Secret Manager를 사용하면 된다고 알려주셔서 실제 코드가 실행되기 전에 config.js에 값을 세팅해 주는 코드를 짜서 값을 세팅해 줬다.

 

events를 명시해서, 실제 람다에 대한 크론 트리거를 걸 수 도 있다

다음과 같은 명령어를 통해 배포를 하면, 최종적으로 stage이름이 포함된 각각의 lambda함수를 배포할 수 있다.

$ npx serverless deploy -s global
$ npx serverless deploy -s doctor

 

구현 코드는 깃에 올려놨다.

sienna011022/serverless-lambda (github.com)

해당 아키텍처는, Best Practice는 아닐수도 있다는 생각을 한다.

  • 우선, 논리적 DB로 연결돼, 태넌트들이 많아진다면 Connection 관리가 필요해진다.
  • 람다를 위한 별도의 config.js라든가 관리해야할 코드가 필요해진다.
  • AWS 인프라를 사용하는데 비용이 든다.

 

하지만, 현재 요구되는 태넌트가 2개정도이고 그렇게 많은 태넌트들이 생길 예정은 아니라, 현재 아키텍처에선 적절하다고 생각한다. 물론 요구사항이 변경될 수 있기에, 염두는 해야한다. AWS 비용 같은 경우 회사가 아낌없이 지원하는 편이고 람다 자체가 비싼 가격이 아니라 나쁜 선택지는 아니였다.

 

별 내용은 아닐지 몰라도, 누군가의 지시를 받고 일하기보다는 필요한 것들은 내가 고민하고 생각해서 제안해야 해 그만큼 성장했던 것 같다. 재미를 붙여 주말에도 계속 생각을 해보면서 결국 마지막에 람다가 다 올려졌을 때 뿌듯했던 것 같다. 앞으로도 화이팅

 

 

 

728x90