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
을 통해서 진행한다. 이 때, 구매 lambda
가 SNS
에 전달하는 요청은 유저가 상품 요청을 하고 구매 요청 결과를 받는 흐름과는 별개로 진행된다. 재고 부족 상태 결과는 SNS
, SQS
를 거쳐 상품 생산 요청 lambda
에 도달하고 해당 람다가 재고 갱신 Lambda
에 상품 생산을 요청한다. 재고 갱신 Lambda
는 결과값을 수신받아 상품 재고 RDS
에 생산 결과를 반영한다.
해당 소스는 본인이 직접
AWS 콘솔
로 구현해보고 만든 소스코드입니다.
$ git clone https://github.com/SangYunLeee/serverless-sns-sqs-lambda
테라폼을 통해서도 만들 수 있습니다.
- 테라폼으로 만든 IaC : https://github.com/SangYunLeee/project3-msa
# mysql 접속
# mysql mysql://<user_id>:<password>@<db_hostname>/<db_name>
$ mysql mysql://root:example@www.example.com:43306/donut
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'));
env.yaml
에 자신의 DB 서버 정보를 입력한다.
PASSWORD: example
DB_USERNAME: root
DATABASE: donut
HOSTNAME: www.example.com
PORT: 3306
pakage.json
에 있는 패키지를 설치한다.$ cd serverless-sns-sqs-lambda
$ npm install
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
배포가 완료되면 아래와 같은 인프라가 구성된다.
인프라가 정상적으로 올라왔는 지 확인한다.
curl
로 테스트를 해보려 한다.# 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
를 통해서 메세지를 전달하나 재고가 부족하지 않기 때문에 아키텍쳐에서 위에서 색칠된 부분까지만 진행된다.
# 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"}
상품 구매에 실패하면 위의 그림과 같이 유저에게 구매 실패 메세지를 전달하고, 구매 실패와는 별개로 상품 생산 요청을 진행한다. # 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
로 메세지가 이동하도록 하였다.# 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"}
DLQ
로 메세지가 이동되는 것을 확인할 수 있다. 이동된 메세지는 시나리오에 따라서 이메일로 전달할 수도 있을 것이다.
각각 lambda
를 담당하는 파일명은 위의 그림과 같다.
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
}
}
정상적으로 테스트가 됨을 확인할 수 있다.
Serverless
로 IaC
를 구성해보았다. 그 와중에 구매 lambda
에서 SNS
로 메세지를 전달했으나 수량 오청 lambda
로 메세지가 도착하지 못하는 현상이 발생했다. 원인을 파악해본 결과, SNS
가 SQS
에 메세지를 전달하지 못하고 있었다. 이는 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 로 로컬 테스트 진행하는 방법