Post

Serverless Framework - AWS 서버리스 배포의 모든 것

Serverless Framework로 AWS Lambda 기반 애플리케이션을 쉽고 빠르게 배포하는 방법

Serverless Framework란?

Serverless Framework는 서버리스 애플리케이션 개발 및 배포를 간소화하는 오픈소스 프레임워크임.

단일 설정 파일(serverless.yml)로 Lambda 함수, API Gateway, DynamoDB, S3 등 모든 AWS 리소스를 정의하고, 명령어 하나로 전체 인프라를 배포할 수 있음.


왜 Serverless Framework를 사용해야 하는가?

수동 배포 방식의 문제점

AWS 콘솔에서 직접 배포:

1
2
3
4
5
6
7
8
9
10
1. Lambda 함수 생성
2. IAM 역할 생성 및 권한 설정
3. API Gateway 생성 및 설정
4. DynamoDB 테이블 생성
5. 환경 변수 설정
6. Lambda 함수 코드 업로드
7. API Gateway 배포
8. CloudWatch 로그 설정
9. 모든 설정을 문서화
10. dev/staging/prod 환경별로 반복

문제점:

  1. 시간 소모: 클릭 수십 번, 수십 분 소요
  2. 휴먼 에러: 설정 누락, 오타, 잘못된 ARN
  3. 재현 불가능: 동일한 환경 재구성 어려움
  4. 환경 관리: dev/prod 환경 불일치
  5. 팀 협업: 설정 공유 어려움
  6. 롤백: 이전 버전으로 되돌리기 복잡

Serverless Framework의 해결 방법

serverless.yml 하나로 모든 것 정의:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
service: my-api

provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  environment:
    TABLE_NAME: !Ref UsersTable

functions:
  createUser:
    handler: handler.create
    events:
      - http:
          path: /users
          method: post

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH

배포 명령:

1
serverless deploy --stage prod

결과:

  • ✅ Lambda 함수 자동 생성
  • ✅ IAM 역할 자동 생성 (최소 권한)
  • ✅ API Gateway 자동 설정
  • ✅ DynamoDB 테이블 자동 생성
  • ✅ 환경 변수 자동 주입
  • ✅ CloudWatch 로그 그룹 자동 생성
  • ✅ 버전 관리 자동
  • ✅ 5분 내 완료

Serverless Framework 핵심 개념

1. serverless.yml 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 서비스 식별자
service: my-service

# AWS 제공자 설정
provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
  memorySize: 512
  timeout: 30
  environment:
    # 모든 함수에 적용되는 환경 변수
    STAGE: ${self:provider.stage}
  iam:
    role:
      statements:
        # IAM 권한 정의
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
          Resource: !GetAtt UsersTable.Arn

# Lambda 함수 정의
functions:
  createUser:
    handler: src/handlers/users.create
    events:
      - http:
          path: /users
          method: post
          cors: true
    environment:
      # 이 함수에만 적용되는 환경 변수
      FUNCTION_SPECIFIC_VAR: value

  getUser:
    handler: src/handlers/users.get
    events:
      - http:
          path: /users/{id}
          method: get

# CloudFormation 리소스
resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-users-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH

  Outputs:
    UsersTableArn:
      Value: !GetAtt UsersTable.Arn
      Export:
        Name: ${self:service}-${self:provider.stage}-UsersTableArn

2. 변수와 참조

환경 변수 참조:

1
2
3
provider:
  environment:
    API_KEY: ${env:API_KEY}  # .env 파일이나 시스템 환경 변수

stage 변수:

1
2
3
4
5
6
7
8
9
10
11
provider:
  stage: ${opt:stage, 'dev'}  # CLI 옵션 또는 기본값 'dev'

resources:
  Resources:
    MyBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: my-app-${self:provider.stage}
        # dev → my-app-dev
        # prod → my-app-prod

CloudFormation 함수:

1
2
3
4
5
provider:
  environment:
    TABLE_NAME: !Ref UsersTable
    TABLE_ARN: !GetAtt UsersTable.Arn
    QUEUE_URL: !Ref MyQueue

커스텀 변수:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
custom:
  tableName: ${self:service}-users-${self:provider.stage}
  s3Bucket: ${self:service}-uploads-${self:provider.stage}

provider:
  environment:
    TABLE_NAME: ${self:custom.tableName}

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}

실전 프로젝트 활용 사례

사례 1: REST API 서비스

프로젝트 구조:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
my-api/
├── serverless.yml
├── .env
├── package.json
├── src/
│   ├── handlers/
│   │   ├── users.js
│   │   ├── orders.js
│   │   └── products.js
│   ├── utils/
│   │   └── dynamodb.js
│   └── middlewares/
│       └── auth.js
└── node_modules/

serverless.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
service: ecommerce-api

provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
  environment:
    USERS_TABLE: !Ref UsersTable
    ORDERS_TABLE: !Ref OrdersTable
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:*
          Resource:
            - !GetAtt UsersTable.Arn
            - !GetAtt OrdersTable.Arn
            - !Sub "${UsersTable.Arn}/index/*"
            - !Sub "${OrdersTable.Arn}/index/*"

# HTTP API (API Gateway v2 - 더 저렴)
functions:
  # 사용자 관리
  createUser:
    handler: src/handlers/users.create
    events:
      - httpApi:
          path: /users
          method: post

  getUser:
    handler: src/handlers/users.get
    events:
      - httpApi:
          path: /users/{id}
          method: get

  listUsers:
    handler: src/handlers/users.list
    events:
      - httpApi:
          path: /users
          method: get

  # 주문 관리
  createOrder:
    handler: src/handlers/orders.create
    events:
      - httpApi:
          path: /orders
          method: post

  getOrder:
    handler: src/handlers/orders.get
    events:
      - httpApi:
          path: /orders/{id}
          method: get

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-users-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: userId
            KeyType: HASH

    OrdersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-orders-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: orderId
            AttributeType: S
          - AttributeName: userId
            AttributeType: S
          - AttributeName: createdAt
            AttributeType: S
        KeySchema:
          - AttributeName: orderId
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: UserOrdersIndex
            KeySchema:
              - AttributeName: userId
                KeyType: HASH
              - AttributeName: createdAt
                KeyType: RANGE
            Projection:
              ProjectionType: ALL

배포:

1
2
3
4
5
# dev 환경
serverless deploy --stage dev

# prod 환경
serverless deploy --stage prod

사례 2: 이벤트 기반 처리

SQS + Lambda 패턴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
functions:
  orderProcessor:
    handler: src/handlers/processOrder.handler
    timeout: 60
    reservedConcurrency: 10  # 최대 동시 실행 수
    events:
      - sqs:
          arn: !GetAtt OrdersQueue.Arn
          batchSize: 10
          maximumBatchingWindowInSeconds: 5

resources:
  Resources:
    OrdersQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-orders-${self:provider.stage}
        VisibilityTimeout: 300
        MessageRetentionPeriod: 345600  # 4일
        RedrivePolicy:
          deadLetterTargetArn: !GetAtt OrdersDLQ.Arn
          maxReceiveCount: 3

    OrdersDLQ:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-orders-dlq-${self:provider.stage}
        MessageRetentionPeriod: 1209600  # 14일

S3 + Lambda 패턴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
functions:
  imageProcessor:
    handler: src/handlers/processImage.handler
    timeout: 300
    memorySize: 1024
    events:
      - s3:
          bucket: ${self:custom.uploadsBucket}
          event: s3:ObjectCreated:*
          rules:
            - prefix: uploads/
            - suffix: .jpg

custom:
  uploadsBucket: ${self:service}-uploads-${self:provider.stage}

resources:
  Resources:
    UploadsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.uploadsBucket}

사례 3: Step Functions 통합

serverless-step-functions 플러그인:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
plugins:
  - serverless-step-functions

stepFunctions:
  stateMachines:
    orderWorkflow:
      name: ${self:service}-order-workflow-${self:provider.stage}
      definition:
        Comment: 주문 처리 워크플로우
        StartAt: ValidateOrder
        States:
          ValidateOrder:
            Type: Task
            Resource: !GetAtt ValidateOrderLambdaFunction.Arn
            Next: ProcessPayment
            Catch:
              - ErrorEquals: ["States.ALL"]
                Next: HandleError

          ProcessPayment:
            Type: Task
            Resource: !GetAtt ProcessPaymentLambdaFunction.Arn
            Next: SendNotification
            Retry:
              - ErrorEquals: ["PaymentError"]
                MaxAttempts: 3
                BackoffRate: 2.0

          SendNotification:
            Type: Task
            Resource: !GetAtt SendNotificationLambdaFunction.Arn
            End: true

          HandleError:
            Type: Task
            Resource: !GetAtt HandleErrorLambdaFunction.Arn
            End: true

functions:
  validateOrder:
    handler: src/workflow/validate.handler

  processPayment:
    handler: src/workflow/payment.handler

  sendNotification:
    handler: src/workflow/notify.handler

  handleError:
    handler: src/workflow/error.handler

환경별 설정 관리

Stage 변수 활용

serverless.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
custom:
  stages:
    dev:
      memorySize: 512
      timeout: 30
      logRetention: 3
    prod:
      memorySize: 1024
      timeout: 60
      logRetention: 30

provider:
  stage: ${opt:stage, 'dev'}

functions:
  myFunction:
    handler: handler.main
    memorySize: ${self:custom.stages.${self:provider.stage}.memorySize}
    timeout: ${self:custom.stages.${self:provider.stage}.timeout}
    events:
      - http:
          path: /api
          method: get

.env 파일

.env.dev:

1
2
3
API_KEY=dev-api-key-12345
DB_ENDPOINT=https://dev-db.example.com
LOG_LEVEL=debug

.env.prod:

1
2
3
API_KEY=prod-api-key-67890
DB_ENDPOINT=https://prod-db.example.com
LOG_LEVEL=info

serverless.yml:

1
2
3
4
5
6
7
useDotenv: true

provider:
  environment:
    API_KEY: ${env:API_KEY}
    DB_ENDPOINT: ${env:DB_ENDPOINT}
    LOG_LEVEL: ${env:LOG_LEVEL}

배포 시 환경 변수 로드:

1
2
3
4
5
# .env.dev 로드
serverless deploy --stage dev

# .env.prod 로드
serverless deploy --stage prod

플러그인 활용

serverless-offline (로컬 테스트)

1
npm install --save-dev serverless-offline
1
2
3
4
5
6
7
plugins:
  - serverless-offline

custom:
  serverless-offline:
    httpPort: 3000
    lambdaPort: 3002
1
2
3
4
5
# 로컬에서 API 실행
serverless offline

# 이제 http://localhost:3000 에서 테스트 가능
curl http://localhost:3000/users

serverless-prune-plugin (버전 정리)

1
npm install --save-dev serverless-prune-plugin
1
2
3
4
5
6
7
plugins:
  - serverless-prune-plugin

custom:
  prune:
    automatic: true
    number: 3  # 최신 3개 버전만 유지

Lambda는 배포마다 새 버전을 생성하므로, 오래된 버전이 누적되어 Code Storage 한도(75GB)를 초과할 수 있음. 이 플러그인으로 자동 정리.

serverless-dotenv-plugin

1
npm install --save-dev serverless-dotenv-plugin
1
2
3
4
5
6
plugins:
  - serverless-dotenv-plugin

custom:
  dotenv:
    path: .env.${self:provider.stage}

배포 전략

1. CI/CD 통합 (GitHub Actions)

.github/workflows/deploy.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: Deploy

on:
  push:
    branches:
      - main
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Deploy to Dev
        if: github.ref == 'refs/heads/develop'
        env:
          AWS_ACCESS_KEY_ID: $
          AWS_SECRET_ACCESS_KEY: $
        run: |
          npx serverless deploy --stage dev

      - name: Deploy to Prod
        if: github.ref == 'refs/heads/main'
        env:
          AWS_ACCESS_KEY_ID: $
          AWS_SECRET_ACCESS_KEY: $
        run: |
          npx serverless deploy --stage prod

2. 단일 함수 배포 (빠른 업데이트)

1
2
3
4
5
6
7
8
9
# 전체 배포 (느림, 5-10분)
serverless deploy

# 함수만 배포 (빠름, 10초)
serverless deploy function -f createUser

# 여러 함수 배포
serverless deploy function -f createUser
serverless deploy function -f getUser

3. 롤백

1
2
3
4
5
# 이전 배포로 롤백
serverless rollback --timestamp <timestamp>

# 타임스탬프 확인
serverless deploy list

모니터링 및 로그

CloudWatch Logs

1
2
3
4
5
6
7
8
# 실시간 로그 스트리밍
serverless logs -f createUser --tail

# 특정 기간 로그
serverless logs -f createUser --startTime 1h

# 필터링
serverless logs -f createUser --filter "ERROR"

함수 실행

1
2
3
4
5
6
7
8
# 로컬 실행
serverless invoke local -f createUser -d '{"body": "{\"name\": \"John\"}"}'

# AWS에서 실행
serverless invoke -f createUser -d '{"body": "{\"name\": \"John\"}"}'

# 로그 포함
serverless invoke -f createUser -l

비용 최적화

1. 적절한 메모리 크기

1
2
3
4
5
6
7
8
9
10
functions:
  lightTask:
    handler: handler.light
    memorySize: 128  # 가벼운 작업
    timeout: 10

  heavyTask:
    handler: handler.heavy
    memorySize: 1024  # CPU 집약적 작업
    timeout: 60

AWS Lambda Power Tuning 도구로 최적 메모리 찾기.

2. Reserved Concurrency

1
2
3
4
functions:
  criticalFunction:
    handler: handler.critical
    reservedConcurrency: 10  # 항상 10개 인스턴스 예약 (콜드 스타트 방지)

예측 가능한 트래픽이 있을 때만 사용. 비용 증가 가능.

3. Provisioned Concurrency

1
2
3
4
functions:
  highTrafficFunction:
    handler: handler.main
    provisionedConcurrency: 5  # 미리 5개 워밍

콜드 스타트가 치명적인 API에만 사용.


Best Practices

1. 구조화된 프로젝트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
my-service/
├── serverless.yml
├── package.json
├── .env.dev
├── .env.prod
├── src/
│   ├── handlers/
│   │   ├── users/
│   │   │   ├── create.js
│   │   │   ├── get.js
│   │   │   └── list.js
│   │   └── orders/
│   │       ├── create.js
│   │       └── get.js
│   ├── services/
│   │   ├── dynamodb.js
│   │   └── s3.js
│   ├── utils/
│   │   ├── validation.js
│   │   └── response.js
│   └── config/
│       └── constants.js
└── tests/
    └── handlers/
        └── users.test.js

2. 공통 레이어 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
layers:
  commonDeps:
    path: layers/commonDeps
    name: ${self:service}-common-deps-${self:provider.stage}
    description: 공통 라이브러리
    compatibleRuntimes:
      - nodejs20.x

functions:
  myFunction:
    handler: handler.main
    layers:
      - !Ref CommonDepsLambdaLayer

layers/commonDeps/nodejs/package.json:

1
2
3
4
5
6
{
  "dependencies": {
    "aws-sdk": "^3.0.0",
    "lodash": "^4.17.21"
  }
}

3. IAM 최소 권한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
provider:
  iam:
    role:
      statements:
        # ✅ 특정 리소스만
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
          Resource: !GetAtt UsersTable.Arn

        # ❌ 와일드카드 지양
        # - Effect: Allow
        #   Action:
        #     - dynamodb:*
        #   Resource: "*"

4. 환경 변수 보안

1
2
3
4
5
6
7
8
9
10
provider:
  environment:
    # ❌ 민감한 정보 직접 저장
    # API_KEY: "my-secret-key-12345"

    # ✅ Systems Manager Parameter Store
    API_KEY: ${ssm:/my-service/${self:provider.stage}/api-key}

    # ✅ AWS Secrets Manager
    DB_PASSWORD: ${ssm:/aws/reference/secretsmanager/my-db-password~true}

문제 해결

문제 1: 배포 실패 - CloudFormation Stack 롤백

1
2
3
4
5
6
7
8
9
# 스택 상태 확인
aws cloudformation describe-stacks --stack-name my-service-dev

# 이벤트 확인
aws cloudformation describe-stack-events --stack-name my-service-dev

# 강제 삭제 후 재배포
serverless remove --stage dev
serverless deploy --stage dev

문제 2: Lambda 함수 타임아웃

1
2
3
4
functions:
  myFunction:
    handler: handler.main
    timeout: 300  # 5분으로 증가 (기본 6초)

문제 3: Cold Start 느림

1
2
3
4
5
6
functions:
  myFunction:
    handler: handler.main
    provisionedConcurrency: 2  # 미리 워밍
    # 또는
    # reservedConcurrency: 5

또는 Provisioned Concurrency 스케줄링:

1
2
3
4
5
6
7
8
9
10
plugins:
  - serverless-plugin-warmup

custom:
  warmup:
    default:
      enabled: true
      events:
        - schedule: rate(5 minutes)
      concurrency: 1

Serverless Framework vs 다른 도구

vs AWS SAM (Serverless Application Model)

특성Serverless FrameworkAWS SAM
지원 클라우드AWS, Azure, GCP 등AWS만
문법YAML (간결)YAML/JSON (상세)
플러그인풍부한 생태계제한적
로컬 테스트serverless-offlinesam local
학습 곡선낮음중간

vs Terraform

특성Serverless FrameworkTerraform
초점서버리스 앱모든 인프라
추상화높음 (간단)낮음 (상세)
Lambda 배포자동수동 처리 필요
상태 관리AWS에서 관리별도 백엔드 필요

마치며

Serverless Framework는 AWS 서버리스 애플리케이션 개발의 필수 도구임.

Serverless Framework를 사용해야 할 때:

  • Lambda 기반 API 구축
  • 이벤트 기반 처리 시스템
  • 여러 환경 관리 (dev/staging/prod)
  • 팀 협업 프로젝트
  • 빠른 프로토타이핑

핵심 장점:

  1. 단일 파일 설정: serverless.yml로 모든 것 정의
  2. 빠른 배포: 5분 내 전체 인프라 배포
  3. 환경 관리: stage 변수로 쉬운 환경 분리
  4. IAM 자동화: 최소 권한 자동 생성
  5. 풍부한 플러그인: offline, prune, warmup 등

실전 활용:

  • REST API: HTTP API + Lambda + DynamoDB
  • 비동기 처리: SQS + Lambda
  • 이벤트 처리: S3 + Lambda
  • 워크플로우: Step Functions + Lambda

프로젝트 경험을 통해 Serverless Framework는 개발 생산성을 10배 이상 향상시키고, 인프라 관리 부담을 제거하며, 팀 협업을 원활하게 만드는 것을 확인했음.

단일 명령어로 복잡한 서버리스 아키텍처를 배포하고, Git으로 인프라를 버전 관리하며, 환경별로 일관된 배포를 보장할 수 있음.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.