서비스 간 코드 공유

이번 장에서는 모든 비즈니스 로직 서비스(API)를 하나의 저장소에서 어떻게 구성할지 살펴보겠습니다. 다음 질문에 대한 답을 찾는 것부터 시작하겠습니다.

  1. package.json 파일을 하나만 사용할지, 아니면 여러 개를 사용할지?
  2. 서비스 간에 공통 코드와 설정을 어떻게 공유할지?
  3. 여러 serverless.yml 파일 간에 공통 설정을 어떻게 공유할지?

이번 섹션에서는 확장된 버전의 노트 앱을 사용합니다. 샘플 저장소는 여기에서 확인할 수 있습니다. 저장소가 어떻게 구성되어 있는지 간단히 살펴보겠습니다.

/
  package.json
  config.js
  serverless.common.yml
  libs/
  services/
    notes-api/
      package.json
      serverless.yml
      handler.js
    billing-api/
      package.json
      serverless.yml
      handler.js
    notify-job/
      serverless.yml
      handler.js

1. package.json 구조화

가장 먼저 떠오르는 질문은 package.json에 관한 것입니다. 하나의 package.json만 사용해야 할까요, 아니면 각 서비스마다 별도의 package.json을 가져야 할까요? 우리는 여러 개의 package.json 파일을 사용하는 것을 권장합니다.

프로젝트 루트에 있는 package.json은 모든 서비스에서 공유될 의존성을 설치하는 데 사용합니다. 예를 들어, Lambda 함수를 최적으로 패키징하기 위해 사용하는 serverless-bundle 플러그인은 루트 레벨에 설치합니다. 이 플러그인을 모든 서비스마다 설치하는 것은 의미가 없습니다.

반면, 특정 서비스에만 필요한 의존성은 해당 서비스의 package.json에 설치합니다. 예를 들어, billing-api 서비스는 stripe NPM 패키지를 사용하므로 해당 package.json에만 추가합니다. 마찬가지로 notes-api 서비스는 uuid NPM 패키지를 사용하므로 해당 package.json에만 추가합니다.

이러한 설정은 CI를 통해 앱을 배포할 때 npm install을 두 번 실행해야 함을 의미합니다. 한 번은 루트 레벨에서, 다른 한 번은 특정 서비스에서 실행합니다. Seed는 이를 자동으로 처리해 줍니다.

또한 Yarn WorkspacesLerna를 사용하여 모노레포 설정의 의존성을 관리할 수도 있습니다. 이 설정에 대해서는 별도의 챕터인 Using Lerna and Yarn Workspaces with Serverless에서 다룹니다.

일반적으로 Lambda 함수와 함께 패키징해야 할 모듈을 수동으로 선택해야 할 수도 있습니다. 모든 의존성을 패키징하면 Lambda 함수의 코드 크기가 증가하고, 이로 인해 콜드 스타트 시간이 길어질 수 있습니다. 그러나 이 예제에서는 serverless-bundle 플러그인을 사용하고 있으며, 이 플러그인은 내부적으로 Webpack의 트리 셰이킹 알고리즘을 사용하여 Lambda 함수에 필요한 코드만 패키징합니다.

2. 공통 코드와 설정 공유

모노레포 설정을 사용하는 가장 큰 이유는 여러 서비스가 공통 코드를 공유해야 하기 때문입니다. 이 방법이 가장 편리합니다.

다른 방법으로는 모든 공통 코드를 비공개 NPM 패키지로 배포하는 멀티레포 방식을 사용할 수도 있습니다. 하지만 이는 복잡성을 더하고, 작은 팀이 공통 코드를 공유하려는 경우에는 적합하지 않습니다.

이 예제에서는 공통 코드를 공유하려고 합니다. 이 코드는 libs/ 디렉토리에 위치합니다. 서비스들은 AWS SDK를 사용해 다양한 AWS 서비스를 호출해야 합니다. 그리고 공통 SDK 설정 코드는 libs/aws-sdk.js 파일에 있습니다.

import aws from "aws-sdk";
import xray from "aws-xray-sdk";

// 'invoke local' 시에는 트레이싱을 활성화하지 않음
const awsWrapped = process.env.IS_LOCAL ? aws : xray.captureAWS(aws);

export default awsWrapped;

이제 Lambda 함수들은 표준 AWS SDK 대신 이 파일을 임포트합니다.

import AWS from '../../libs/aws-sdk';

이 방식의 장점은 AWS 관련 설정을 쉽게 변경할 수 있고, 모든 서비스에 적용된다는 점입니다. 이 경우, AWS X-Ray를 사용해 전체 애플리케이션에 걸쳐 트레이싱을 활성화합니다. 이 작업이 필수는 아니지만, 이후 챕터에서 이에 대해 다룰 예정입니다. 이는 모든 서비스에서 동일한 AWS 설정을 공유하는 좋은 예시입니다.

3. 공통 serverless.yml 설정 공유하기

서비스마다 별도의 serverless.yml 설정 파일을 가지고 있지만, 모든 serverless.yml 파일에서 공유해야 하는 설정이 있습니다. 이를 위해 다음과 같은 방법을 사용합니다.

  1. 공유할 설정 값을 루트 레벨에 있는 공통 yaml 파일에 저장합니다.
  2. 각각의 serverless.yml 파일에서 이 값을 참조합니다.

예를 들어, 모든 서비스에서 현재 스테이지와 연결하려는 리소스 스테이지를 정의하고, X-Ray를 사용하기 위해 Lambda IAM 역할에 필요한 X-Ray 권한을 부여해야 합니다. 그래서 레포지토리 루트에 serverless.common.yml 파일을 추가했습니다.

custom:
  # 스테이지는 serverless 명령어를 실행할 때 전달된 값에 따라 결정됩니다.
  # 또는 provider 섹션에 설정된 값으로 대체됩니다.
  stage: ${opt:stage, self:provider.stage}
  sstAppMapping:
    prod: prod
    dev: dev
  sstApp: ${self:custom.sstAppMapping.${self:custom.stage}, self:custom.sstAppMapping.dev}-notes-ext-infra

lambdaPolicyXRay:
  Effect: Allow
  Action:
    - xray:PutTraceSegments
    - xray:PutTelemetryRecords
  Resource: "*"

그리고 각 서비스의 serverless.yml 파일에서 custom 정의를 포함시킵니다.

custom: ${file(../../serverless.common.yml):custom}

또한 lambdaPolicyXRay IAM 정책도 포함시킵니다.

  iamRoleStatements:
    - ${file(../../serverless.common.yml):lambdaPolicyXRay}

이와 같은 방식으로 공유해야 하는 다른 serverless.yml 설정도 처리할 수 있습니다.

서비스 내에서 serverless.yml 설정을 더 간소화하기 위해, 이를 더 세분화했습니다. 샘플 레포지토리services/notes-api/serverless.yml 파일에서 다음과 같은 내용을 확인할 수 있습니다.

resources:
  # API Gateway 오류
  - ${file(resources/api-gateway-errors.yml)}
  # Cognito Identity Pool 정책
  - ${file(resources/cognito-policy.yml)}

api-gateway-errors.yml 파일은 4xx 및 5xx API 오류에 대한 헤더를 추가합니다. cognito-policy.yml 파일은 Cognito 인증 사용자가 Notes API에 접근할 수 있도록 IAM 정책을 추가합니다.

Statement:
  - Effect: 'Allow'
    Action:
      - 'execute-api:Invoke'
    Resource:
      !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*'

다음으로, 여러 API 서비스가 동일한 API 엔드포인트를 공유해야 할 때 어떤 일이 발생하는지 살펴보겠습니다.