junyeokk
Blog
Serverless·2025. 11. 16

Serverless Framework

백엔드 서버를 운영하려면 인프라를 관리해야 한다. EC2 인스턴스를 띄우고, 오토스케일링을 설정하고, 로드밸런서를 붙이고, 배포 파이프라인을 만들고... 간단한 API 하나 만드는 데도 꽤 많은 인프라 작업이 따라온다. 트래픽이 없을 때도 서버는 돌아가고 있어야 하니 비용도 계속 나간다.

AWS Lambda는 이 문제를 해결한다. 함수 단위로 코드를 올리면 요청이 올 때만 실행되고, 안 쓰면 비용이 0이다. 서버 관리도 필요 없다. 그런데 Lambda를 직접 설정하려면 또 다른 복잡함이 생긴다. Lambda 함수 하나 배포하는 데 IAM Role, API Gateway, CloudFormation 스택, S3 버킷 등을 AWS 콘솔이나 CLI로 일일이 만들어야 한다. 인프라 코드를 직접 CloudFormation YAML로 작성하면 수백 줄이 금방 넘어간다.

Serverless Framework는 이 과정을 serverless.yml 파일 하나로 추상화한다. 함수 정의, 이벤트 트리거, 리소스 생성을 선언적으로 작성하면 내부적으로 CloudFormation 템플릿을 생성하고 배포까지 자동으로 처리한다. IaC(Infrastructure as Code)의 편의성과 서버리스의 장점을 동시에 가져갈 수 있는 도구다.


핵심 개념

serverless.yml의 구조

serverless.yml은 크게 네 영역으로 나뉜다.

yaml
# 1. 서비스 메타 정보
service: my-api
frameworkVersion: '3'

# 2. 프로바이더 설정 (AWS, GCP, Azure 등)
provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}

# 3. 함수 정의
functions:
  hello:
    handler: src/handler.hello
    events:
      - httpApi:
          path: /hello
          method: get

# 4. 추가 AWS 리소스
resources:
  Resources:
    MyBucket:
      Type: AWS::S3::Bucket

service는 프로젝트 이름이다. 이 이름으로 CloudFormation 스택이 생성되고, AWS 리소스들의 네이밍에도 사용된다. 같은 계정에서 여러 서비스를 운영할 때 충돌하지 않도록 고유한 이름을 사용해야 한다.

frameworkVersion은 사용할 Serverless Framework 메이저 버전을 지정한다. 버전 3과 4 사이에 설정 방식이 꽤 다르기 때문에 명시하는 게 안전하다.

provider에서 클라우드 제공자와 기본 설정을 정의한다. runtime, region, stage 같은 값을 여기서 설정하면 모든 함수에 공통으로 적용된다.

functions에서 Lambda 함수를 선언하고, 각 함수가 어떤 이벤트에 반응할지 정의한다. handler는 파일경로.함수명 형태다.

resources는 CloudFormation 문법을 그대로 사용한다. S3 버킷, DynamoDB 테이블, SQS 큐 같은 AWS 리소스를 함수와 함께 관리할 수 있다.


Stage 관리

실무에서는 개발(dev), 스테이징(staging), 프로덕션(prod) 환경을 분리해서 운영한다. Serverless Framework에서는 stage라는 개념으로 이를 처리한다.

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

${opt:stage, 'dev'}는 CLI에서 --stage 옵션으로 전달된 값을 사용하고, 없으면 기본값 dev를 쓴다는 의미다.

bash
# 개발 환경 배포
serverless deploy --stage dev

# 프로덕션 배포
serverless deploy --stage prod

stage 값은 서비스 전체에 걸쳐 활용된다. CloudFormation 스택 이름이 my-api-dev, my-api-prod로 분리되고, Lambda 함수 이름에도 stage가 포함된다. 덕분에 같은 AWS 계정에서 여러 환경을 완전히 독립적으로 운영할 수 있다.

stage별 설정 분기

환경마다 다른 값을 써야 할 때는 custom 블록에 stage별 설정을 정의하고 참조하는 패턴이 일반적이다.

yaml
custom:
  vpcConfig:
    dev:
      securityGroupId: sg-dev12345
      subnetIds:
        - subnet-dev-a
        - subnet-dev-b
    prod:
      securityGroupId: sg-prod67890
      subnetIds:
        - subnet-prod-a
        - subnet-prod-b

provider:
  vpc:
    securityGroupIds:
      - ${self:custom.vpcConfig.${self:provider.stage}.securityGroupId}
    subnetIds: ${self:custom.vpcConfig.${self:provider.stage}.subnetIds}

${self:custom.vpcConfig.${self:provider.stage}.securityGroupId}처럼 변수 참조 안에 변수 참조를 중첩할 수 있다. stage가 devcustom.vpcConfig.dev.securityGroupId를 참조하고, prodcustom.vpcConfig.prod.securityGroupId를 참조한다.


변수 시스템

Serverless Framework의 변수 시스템은 ${} 문법을 사용한다. 여러 소스에서 값을 가져올 수 있다.

yaml
# CLI 옵션
stage: ${opt:stage, 'dev'}

# 같은 파일 내 참조
bucketName: ${self:service}-${self:provider.stage}-assets

# 환경 변수
dbHost: ${env:DATABASE_HOST}

# SSM Parameter Store
dbPassword: ${ssm:/my-app/${self:provider.stage}/db-password}

# 파일 참조
secrets: ${file(./config.${self:provider.stage}.json)}

각 소스의 용도가 다르다.

opt — CLI 옵션 값. serverless deploy --stage prod에서 ${opt:stage}prod가 된다.

self — 같은 serverless.yml 파일 내의 다른 값을 참조. DRY 원칙을 지키는 데 핵심적이다. ${self:service}로 서비스 이름을 재사용하거나, ${self:custom.myValue}로 custom 블록의 값을 참조할 수 있다.

env — OS 환경 변수. CI/CD 파이프라인에서 주입되는 값에 유용하다.

ssm — AWS Systems Manager Parameter Store. DB 비밀번호 같은 시크릿을 코드에 하드코딩하지 않고 AWS에서 안전하게 가져올 수 있다. ~true를 붙이면 SecureString(암호화된 값)도 자동 복호화된다.

file — 외부 JSON/YAML/JS 파일에서 값을 가져온다. stage별 설정 파일을 분리할 때 유용하다.

쉼표 뒤의 값은 폴백(기본값)이다. ${opt:stage, 'dev'}에서 --stage가 없으면 dev가 사용된다.


플러그인 시스템

Serverless Framework의 핵심 강점 중 하나는 플러그인 생태계다. 기본 기능만으로는 부족한 부분을 플러그인으로 확장할 수 있다.

yaml
plugins:
  - serverless-offline
  - serverless-esbuild
  - serverless-finch

플러그인은 npm 패키지로 설치하고 plugins 배열에 추가하면 된다.

bash
npm install -D serverless-offline

플러그인의 순서가 중요할 수 있다. 예를 들어 번들링 플러그인(serverless-esbuild)이 로컬 실행 플러그인(serverless-offline)보다 먼저 와야 번들링된 코드로 로컬 테스트가 가능하다. 플러그인은 Serverless Framework의 라이프사이클 훅에 걸리는 방식으로 동작하는데, 배포 전/후, 패키징 시점 등에 개입할 수 있다.

자주 사용되는 플러그인들:

플러그인역할
serverless-offline로컬에서 API Gateway + Lambda 에뮬레이션
serverless-esbuildesbuild로 함수 번들링 (빠른 빌드, 트리쉐이킹)
serverless-finchS3 정적 사이트 배포 자동화
serverless-domain-manager커스텀 도메인 설정 자동화
serverless-prune-plugin오래된 Lambda 버전 정리

배포 동작 원리

serverless deploy를 실행하면 내부적으로 여러 단계가 순차적으로 진행된다.

1. 패키징

함수 코드를 zip 파일로 압축한다. package 설정으로 포함/제외할 파일을 제어할 수 있다.

yaml
package:
  individually: true  # 함수별로 개별 패키징
  patterns:
    - '!node_modules/**'
    - '!tests/**'
    - 'src/**'

individually: true를 설정하면 함수마다 별도의 zip이 만들어진다. 함수별로 필요한 코드만 포함되므로 패키지 크기가 줄어들고, Lambda의 cold start 시간도 단축된다. 기본값은 모든 함수가 하나의 zip으로 패키징되는 것인데, 함수가 많아지면 개별 패키징이 거의 필수다.

2. CloudFormation 템플릿 생성

serverless.yml을 파싱해서 CloudFormation JSON 템플릿으로 변환한다. 함수 하나를 선언하면 내부적으로는 Lambda Function, IAM Role, API Gateway 리소스, Lambda Permission 등 여러 AWS 리소스가 자동으로 만들어진다. .serverless/ 디렉토리에서 생성된 템플릿을 확인할 수 있다.

3. S3 업로드

zip 파일과 CloudFormation 템플릿을 배포용 S3 버킷에 업로드한다. 이 버킷은 첫 배포 시 자동으로 생성된다.

4. CloudFormation 스택 배포

AWS CloudFormation에 스택 생성/업데이트를 요청한다. CloudFormation이 템플릿에 정의된 리소스를 순서대로 생성하거나 변경한다. 이 과정에서 문제가 생기면 자동으로 롤백된다.

bash
# 배포 진행 상황 확인
serverless deploy --verbose

# 특정 함수만 빠르게 배포 (코드만 업데이트, 인프라 변경 없음)
serverless deploy function -f hello

serverless deploy function은 전체 CloudFormation 스택을 업데이트하지 않고 Lambda 함수 코드만 교체한다. 전체 배포가 몇 분 걸리는 반면, 함수 단위 배포는 몇 초면 끝난다. 개발 중에 자주 사용하게 되는 명령이다.


resources 블록과 CloudFormation

serverless.ymlresources 블록에서는 CloudFormation 문법을 그대로 사용할 수 있다. 이게 Serverless Framework의 큰 강점인데, Lambda 함수뿐만 아니라 함수가 의존하는 모든 AWS 인프라를 한 파일에서 관리할 수 있기 때문이다.

yaml
resources:
  Resources:
    # S3 버킷
    MediaBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-${self:provider.stage}-media
        CorsConfiguration:
          CorsRules:
            - AllowedHeaders: ['*']
              AllowedMethods: [GET, PUT]
              AllowedOrigins: ['*']

    # CloudFront Distribution
    AssetsCDN:
      Type: AWS::CloudFront::Distribution
      DependsOn:
        - MediaBucket
      Properties:
        DistributionConfig:
          Enabled: true
          # ...

  # CloudFormation 출력값
  Outputs:
    MediaBucketName:
      Value: !Ref MediaBucket
    CDNDomainName:
      Value: !GetAtt AssetsCDN.DomainName

Outputs에 정의한 값은 배포 후 serverless info로 확인할 수 있고, 다른 CloudFormation 스택에서 !ImportValue로 참조할 수도 있다. 서비스 간 의존성이 있을 때 유용하다.

CloudFormation의 내장 함수도 그대로 사용 가능하다.

yaml
# 리소스 참조
!Ref MediaBucket          # 리소스의 물리적 ID (버킷 이름 등)
!GetAtt MediaBucket.Arn   # 리소스의 특정 속성

# 조건부
!If [IsProduction, 't3.large', 't3.micro']

# 문자열 조합
!Sub '${self:service}-${AWS::Region}-media'

# 리전, 계정 ID 등 의사 파라미터
!Sub 'arn:aws:s3:::${MediaBucket}/*'

provider 설정 심화

provider 블록에서 Lambda 함수의 공통 설정을 관리한다. 함수별로 개별 설정도 가능하지만, 공통 설정은 여기서 하는 게 깔끔하다.

yaml
provider:
  name: aws
  runtime: nodejs20.x
  region: ap-northeast-2
  stage: ${opt:stage, 'dev'}
  memorySize: 512          # Lambda 메모리 (MB)
  timeout: 30              # 실행 제한 시간 (초)
  architecture: arm64      # Graviton2 (비용 20% 절감)
  
  environment:             # 모든 함수에 적용되는 환경 변수
    NODE_ENV: ${self:provider.stage}
    DB_HOST: ${ssm:/my-app/${self:provider.stage}/db-host}

  iam:                     # IAM 권한
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
          Resource: !Sub 'arn:aws:s3:::${self:service}-*/*'

  vpc:                     # VPC 설정 (RDS 접근 등)
    securityGroupIds:
      - sg-12345
    subnetIds:
      - subnet-a
      - subnet-b

메모리와 타임아웃

Lambda의 메모리 설정은 CPU 성능과 직결된다. 메모리를 늘리면 비례해서 CPU도 더 할당된다. 128MB에서는 CPU가 극도로 제한되어 있고, 1769MB에서 vCPU 1코어가 완전히 할당된다. 메모리를 적게 주면 비용은 줄지만 실행 시간이 늘어나서 오히려 더 비쌀 수 있다.

타임아웃 기본값은 6초고, 최대 900초(15분)까지 설정 가능하다. API Gateway와 연결된 함수는 API Gateway 자체의 타임아웃이 29초이므로, Lambda 타임아웃을 그 이상으로 설정해도 의미가 없다.

IAM 권한

iam.role.statements로 Lambda 함수에 필요한 AWS 권한을 부여한다. 최소 권한 원칙(Principle of Least Privilege)에 따라 필요한 리소스와 액션만 명시하는 게 보안상 중요하다. *로 모든 권한을 주면 편하지만 위험하다.

VPC 설정

Lambda가 VPC 내의 리소스(RDS, ElastiCache 등)에 접근해야 할 때 VPC 설정이 필요하다. 단, VPC에 연결된 Lambda는 ENI(Elastic Network Interface)를 생성해야 해서 cold start가 약간 길어질 수 있다. 최근에는 AWS가 이 부분을 많이 개선해서 영향이 크지 않지만, VPC가 필요 없는 함수는 연결하지 않는 게 좋다.


이벤트 트리거

Lambda 함수를 호출하는 방법은 다양하다. events에서 어떤 이벤트에 반응할지 정의한다.

HTTP API (API Gateway v2)

가장 흔한 패턴이다. HTTP 요청에 Lambda 함수를 연결한다.

yaml
functions:
  getUser:
    handler: src/users.get
    events:
      - httpApi:
          path: /users/{id}
          method: get
  
  createUser:
    handler: src/users.create
    events:
      - httpApi:
          path: /users
          method: post

API Gateway v1(http)과 v2(httpApi)가 있는데, v2가 더 저렴하고 빠르다. 특별한 이유가 없으면 httpApi를 사용한다.

스케줄 (cron)

주기적으로 실행되는 함수를 만들 때 사용한다.

yaml
functions:
  cleanup:
    handler: src/cleanup.run
    events:
      - schedule:
          rate: rate(1 hour)
      # 또는 cron 표현식
      - schedule:
          rate: cron(0 9 * * ? *)  # 매일 09:00 UTC

S3 이벤트

파일이 업로드되면 자동으로 처리하는 패턴이다.

yaml
functions:
  processImage:
    handler: src/images.process
    events:
      - s3:
          bucket: uploads
          event: s3:ObjectCreated:*
          rules:
            - prefix: images/
            - suffix: .jpg

SQS 큐

비동기 작업 처리에 사용한다. 메시지가 큐에 들어오면 Lambda가 자동으로 처리한다.

yaml
functions:
  processOrder:
    handler: src/orders.process
    events:
      - sqs:
          arn: !GetAtt OrderQueue.Arn
          batchSize: 10

CLI 명령어

bash
# 전체 배포
serverless deploy --stage dev

# 함수 단위 배포 (빠름)
serverless deploy function -f hello --stage dev

# 서비스 정보 확인
serverless info --stage dev

# 로그 확인 (실시간)
serverless logs -f hello --stage dev --tail

# 함수 직접 호출
serverless invoke -f hello --stage dev --data '{"key": "value"}'

# 로컬 호출 (배포 없이 테스트)
serverless invoke local -f hello --data '{"key": "value"}'

# 서비스 삭제 (모든 리소스 제거)
serverless remove --stage dev

serverless invoke local은 로컬에서 Lambda 핸들러를 직접 실행한다. 배포할 필요 없이 빠르게 테스트할 수 있지만, AWS 서비스 연동은 실제 AWS 자격증명이 필요하다.


CloudFormation과의 관계

Serverless Framework를 이해하려면 CloudFormation과의 관계를 이해해야 한다. Serverless Framework는 CloudFormation의 추상화 레이어다. serverless deploy를 실행하면 최종적으로 CloudFormation 템플릿이 만들어지고, 이 템플릿이 AWS에 적용된다.

이게 의미하는 것:

  1. 배포 단위가 CloudFormation 스택이다. my-api-dev라는 이름의 스택이 생기고, 이 안에 모든 리소스가 포함된다.

  2. 변경 감지가 자동이다. 두 번째 배포부터는 이전 템플릿과 비교해서 변경된 리소스만 업데이트한다 (Change Set).

  3. 롤백이 자동이다. 배포 중 에러가 발생하면 이전 상태로 자동 롤백된다.

  4. 리소스 제한이 있다. CloudFormation 스택 하나에 500개 리소스 제한이 있다. 함수가 많으면 스택을 분리해야 할 수 있다.

  5. 삭제가 깔끔하다. serverless remove 하면 스택의 모든 리소스가 삭제된다. 수동으로 만든 리소스를 정리하느라 고생할 일이 없다.


Terraform과의 비교

인프라 관리 도구로 Terraform도 많이 사용된다. 어떤 차이가 있을까?

Serverless Framework는 서버리스 워크로드에 특화되어 있다. Lambda 함수를 선언하면 관련된 IAM Role, API Gateway, CloudWatch Logs 등이 자동으로 만들어진다. 적은 코드로 빠르게 서버리스 앱을 만들 수 있다. 대신 Lambda + API Gateway 중심이라 범용 인프라 관리에는 한계가 있다.

Terraform은 범용 인프라 관리 도구다. AWS뿐만 아니라 GCP, Azure, 기타 수백 개 제공자의 모든 리소스를 관리할 수 있다. 더 세밀한 제어가 가능하지만 그만큼 설정할 것도 많다. Lambda 함수 하나를 만들려면 IAM Role, Policy, Lambda Function, API Gateway, 각종 Permission을 일일이 선언해야 한다.

실무에서는 둘을 같이 쓰기도 한다. VPC, RDS 같은 기반 인프라는 Terraform으로 관리하고, 서버리스 애플리케이션은 Serverless Framework로 관리하는 식이다.


실전 팁

환경 변수 관리

민감한 정보는 코드에 넣지 말고 SSM Parameter Store에서 가져온다.

yaml
provider:
  environment:
    DB_PASSWORD: ${ssm:/my-app/${self:provider.stage}/db-password~true}

~true는 SecureString을 복호화해서 가져온다는 의미다. 배포 시점에 SSM에서 값을 가져와서 Lambda 환경 변수로 주입한다.

패키지 크기 최적화

Lambda의 패키지 크기 제한은 50MB(zip), 250MB(unzip)이다. Node.js 프로젝트에서 node_modules를 통째로 넣으면 금방 넘칠 수 있다. serverless-esbuild 같은 번들러 플러그인을 사용하면 필요한 코드만 번들링해서 크기를 대폭 줄일 수 있다.

yaml
plugins:
  - serverless-esbuild

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
    target: node20
    external:
      - '@aws-sdk/*'  # Lambda 런타임에 이미 포함됨

@aws-sdk/*를 external로 빼는 건 중요하다. Lambda 런타임에 AWS SDK가 이미 설치되어 있기 때문에 번들에 포함할 필요가 없다.

개별 패키징

함수가 여러 개일 때 individually: true로 개별 패키징하면 각 함수에 필요한 코드만 포함된다.

yaml
package:
  individually: true

이렇게 하면 함수 A에서만 쓰는 무거운 라이브러리가 함수 B의 패키지에 포함되지 않는다. cold start 시간과 메모리 사용량 모두 개선된다.

디버깅

배포 실패 시 .serverless/ 디렉토리의 CloudFormation 템플릿을 확인하면 어떤 리소스가 문제인지 파악할 수 있다. AWS 콘솔의 CloudFormation 서비스에서 이벤트 탭을 보면 어떤 리소스 생성이 실패했는지 상세한 에러 메시지를 확인할 수 있다.

bash
# 생성된 CloudFormation 템플릿 확인
cat .serverless/cloudformation-template-update-stack.json | jq .

# 상세 배포 로그
serverless deploy --verbose