프로젝트 3: 마이크로서비스
Achievement Goals
- AWS 클라우드 환경을 기반으로 하는 느슨하게 연결된(loosely coupled) 애플리케이션 아키택처에 대한 이해
Bare minimum
- Serverless를 이용한 메시지 대기열 활용 이해 및 구현
- 요구사항에 따른 애플리케이션과 인프라 구현
- 문제사항 해결을 위한 추가 리소스 생성 → DLQ, Legacy 시스템 성능문제 해결, SES
- 아키택처 다이어그램 제작
Day1
Section3 실습과제에서는 이때껏 배웠던 AWS의 Lambda, SQS를 이용한 마이크로소프트 아키텍처를 구현해야했다.
Day1은 튜토리얼로, 목표는 다음과 같다.
- Serverless를 이용한 AWS 리소스 생성
- 메시지 Queue가 사용되는 구조 이해
1. Serverless를 이용한 Lambda 생성
Serverless Framework를 이용하여 간단한 Lambda 함수를 생성하고 배포한다.
handler.js
module.exports.hello = async (event) => {
let inputValue, outputValue
console.log(event.body)
if (event.body) {
let body = JSON.parse(event.body)
// 추가 도전과제: body가 { input: 숫자 } 가 맞는지 검증하고, 검증에 실패하면 응답코드 400 및 에러 메시지를 반환하는 코드를 넣어봅시다.
inputValue = parseInt(body.input)
outputValue = inputValue + 1
}
const message = `메시지를 받았습니다. 입력값: ${inputValue}, 결과: ${outputValue}`
return {
statusCode: 200,
body: JSON.stringify(
{
message
},
null,
2
),
};
};
serverless deploy를 통한 배포
cURL을 통한 테스트: 입력값의 +1 반환
$ curl -X POST https://API_GATEWAY_ID.execute-api.ap-northeast-2.amazonaws.com \
--header 'Content-type: application/json' \
--data-raw '{ "input": 1 }'
문제사항
cURL을 통해 POST 요청 시 Not Found 에러 메세지 반환됨
Error Message
{"message":"Not Found"}
원인
Serverless framework를 이용해 Serverless 프로젝트 생성 시 Serverless.yml 파일에 Api Gateway 메소드가 get으로 되어있었다.

해결
method에 get을 post로 고쳐주고 다시 serverless deploy를 해주었다.
2. Serverless를 이용한 Lambda - SQS - Lambda 구조 생성
serverless framework를 이용하여 메시지 큐(SQS)를 이용한 producer/consumer 구조를 생성하고 배포합니다.
handler.js - consumer code
const consumer = async (event) => {
for (const record of event.Records) {
console.log("Message Body: ", record.body);
let inputValue, outputValue
// TODO: Step 1을 참고하여, +1 를 하는 코드를 넣으세요
if (record.body) {
inputValue = parseInt(record.body)
outputValue = inputValue + 1
}
const message = `메시지를 받았습니다. 입력값: ${inputValue}, 결과: ${outputValue}`
console.log(message)
}
};
serverless deploy를 통한 배포
프로듀서 실행 → CloudWatch를 통해 컨슈머가 메시지를 소비하는 것을 확인

3. DLQ 연결 및 K6 성능 테스트
문제사항
K6 성능 테스트 시 중간중간 error_code 1503, 503 status code 반환됨

원인
짧은 시간 내에 과도한 요청을 보내어 같은 id로 선요청된 요청을 처리하지 못했을 때 생기는 에러
해결
테스트 요청하는 함수의 sleep 시간을 늘려줌
초기 다이어그램

Day2
목표
- 메시지 큐의 Pub/Sub 패턴과 Producer/Consumer 패턴의 차이를 이해한다
- DB와 서버와의 통신이 가능하도록 연결한다
- 특정 상황에서 SNS, SQS로 메시지가 전달되도록 시스템을 구성한다
- SQS에 들어온 메시지를 레거시 시스템(Factory API)으로 전달하는 시스템을 구성한다
- 레거시 시스템(Factory API)의 콜백 대상이 되는 리소스를 생성해 데이터베이스에 접근할 수 있게 한다
1. Lambda 서버(Sales API) - DB 연결

문제사항
npm start 후 localhost:8080에 GET 요청을 통해 데이터베이스 연결확인을 하려했지만, 데이터베이스 연결 오류 메시지가 반환됨
Error message
Error: Access denied for user 'minju'@'000.000.000.00' (using password: YES)
원인
해당 계정에 권한을 부여하지 않았을 시 발생하는 에러
해결
https://chobopark.tistory.com/205
2. "재고 없음" 메시지 전달 시스템 구성

재고 부족 메시지를 SNS에 발행하는 코드 적용
app.post("/checkout", connectDb, async (req, res, next) => {
const [ result ] = await req.conn.query(
getProduct('CP-502101')
)
if (result.length > 0) {
const product = result[0]
const count = req.body.count;
if (product.stock > count) {
await req.conn.query(setStock(product.product_id, product.stock - count))
return res.status(200).json({ message: `구매 완료! 남은 재고: ${product.stock - count}`});
}
else {
await req.conn.end()
const now = new Date().toString()
const message = `도너츠 재고가 없습니다. 제품을 생산해주세요! \n메시지 작성 시각: ${now}`
const params = {
Message: message,
Subject: '도너츠 재고 부족',
MessageAttributes: {
MessageAttributeProductId: {
StringValue: product.product_id,
DataType: "String",
},
MessageAttributeFactoryId: {
StringValue: req.body.MessageAttributeFactoryId,
DataType: "String",
},
},
TopicArn: process.env.TOPIC_ARN
}
const result = await sns.publish(params).promise()
return res.status(200).json({ message: `구매 실패! 남은 재고: ${product.stock}`});
}
} else {
await req.conn.end()
return res.status(400).json({ message: "상품 없음" });
}
});
cURL을 이용해 재고가 없을 때까지 요청을 보냄
재고가 없는 경우 stock_queue에 메시지가 들어온 것을 확인
문제사항
재고가 0개 남은 상황에서 cURL을 이용해 POST 요청을 보냈을 시, 예상 반환 값이 "구매 실패! 남은 재고: 0" 이었으나, 에러로 서버가 꺼져버리는 문제 발생
원인

POST 요청 시 Body 값에 필요한 값을 넣어서 요청을 해야하는데, Body에 아무것도 넣지 않은 채 요청을 보내어 서버에러가 뜨게 된 것 같다.
해결
$ curl --location --request POST 'https://API_GATE_ID.execute-api.ap-northeast-2.amazonaws.com/checkout' --header 'Content-Type: application/json' --data-raw '{ "MessageGroupId": "stock-empty-group", "subject": "부산도너츠 재고 부족", "message": "재고 부족", "MessageAttributeProductId": "CP-502101", "MessageAttributeFactoryId": "FF-500293"}'
3. 메시지를 Factory API로 전송하는 Lambda 구성 및 DLQ 추가

stock-lambda code
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
exports.handler = async (event) => {
await delay(50000)
console.log(event)
return event
}
딜레이를 SQS 기본 표시 제한 시간인 30초보다 길게 주었으므로 메시지는 소비되지 못하고 DLQ로 이동하게 된다.
Day3
Factory-API Document
Method
POST
Path
/api/manufactures
Request Body Schema : application/json
{
MessageGroupId : string(메시지 그룹 아이디) //"stock-arrival-group",
MessageAttributeProductId : string(추가 생산이 필요한 제품 아이디),
MessageAttributeProductCnt : string(추가 생산 요청 수량),
MessageAttributeFactoryId : string(추가 생산을 요청할 공장 아이디),
MessageAttributeRequester : string(추가 생산 요청 담당자)
CallbackUrl : string(생산 내역으로 데이터베이스에 재고를 추가할 서버의 주소)
}
4. 데이터베이스의 재고를 증가시키는 Lambda 함수 생성
데이터베이스의 재고를 증가시키는 Lambda 함수 배포
stock-increase-lambda handler.js
const serverless = require("serverless-http");
const express = require("express");
const app = express();
app.use(express.json())
const {
connectDb,
queries: { getProduct, increaseStock }
} = require('./database')
app.post("/product/donut", connectDb, async (req, res, next) => {
const [ result ] = await req.conn.query(
getProduct(req.body.MessageAttributeProductId || 'CP-502101')
)
if (result.length > 0) {
const product = result[0]
console.log("req.body = ", req.body)
const incremental = req.body.MessageAttributeProductCnt || 0
const queryResult = await req.conn.query(increaseStock(product.product_id, incremental))
console.log('입고성공! result = ', queryResult);
return res.status(200).json({ message: `입고 완료! 남은 재고: ${product.stock + incremental}`});
} else {
return res.status(400).json({ message: "상품 없음" });
}
});
app.use((req, res, next) => {
return res.status(404).json({
error: "Not Found",
});
});
module.exports.handler = serverless(app);
module.exports.app = app;
stock-lambda에서 레거시 시스템(Factory API)에 제품 생산 요청
stock-lambda handler.js
const axios = require('axios').default
module.exports.handler = async (event) => {
for (const record of event.Records) {
console.log(event);
const body = JSON.parse(record.body)
console.log(body)
const payload = {
"MessageGroupId": 'stock-arrival-group',
"MessageAttributeProductId": 'CP-502101',
"MessageAttributeProductCnt": 3,
"MessageAttributeFactoryId": body.MessageAttributes.MessageAttributeFactoryId.Value,
"MessageAttributeRequester": 'minju',
"CallbackUrl": 'https://26ajnrbfy9.execute-api.ap-northeast-2.amazonaws.com/product/donut'
}
console.log(payload)
console.log(payload.MessageAttributeFactoryId)
axios.post('http://project3-factory.coz-devops.click/api/manufactures', payload)
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
}
5. 추가 시나리오에 대한 아키텍처 구성 추가
a. 광고 중단 요청 진행 시나리오
재고가 없는 상황에서도 광고가 계속 진행되고 있습니다.
광고 비용 절감과 고객불만을 낮추기 위한 조치가 필요합니다.
메시지가 유실되는 상황을 막기 위해 내구성을 갖춘 시스템이 필요합니다.
- 요구사항
- 재고를 채우기 위한 과정이 진행될 때 광고 담당자에게 광고 중단 요청 내용을 담은 이메일이 전송되어야 합니다.
- 메시지에 대한 내구성을 강화하기 위해 메시지 Queue가 사용되어야 합니다.
- AWS SES 서비스를 이용해서 이메일을 전송해야 합니다.
b. VIP 고객관리 프로세스 추가 시나리오
모니터링 결과 대량 주문을 하는 일부 고객들이 확인되었습니다.
대량 구매 고객들의 사용자 정보를 식별할 수 있어야 합니다.
고객정보는 별도의 서버(EC2)와 데이터베이스(RDS)에서 관리되고 있습니다.
데이터베이스 기록과 외부 마케팅 시스템으로의 연결과정의 오류를 대비하기 위한 내구성 갖춘 시스템이 필요합니다.
- 요구사항
- 100개 이상 구매가 발생 시 해당 유저의 타입이 normal에서 Vip로 변경되어야 합니다.
- 메시지에 대한 내구성을 강화하기 위해 메시지 Queue가 사용되어야 합니다.
- 고객관리는 별도의 데이터베이스(RDS)로 관리되고 있기 때문에 해당 데이터베이스에 접근해서 정보를 수정해야 합니다.
완성 다이어그램

'Codestates' 카테고리의 다른 글
| 코드스테이츠 DevOps #Final-Project Day4 (0) | 2023.06.15 |
|---|---|
| 코드스테이츠 DevOps #Final-Project Day3 (0) | 2023.06.14 |
| 코드스테이츠 DevOps #Final-Project Day2 (0) | 2023.06.13 |
| 코드스테이츠 DevOps #Final-Project Day1 (0) | 2023.06.13 |
| 코드스테이츠 DevOps #Section1 실습과제 회고 (0) | 2023.04.05 |
