서버리스에서 DynamoDB 설정하기

우리의 Serverless Framework 앱에서, 이전에 AWS 콘솔을 통해 DynamoDB 테이블을 생성했습니다. 여러 앱이나 환경을 만들 때 이 방법은 번거로울 수 있습니다. 이상적으로는 이를 프로그래밍적으로 할 수 있기를 원합니다. 이번 섹션에서는 코드로서의 인프라를 사용하여 이를 수행하는 방법을 살펴보겠습니다.

리소스 생성하기

Serverless FrameworkCloudFormation을 지원하여 인프라를 코드로 구성할 수 있게 도와줍니다. CloudFormation은 AWS 콘솔을 사용하지 않고 YAML이나 JSON을 통해 AWS 리소스를 정의하는 방법입니다. 이에 대해 이번 섹션에서 자세히 다룰 예정입니다.

Change indicator 리소스를 추가할 디렉토리를 생성해 보겠습니다.

$ mkdir resources/

Change indicator resources/dynamodb-table.yml에 다음 내용을 추가합니다.

Resources:
  NotesTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ${self:custom.tableName}
      AttributeDefinitions:
        - AttributeName: userId
          AttributeType: S
        - AttributeName: noteId
          AttributeType: S
      KeySchema:
        - AttributeName: userId
          KeyType: HASH
        - AttributeName: noteId
          KeyType: RANGE
      # 용량을 자동 조정으로 설정
      BillingMode: PAY_PER_REQUEST

여기서 무엇을 하고 있는지 간단히 살펴보겠습니다.

  1. NotesTable이라는 이름의 DynamoDB 테이블 리소스를 정의합니다.

  2. 테이블 이름은 ${self:custom.tableName}이라는 커스텀 변수에서 가져옵니다. 이 변수는 serverless.yml에서 동적으로 생성됩니다. 이에 대해 아래에서 자세히 살펴볼 예정입니다.

  3. 테이블의 두 속성인 userIdnoteId를 구성하고, 이를 기본 키로 지정합니다.

  4. 마지막으로, 테이블의 읽기/쓰기 용량을 몇 가지 커스텀 변수를 통해 프로비저닝합니다. 이 부분도 곧 정의할 예정입니다.

리소스 추가하기

이제 프로젝트에 이 리소스를 참조하도록 추가해 보겠습니다.

Change indicator serverless.yml 파일 하단에 다음 resources: 블록을 추가합니다.

# CloudFormation 템플릿을 사용해 리소스 생성
resources:
  # DynamoDB
  - ${file(resources/dynamodb-table.yml)}

Change indicator serverless.yml 파일 상단의 provider: 블록 위에 다음 custom: 블록을 추가합니다.

custom:
  # serverless 명령어 실행 시 전달된 stage 값을 사용하거나,
  # provider 섹션에 설정된 값으로 대체
  stage: ${opt:stage, self:provider.stage}
  # 로컬 테스트 시 사용할 테이블 이름 설정
  tableName: ${self:custom.stage}-notes

여기서 몇 가지 중요한 내용을 추가했습니다.

  • 먼저 stage라는 커스텀 변수를 생성했습니다. 이미 provider: 블록에 stage: dev가 설정되어 있는데, 왜 커스텀 변수가 필요한지 궁금할 수 있습니다. 이는 serverless deploy --stage $STAGE 명령어를 통해 전달된 stage 값을 사용하고, stage가 설정되지 않은 경우 provider 블록의 값으로 대체하기 위함입니다. ${opt:stage, self:provider.stage}는 Serverless Framework에게 먼저 opt:stage(커맨드라인으로 전달된 값)를 찾고, 그다음 self:provider.stage(provider 블록의 값)로 대체하도록 지시합니다.

  • 테이블 이름은 배포하는 stage에 따라 동적으로 설정됩니다. ${self:custom.stage}-notes. 이렇게 동적으로 설정하는 이유는 새로운 stage(환경)에 배포할 때마다 별도의 테이블을 생성하기 위함입니다. 예를 들어 dev에 배포하면 dev-notes라는 DynamoDB 테이블이 생성되고, prod에 배포하면 prod-notes 테이블이 생성됩니다. 이를 통해 다양한 환경에서 사용하는 리소스(및 데이터)를 명확히 분리할 수 있습니다.

  • 마지막으로 BillingModePAY_PER_REQUEST 설정을 사용합니다. 이는 DynamoDB에게 요청당 비용을 지불하고 On-Demand Capacity 옵션을 사용하도록 지시합니다. On-Demand 모드에서는 데이터베이스가 진정한 서버리스가 됩니다. 이 옵션은 특히 시작 단계에서 워크로드가 예측 불가능하거나 안정적이지 않을 때 매우 비용 효율적입니다. 반면, 필요한 용량을 정확히 알고 있다면 Provisioned Capacity 모드가 더 저렴할 수 있습니다.

위 내용이 지금은 복잡하고 어려워 보일 수 있습니다. 하지만 전체 설정을 쉽게 자동화하고 복제할 수 있도록 준비하는 과정입니다. Serverless Framework(및 내부의 CloudFormation)은 serverless.yml을 기반으로 리소스를 완전히 관리합니다. 이는 테이블 이름에 오타가 있는 경우, 기존 테이블이 삭제되고 새 테이블이 생성된다는 의미입니다. 서버리스 리소스(예: DynamoDB 테이블)를 실수로 삭제하는 것을 방지하려면 DeletionPolicy: Retain 플래그를 설정해야 합니다. 이에 대한 자세한 내용은 Seed 블로그 포스트에서 확인할 수 있습니다.

이제 생성할 DynamoDB 리소스를 참조하도록 빠르게 수정해 보겠습니다.

Change indicator 환경 변수를 새로 생성된 테이블 이름으로 업데이트합니다. environment: 블록을 다음으로 교체합니다.

  # 이 환경 변수들은 함수에서 process.env로 사용 가능
  environment:
    tableName: ${self:custom.tableName}
    stripeSecretKey: ${env:STRIPE_SECRET_KEY}

Change indicator serverless.yml 파일의 iamRoleStatements: 블록을 다음으로 교체합니다.

  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      # IAM 역할 권한을 특정 stage의 테이블로 제한
      Resource:
        - "Fn::GetAtt": [ NotesTable, Arn ]

들여쓰기를 정확히 복사해야 합니다. 이 두 블록은 provider 블록 아래에 속하므로 그에 맞게 들여쓰기 해야 합니다.

여기서 몇 가지 흥미로운 작업을 수행했습니다.

  1. environment: 블록은 Serverless Framework에게 Lambda 함수에서 process.env로 사용할 수 있는 변수를 설정하도록 지시합니다. 예를 들어, process.env.tableName은 이 stage의 DynamoDB 테이블 이름으로 설정됩니다. 나중에 데이터베이스에 연결할 때 이 값이 필요합니다.

  2. tableName의 경우, 위에서 설정한 커스텀 변수를 참조합니다.

  3. iamRoleStatements:의 경우, 연결할 테이블을 명시적으로 지정합니다. 이 블록은 AWS에게 Lambda 함수가 접근할 수 있는 리소스를 제한하도록 지시합니다.

다음으로, 파일 업로드를 위한 S3 버킷을 추가해 보겠습니다.