Serverless IaC를 활용한 Lambda-SNS-SQS-Lambda 아키텍쳐 구조 프로비져닝

sang yun Lee·2023년 5월 28일
1

Devops 실습

목록 보기
10/20
post-thumbnail

개요


Serverless IaC Framework를 활용해 아래와 같은 마이크로 서비스 아키틱쳐를 구현해보았다.
SNS 를 활용해 추후 메일, 문자등을 받을 수 있는 확장성을 열어두었고 SQS 를 둠으로써 람다 함수간 직접 호출 시에 발생할 수 있는 메세지 유실을 방지하고자 하였다.
AWS 콘솔로 먼저 구현을 진행해보았고 이후 Serverless IaC Framework 를 통해 IaC 작업을 마무리하였다.
현 프로젝트에서는 기존에 미리 설치되어 있는 DB 서버가 이미 있다는 가정하에 IaC 에서는 기존 DB 서버 를 활용하는 방향으로 구현하였다.
해당 아키텍쳐의 시나리오와 아키텍쳐를 간략히 설명하고 Serverless 를 통한 IaC 를 배포를 재현한 뒤 테스트를 보인 뒤 코드에 대한 설명으로 마무리하려 한다.

프로젝트 시나리오


유저는 상품을 구매합니다. 구매 시 DB 내 재고가 감소되며 만약 상품 재고가 소진되었을 경우 상품 생산을 요청하여야 합니다.상품 생산 요청을 받으면 재고에 상품을 추가하여야 합니다.

시나리오 주요사항


상품 생산 요청 메세지가 예상치 못한 문제로 인하여 처리되지 못했을 경우 요청 메세지는 별도로 보관하여 추후 처리될 수 있어야 합니다. 즉 메세지는 분실되지 않아야 합니다.

인프라 아키텍쳐


아키텍쳐 리소스 역할

  • 구매 lambda : 상품 재고 현황을 상품 재고 RDS에서 확인하여 상품 요청 결과를 유저에게 전달한다. 그리고 요청 결과와는 별개로 상품 재고가 부족할 시 SNS 를 통해 재고 부족을 알린다.
  • 상품 재고 RDS : 상품 재고의 상태를 저장해놓은 DB이다.
  • SNS : 구매 lambda에서 받은 상품 부족 결과를 자신을 구독하고 있는 리소스 들에게 알린다.
  • SQS : SNS 를 구독하고 있다가 구독 결과를 받은 뒤, 받은 구독 결과를 자신의 메세지큐에 저장한다.
  • 상품 생산 요청 lambda : SQS 의 메세지큐에서 메세지를 가져온 후 재고 갱신 Lambda에 상품 생산 요청한다.
  • 재고 갱신 Lambda : 생산 결과를 수신받고 생산 결과를 상품 재고 RDS 에 반영한다.

아키텍쳐 설명

유저가 상품을 요청하면 구매 lambda 가 상품 재고를 확인하고 구매 결과를 유저에게 즉각 전달한다. 그리고 상품 재고가 존재하지 않으면 상품 생산 요청을 SNS 을 통해서 진행한다. 이 때, 구매 lambdaSNS 에 전달하는 요청은 유저가 상품 요청을 하고 구매 요청 결과를 받는 흐름과는 별개로 진행된다. 재고 부족 상태 결과는 SNS, SQS를 거쳐 상품 생산 요청 lambda 에 도달하고 해당 람다가 재고 갱신 Lambda 에 상품 생산을 요청한다. 재고 갱신 Lambda 는 결과값을 수신받아 상품 재고 RDS 에 생산 결과를 반영한다.

배포 순서


STEP 0 : 소스코드 획득

해당 소스는 본인이 직접 AWS 콘솔로 구현해보고 만든 소스코드입니다.

$ git clone https://github.com/SangYunLeee/serverless-sns-sqs-lambda

테라폼을 통해서도 만들 수 있습니다.

STEP 1 : 상품 재고 테이블 생성

  1. MySQL 에 접속
    # mysql 접속
    # mysql mysql://<user_id>:<password>@<db_hostname>/<db_name>
    $ mysql mysql://root:example@www.example.com:43306/donut
  2. 초기 테이블 설정
    프로젝트 폴더 내 init.sql 을 참고하여 초기 테이블을 설정해준다.
    # init.sql
    CREATE TABLE `product` (
        `product_id` BINARY(16)  NOT NULL ,
    ...
    INSERT INTO product(product_id, sku, name, price, stock, factory_id, ad_id)
    VALUES(UUID_TO_BIN(UUID()),'CP-502101','부산도너츠', 19900, 1000, UUID_TO_BIN("a5cd8403-fad0-11ed-8f43-0e2f76dd43b0"),
    UUID_TO_BIN('a5ce9b87-fad0-11ed-8f43-0e2f76dd43b0'));

STEP 2 : DB 서버 정보 입력

env.yaml 에 자신의 DB 서버 정보를 입력한다.

PASSWORD: example
DB_USERNAME: root
DATABASE: donut
HOSTNAME: www.example.com
PORT: 3306

STEP 3 : AWS에 배포

  1. pakage.json 에 있는 패키지를 설치한다.
$ cd serverless-sns-sqs-lambda
$ npm install
  1. serverless 를 통해 배포한다.
$ cd serverless-sns-sqs-lambda
$ serverless deploy
Running "serverless" from node_modules

Deploying sales-api-again to stage dev (ap-northeast-2)

✔ Service deployed to stack sales-api-again-dev (263s)

console: https://console.serverless.com/detailed-backends/metrics/awsLambda?globalEnvironments=dev&globalNamespace=sales-api-again&globalRegions=ap-northeast-2&globalScope=awsLambda&globalTimeFrame=15m
endpoints:
  GET - https://180ho2w3el.execute-api.ap-northeast-2.amazonaws.com/product/donut
  POST - https://180ho2w3el.execute-api.ap-northeast-2.amazonaws.com/checkout
  POST - https://180ho2w3el.execute-api.ap-northeast-2.amazonaws.com/product/donut
functions:
  purchase: sales-api-again-dev-purchase (17 MB)
  productRequest: sales-api-again-dev-productRequest (17 MB)
  productUpdate: sales-api-again-dev-productUpdate

STEP 4 : 인프라 리소스 생성 확인

배포가 완료되면 아래와 같은 인프라가 구성된다.

인프라가 정상적으로 올라왔는 지 확인한다.

람다 함수 확인

  1. 람다 함수 생성 확인
  2. 람다 함수 환경 변수 생성 확인

SNS 생성 확인

SQS, DLQ 생성 확인

STEP 5 : 정상동작 확인

  • API Gateway 의 엔드포인트를 확인한다.


    아래 테스트에서는 API Gateway 의 엔드포인트를 복사하여 curl 로 테스트를 해보려 한다.
  • 정상 구매 시나리오 테스트

    현재 DB에는 재고가 3개이다. 제품 1개만 요청하여 정상적으로 구매가 되는 것을 확인한다.
    # curl -X POST <API-ENDPOINT>/checkout ...
    $ curl -X POST https://***.execute-api.ap-northeast-2.amazonaws.com/checkout \
       -H "Content-Type: application/json" \
       -d '{"count": 1}'
    # 정상 결과
    {"message":"구매 완료! 남은 재고: 2"}

재고에 상품이 부족하면 SNS 를 통해서 메세지를 전달하나 재고가 부족하지 않기 때문에 아키텍쳐에서 위에서 색칠된 부분까지만 진행된다.

  • 재고에 상품이 부족해 구매 요청하는 시나리오 테스트

    아까 상품을 1개 구매했기 때문에 DB 재고에 상품이 2개밖에 없다. 상품을 100개를 요청하여 제품 구매를 실패해본다.
    # curl -X POST <API-ENDPOINT>/checkout ...
    $ curl -X POST https://***.execute-api.ap-northeast-2.amazonaws.com/checkout \
       -H "Content-Type: application/json" \
       -d '{"count": 100}'
    # 실패 결과
    {"message":"구매 실패! 남은 재고: 2"}
    상품 구매에 실패하면 위의 그림과 같이 유저에게 구매 실패 메세지를 전달하고, 구매 실패와는 별개로 상품 생산 요청을 진행한다.
    상품 요청을 100개 했고 구매에 실패했다면 상품 생산 요청을 100개 하도록 구현되어 있다. 따라서 100개의 상품 재고가 채워져 있을 것이다.
    따라서 다시 한번 구매 요청을 50개 해봄으로써 구매가 성공하는 것을 확인할 수 있다.
     # curl -X POST <API-ENDPOINT>/checkout ...
     $ curl -X POST https://***.execute-api.ap-northeast-2.amazonaws.com/checkout \
        -H "Content-Type: application/json" \
        -d '{"count": 50}' 
     # 정상적으로 동작했다. 이전 요청으로 인해 재고가 100개 채워졌었고 방금 50개 주문했기 때문에 52개가 나오는 게 정상이다.
     {"message":"구매 완료! 남은 재고: 52"}
  • 상품 생산 요청에 문제가 생겼을 경우에 대한 테스트

    상품 생산 요청 lambda 에 생산 요청을 1000개 이상할 경우 람다 함수가 멈추게 만들어보았다. 이는 상품 생산 요청 lambda 에 문제가 발생했을 경우를 가정한 테스트이다. 문제가 발생했을 경우 DLQ 로 메세지가 이동하도록 하였다.
  1. 제품을 2000개 요청해본다.
    # 2000 개를 요청해보았다.
    # curl -X POST <API-ENDPOINT>/checkout ...
    $ curl -X POST https://***.execute-api.ap-northeast-2.amazonaws.com/checkout \
       -H "Content-Type: application/json" \
       -d '{"count": 2000}'
    # 실패 결과
    {"message":"구매 실패! 남은 재고: 2"}
  2. DLQ 로 메세지가 이동되는 것을 확인할 수 있다. 이동된 메세지는 시나리오에 따라서 이메일로 전달할 수도 있을 것이다.

IaC 설명


각각 lambda 를 담당하는 파일명은 위의 그림과 같다.

serverless.yaml

serverless.yaml 에 생성한 리소스를 명세한다.
Lambda 3개와 SQS, DLQ, API Gateway 가 생성되며, 리소스에 대한 권한 정책 및 접근 정책과 Lambda에 대한 환경 변수 설정 내용이 들어있다.

service: sales-api-again
frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-2
  # 람다 함수에 권한 추가
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - 'sns:Publish'
          Resource:
            - !Ref ProduceTopic
      managedPolicies:
              - 'arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole'
custom:
  env: ${file(./env.yaml)}

functions:
  # 구매 요청 람다
  purchase:
    handler: product-purchase-handler.handler
    events:
      - httpApi:
          method: GET
          path: /product/donut
      - httpApi:
          method: POST
          path: /checkout
    environment:
      DB_USERNAME : ${self:custom.env.DB_USERNAME}
      DATABASE    : ${self:custom.env.DATABASE}
      HOSTNAME    : ${self:custom.env.HOSTNAME}
      PORT        : ${self:custom.env.PORT}
      PASSWORD    : ${self:custom.env.PASSWORD}
      TOPIC_ARN   : !Ref ProduceTopic
  # 생산 요청 람다
  productRequest:
    handler: product-request-handler.handler
    timeout: 3
    events:
      - sqs:
          arn: !GetAtt ProduceQueue.Arn
    environment:
      PRODUCT_UPDATE_URL : !GetAtt HttpApi.ApiEndpoint
  # 생산 값 업데이트
  productUpdate:
    handler: stock-increase-handler.handler
    events:
      - httpApi:
          method: POST
          path: /product/donut
    environment:
      DB_USERNAME : ${self:custom.env.DB_USERNAME}
      DATABASE    : ${self:custom.env.DATABASE}
      HOSTNAME    : ${self:custom.env.HOSTNAME}
      PORT        : ${self:custom.env.PORT}
      PASSWORD    : ${self:custom.env.PASSWORD}

resources:
  Resources:
    # SNS 로부터 메세지를 받는 SQS
    ProduceQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: "ProduceQueue"
        VisibilityTimeout: 4
        RedrivePolicy:
          deadLetterTargetArn:
            Fn::GetAtt:
              - "ProduceDLQ"
              - "Arn"
          maxReceiveCount: 5
    SampleSQSPolicy:
      Type: AWS::SQS::QueuePolicy
      Properties:
        Queues:
          - !Ref ProduceQueue
        PolicyDocument:
          Statement:
            -
              Principal:
                AWS: "*"
              Action:
                - "SQS:SendMessage"
              Effect: "Allow"
              Resource: !GetAtt ProduceQueue.Arn
              Condition:
                ArnLike:
                  aws:SourceArn: !Ref ProduceTopic
    # DLQ
    ProduceDLQ:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: "ProduceDLQ"

    #
    ProduceTopic:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: ProduceTopic
    SnsSubscription:
      Type: AWS::SNS::Subscription
      Properties:
        Protocol: sqs
        Endpoint: !GetAtt ProduceQueue.Arn
        TopicArn: !Ref ProduceTopic

이외에 알면 좋은 로컬 테스트 방법

serverless 에서는 로컬 테스트를 진행하는 방법을 공유하고 있다. (링크 참조)
이번 프로젝트를 예로 아래의 serverless.yaml 에 있는 purchase 함수를 호출시켜보자.

$ serverless invoke local --function purchase --path data-product-purchase.json -e ENV=LOCAL
# 결과값
Running "serverless" from node_modules
publishResult : {
...
    "body": "{\"message\":\"구매 실패! 남은 재고: 52\"}"
...
}

인자로 넣은 data-product-purchase.json 정보

# data-product-purchase.json
{
  "resource": "/",
  "path": "/checkout",
  "httpMethod": "POST",
  "body": {
    "count": 100
  }
}

정상적으로 테스트가 됨을 확인할 수 있다.

에러 후기


ServerlessIaC 를 구성해보았다. 그 와중에 구매 lambda에서 SNS 로 메세지를 전달했으나 수량 오청 lambda 로 메세지가 도착하지 못하는 현상이 발생했다. 원인을 파악해본 결과, SNSSQS 에 메세지를 전달하지 못하고 있었다. 이는 SQS 의 액세스 정책에 SNS 가 접근할 수 있게 해놓지 않아서 발생한 현상이었다. SQS 에 액세스 정책을 아래와 같이 넣어줌으로써 해결하였다.

SampleSQSPolicy:
      Type: AWS::SQS::QueuePolicy
      Properties:
        Queues:
          - !Ref ProduceQueue
        PolicyDocument:
          Statement:
            -
              Principal:
                AWS: "*"
              Action:
                - "SQS:SendMessage"
              Effect: "Allow"
              Resource: !GetAtt ProduceQueue.Arn
              Condition:
                ArnLike:
                  aws:SourceArn: !Ref ProduceTopic

프로젝트 후기

인프라를 serverless 프레임워크로 구현하는 것에 익숙하지 않았어서 쉽지 않았지만 나름 만들고 나니 뿌듯하다. 이번에는 서버리스 아키텍쳐였기에 테라폼 보다 serverless 프레임워크를 활용하는 쪽이 구현에 용이하다 판단하여 활용했는데 다음 프로젝트에 있을 파이널 프로젝트에는 서버리스 아키텍쳐 뿐만 아니라 EC2, Fargate 등이 활용될 예정으로 알고 있어 테라폼 에 대해 미리 좀 더 숙지하려 한다.

참고 자료:
서버리스 변수 가져오는 방법
SNS 토픽 레퍼런스 가져오는 방법
SQS Plugin (left-plugin)
SQS 에 대한 Resource Property 보기
SQS가 SNS를 구독하는 방법
serverless 핸들러 함수 예제
리소스 보는 방법 (!Ref)
SNS 메세지 형식 관련
serverless 로 로컬 테스트 진행하는 방법

0개의 댓글