- 프로젝트 이름: 쓱Bay
- 프로젝트 목적:
- 대한민국에 당근마켓, 중고나라, 번개장터 등 다양한 중고 거래 플랫폼이 있지만, 중고 경매 플랫폼은 아직 활성화되지 않음
- 따라서 다양한 중고 물품들을 경매하는 시스템을 도입하여 쓱(SSG) + Bay(eBay)라는 웹서비스로 P2P 경매 사이트를 제공함
- 이를 통해 소비자들은 필요한 물품이나 명품 등을 중고 시장가보다 합리적인 가격에 구매할 수 있음
- 프로젝트 기간: 2023.10.26 ~ 2023.11.02
(1) 언어
- Python v3.11.5
- JavaScript vES6
- MySQL v8.0
(2) 프레임워크
- Flask v3.0.0
- React v18.2.0
(3) 도구
- Visual Studio Code v1.84.0
- MySQL Workbench v8.0 CE
(3) 환경
- Windows 10 pro v22H2 x64
- Ubuntu v22.04.3 LTS
(4) 버전 관리
- pip v23.3.1
- Node.js v20.9.0
- npm v9.8.1
(5) 테스트
- Insomnia
- Google Chrome v119.0.6045.106 x64
(6) 가상 머신
(7) 운영 및 배포
- Docker
- Kubernetes
일별 | 내용 |
---|---|
1일차 (10.26) | - 주제 선정, 기술 스택 결정, 프로토 타입 구성 |
2일차 (10.27) | - 데이터베이스 완성, 메인 페이지 간단 구현 |
3~6일차 (10.28 ~ 10.31) | - React+Flask CRUD 웹 페이지 완성 |
7일차 (11.01) | - React+Flask 부가 기능 추가 |
8일차 (11.02) | - 도커 이미지 생성 및 서비스 개선 후 쿠버네티스 배포 |
📁 project
├──── 📁 server
│ ├──── 📁 resources
│ ├──── 📁 api
│ │ ├──── 📄 app.py
│ │ ├──── 📄 database.py
│ │ ├──── 📄 historyUpdate.py
│ │ ├──── 📄 requirements.txt
│ │ ├──── 📄 crontabFile
│ │ └──── 📄 Dockerfile-flask
│ └──── 📁 test
│ └──── 📄 unitTest.py
│
├──── 📁 database
│ ├──── 📄 init.sql
│ └──── 📄 Dockerfile-mysql
│
├──── 📁 client
│ ├──── 📁 node_modules
│ ├──── 📁 src
│ │ ├──── 📁 styles
│ │ │ ├──── 📄 Card.js
│ │ │ ├──── 📄 Footer.js
│ │ │ ├──── 📄 Header.js
│ │ │ ├──── 📄 Card.module.css
│ │ │ ├──── 📄 CreatePage.module.css
│ │ │ ├──── 📄 DetailPage.module.css
│ │ │ ├──── 📄 Footer.module.css
│ │ │ ├──── 📄 Header.module.css
│ │ │ ├──── 📄 LoginPage.module.css
│ │ │ ├──── 📄 MainPage.module.css
│ │ │ └──── 📄 MyPage.module.css
│ │ ├──── 📁 pages
│ │ │ ├──── 📄 CreatePage.js
│ │ │ ├──── 📄 DetailPage.js
│ │ │ ├──── 📄 LoginPage.js
│ │ │ ├──── 📄 MainPage.js
│ │ │ ├──── 📄 MyPage.js
│ │ │ └──── 📄 SignupPage.js
│ │ ├──── 📄 App.js
│ │ └──── 📄 App.css
│ │──── 📄 package-lock.json
│ │──── 📄 package.json
│ └──── 📄 Dockerfile-react
│
└──── 📁 k8s
├──── 📁 nfs
│ ├──── 📄 nfs-deployment-service.yaml
│ ├──── 📄 nfs-persistentvolume.yaml
│ └──── 📄 nfs-persistentvolumeclaim-pod.yaml
└──── 📁 service
├──── 📄 flask-deployment.yaml
├──── 📄 mysql-deployment.yaml
└──── 📄 react-deployment.yaml
Index | Method | URI | Page | Description |
---|---|---|---|---|
0. 메인 | GET | / | 검색어, 필터링 기능 추가 | |
1. 회원 | ||||
1.1 | GET POST | /login | 로그인 | 로그인 시 토큰 발급 |
1.2 | POST | /login/signup | 회원가입 | |
2. 상품 | ||||
2.1 | POST | /create | 경매상품등록 | 사진 등록 가능 |
2.2 | PUT | /detail/${상품번호} | 상품 디테일 및 입찰 | 메인, 마이페이지에서 접근 가능 / 본인 상품이면 입찰 불가 |
2.3 | DELETE | /detail/${상품번호} | 상품 삭제 | 본인 상품이 아니면 삭제 불가 |
3. 마이페이지 | ||||
3.1 | GET | /mypage | 마이페이지 메인 | |
3.2 | GET | /mypage | 마이페이지 - 구매내역 | JavaScript 탭을 이용 |
3.3 | GET | /mypage | 마이페이지 - 내 게시글 목록 | JavaScript 탭을 이용 |
📁 server
├──── 📁 resources
├──── 📁 api
│ ├──── 📄 app.py
│ ├──── 📄 database.py
│ ├──── 📄 historyUpdate.py
│ ├──── 📄 requirements.txt
│ ├──── 📄 crontabFile
│ └──── 📄 Dockerfile-flask
└──── 📁 test
└──── 📄 unitTest.py
- resources
- 사진 업로드 파일 저장 공간
- api
- app.py
- 서버 통신용 api 요청을 수행 하는 코드
- database.py
- 원하는 데이터에 해당하는 sql 코드
- historyUpdate.py
- crontab을 이용해 실시간 데이터를 history 테이블에 반영할 수 있도록 하는 코드
- requirements.txt
- Python 모듈 및 패키지 버전 명시
- crontabFile
- 지정한 시간(1분)이 지나면 자동으로 설정한 명령어 수행
- Dockerfile-flask
- 명령어를 토대로 나열된 명령문을 수행하여 Flask Docker Image를 생성
- app.py
- test
- unitTest.py
- pytest를 이용한 단위 테스트 코드
- unitTest.py
(1) 로그인
- 세션, 토큰
- HTTP 는 stateless 한 특성 때문에 각 통신의 상태는 저장되지 않음
- 서비스에서는 어떤 유저가 어떤 기능을 사용하는지 특정할 수 있어야 됨 → 세션(Session), 토큰(Token)이 사용됨
- 세션 동작과정
- 토큰 동작과정
- JWT 구조
- 토큰을 이용해 로그인 기능 구현
...
...
from flask_jwt_extended import JWTManager
from flask_jwt_extended import create_access_token
app.config["JWT_SECRET_KEY"] = "super-secret"
jwt = JWTManager(app)
@app.route('/login', methods = ["GET", "POST"])
def login():
if request.method == 'POST':
userId = request.json.get('id')
password = request.json.get('password')
isid = database.idCheck(userId, password)
if(isid) :
access_token = create_access_token(identity=userId)
return jsonify({'token': access_token, 'userId':userId}), 200
else :
return jsonify({'message': '잘못된 로그인 정보입니다. 다시 입력해주세요.'}), 401
@app.route('/login/signup', methods=['POST'])
def signup():
try:
userId = request.json.get('userId')
userPwd = request.json.get('userPwd1')
userNickname = request.json.get('userNickname')
userPhone = request.json.get('userPhone')
userInfo, status_code, headers = database.addUserInfo(userId, userPwd, userNickname, userPhone)
access_token = create_access_token(identity=userId)
return jsonify({"message": "계정 추가 및 로그인 성공", "token": access_token, 'userId':userId}), 200, {'Content-Type': 'application/json'}
except Exception as e:
print(e)
return jsonify({"message": "요청중 에러가 발생"}), 500, {'Content-Type': 'application/json'}
...
...
(2) 경매상품등록
- 정적 파일이 위치할 디렉터리를 설정후 이미지 파일이 URL 형식으로 해당 경로에 저장되게 구현
...
...
from os import path
app = Flask(__name__, static_folder='./resources/')
UPLOAD_FOLDER = path.join('.', 'resources/')
@app.route('/create', methods=['POST'])
def create():
try:
file = request.files['itemImage']
filename = file.filename
itemName = request.form.get('itemName')
itemContent = request.form.get('itemContent')
itemPrice = request.form.get('itemPrice')
itemImage = filename
userId = request.form.get('userId')
endTime = request.form.get('endTime')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))
image_url = 'https://10.0.0.4:5000/resources/' + file.filename
print(image_url)
return database.addItemInfo( itemName, itemContent, itemPrice, image_url, endTime, userId)
except Exception as e:
print(e)
return jsonify({"message": "요청중 에러가 발생"}), 500, {'Content-Type': 'application/json'}
...
...
(3) 경매낙찰시스템
- 낙찰 시간에 도달되면 이용자에게 바로 알려주기 위하여 크론탭을 이용해 구현
- 크론탭은 지정한 시간마다 갱신됨
...
...
def moveExpiredItemsToHistory():
try:
with pymysql.connect(**connectionString) as con:
cursor = con.cursor()
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(current_time + ">>>")
sql = "SELECT * FROM prehistory WHERE endTime < %s"
cursor.execute(sql, (current_time,))
expired_items = cursor.fetchall()
if expired_items:
for item in expired_items:
check_sql = "SELECT * FROM history WHERE item_id = %s"
cursor.execute(check_sql, (item['item_id'],))
existing = cursor.fetchone()
if existing:
update_sql = "UPDATE history SET user_id = %s WHERE item_id = %s"
cursor.execute(update_sql, (item['user_id'], item['item_id']))
con.commit()
else:
insert_sql = "INSERT INTO history (item_id, user_id) VALUES (%s, %s)"
cursor.execute(insert_sql, (item['item_id'], item['user_id']))
con.commit()
print("Expired items moved to history table successfully.")
except Exception as e:
print("Error occurred:", e)
moveExpiredItemsToHistory()
- 시간을 1분마다로 지정 → 1분마다 갱신되어 1분주기로 확인하여 낙찰 시간에 도달하면 이용자에게 알림
* * * * * root /usr/local/bin/python /app/historyUpdate.py >> /var/log/cron.log
(4) SQL Injection 방지
- 데이터 처리 SQL 문자열을
%s
로 처리하여 SQL Injection 방지
...
...
def getMyItem(user_id):
try:
with connect(**connectionString) as con:
cursor = con.cursor()
sql = "SELECT * FROM item where user_id = %s;"
cursor.execute(sql, [user_id])
result = cursor.fetchall()
return result
except Exception as e:
print(e)
...
...
📁 database
├──── 📄 init.sql
└──── 📄 Dockerfile-mysql
- init.sql
- Docker 이미지 빌드에 쓰일 MySQL 초기 설정용 query
- MySQL Workbench 8.0 CE의 forwarding tool로 추출한 DB DDL로 작성
- Dockerfile-mysql
- 명령어를 토대로 나열된 명령문을 수행하여 MySQL Docker Image를 생성
(1) MySQL 장점
- 필요한 데이터와 불필요한 데이터의 지표를 정의하고 분리하는 것이 가능
- 사업부와 개발부 사이의 커뮤니케이션을 담당하는 데이터 기획자는 MySQL의 구조를 알면 개발자와의 의사소통이 원활해짐
- 정의하려는 지표를 바탕으로 데이터를 검토하고 백엔드 엔지니어와 소통하며 더욱 정확한 지표를 구현 가능
(2) 정확한 지표구현이 필요한 이유
- 서비스(ex. 거래사이트)에서 ‘거래액’이란 전사적인 지표를 정의할 때 할인이 포함된 금액으로 정의할지, 할인이 포함되지 않은 금액으로 정의할 것인지는 상황에 따라 달라질 수 있음
- 이 때 기획자가 DB에 대해 어느정도 알고 있다면 MySQL의 어떤 컬럼을 사용해서 해당 지표를 구현할 수 있을지 확인할 수 있음
📁 client
├──── 📁 node_modules
├──── 📁 src
│ ├──── 📁 styles
│ │ ├──── 📄 Card.js
│ │ ├──── 📄 Footer.js
│ │ ├──── 📄 Header.js
│ │ ├──── 📄 Card.module.css
│ │ ├──── 📄 CreatePage.module.css
│ │ ├──── 📄 DetailPage.module.css
│ │ ├──── 📄 Footer.module.css
│ │ ├──── 📄 Header.module.css
│ │ ├──── 📄 LoginPage.module.css
│ │ ├──── 📄 MainPage.module.css
│ │ └──── 📄 MyPage.module.css
│ ├──── 📁 pages
│ │ ├──── 📄 CreatePage.js
│ │ ├──── 📄 DetailPage.js
│ │ ├──── 📄 LoginPage.js
│ │ ├──── 📄 MainPage.js
│ │ ├──── 📄 MyPage.js
│ │ └──── 📄 SignupPage.js
│ ├──── 📄 App.js
│ └──── 📄 App.css
│──── 📄 package-lock.json
│──── 📄 package.json
└──── 📄 Dockerfile-react
- node_modules
- 실제 라이브러리가 설치되는 디렉터리
- src
- styles
- 각 페이지 스타일 모아놓은 디렉터리
- pages
- 각 페이지 기능 구현한 코드 디렉터리
- App.js
- 모든 페이지를 핸들링 하는 root 파일
- App.css
- 모든 페이지 공통 적용 스타일 파일
- styles
- package(-lock).json
- 패키지 목록과 프로젝트 기본 정보 및 빌드방식 담는 파일
- Dockerfile-react
- 명령어를 토대로 나열된 명령문을 수행하여 React Docker Image를 생성
(1) 디자인
- 카드 UI를 이용해 전체 상품을 편하게 확인 가능
(2) 필터링
- 필터를 통해 최신순, 높은 가격순, 낮은 가격순으로 볼 수 있음
(3) 검색
- 검색을 통해 해당 문자가 포함된 상품만 볼 수 있음
(4) 탭
- 탭을 이용하여 보다 편리한 UI를 제공
(5) 유효성 검사
- 비로그인시 로그인상태에서만 확인할 수 있는 내용(ex. 마이페이지) 등에 접근 불가능
- 입력하지 않은 내용이 있으면 경고창이 나옴
- 비밀번호가 일치하지않으면 경고창이 나옴
- 본인 상품은 입찰 불가능
(6) 실시간 업데이트
- 현재 낙찰 예정 금액보다 낮은 금액은 입력 불가
- 입찰을 하면 실시간으로 가격이 반영되어 변동됨
(7) acync & await
- async 함수 내부에서 await로 서버와 통신하고 promise가 끝나면 처리 결과에 따라 다른 결과 반환
- 비동기 함수를 사용하여 다른 작업을 병렬로 실행할 수 있어 시스템 성능 향상 가능
- 개발자가 읽기 좋은 코드를 작성할 수 있게 도와줌
...
...
const handlerLogin = async () => {
try {
if (id.trim() === "") {
alert("아이디를 입력하세요.");
return;
} else if (password.trim() === "") {
alert("비밀번호를 입력하세요.");
return;
}
const response = await fetch(`https://10.0.0.4:5000/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id, password }),
});
console.log(response);
if (response.ok) {
const data = await response.json();
const { token, userId } = data;
console.log(token, userId);
localStorage.setItem("token", token);
localStorage.setItem("userId", userId);
navigate("/");
console.log("로그인 성공! 토큰:", token);
} else {
const data = await response.json();
console.log("로그인 실패!");
alert(data.message);
}
} catch (error) {
console.error("로그인 중 오류 발생:", error);
}
};
...
...
📄 Dockerfile-flask
📄 Dockerfile-mysql
📄 Dockerfile-react
(1) Dockerfile-flask
- 프로젝트에 사용한 모듈, 패키지 호환성이 좋은 Python 3.11.5 버전을 사용
- 크론탭 내용 추가
FROM python:3.11
RUN apt-get update && apt-get install -y cron
WORKDIR /app
COPY . .
RUN pip install jwt
RUN pip install --no-cache-dir -r requirements.txt
RUN crontab -l | { cat; echo "* * * * * /usr/local/bin/python /app/historyUpdate.py >> /var/log/cron.log 2>&1"; } | crontab -
CMD ["sh", "-c", "cron && python app.py"]
(2) Dockerfile-mysql
- 프로젝트 MySQL에 맞는 8.0 버전을 사용
- 환경변수는 root password만 할당
- Docker container 내부의 데이터베이스 지정 엔트리포인트인
docker-entrypoint-initdb.d
에 복사
FROM mysql:8.0
ENV MYSQL_ROOT_PASSWORD=1234
COPY ./init.sql /docker-entrypoint-initdb.d
(3) Dockerfile-react
- 다단계 도커 빌드를 이용
- 다단계 도커 빌드: 전체 빌드 시스템을 단일 파일에 포함 가능
FROM node AS builder
RUN mkdir /my-app
WORKDIR /my-app
COPY . .
RUN npm install
RUN npm run build
FROM nginx AS runtime
COPY --from=builder /my-app/build/ /usr/share/nginx/html/
CMD ["nginx", "-g", "daemon off;"]
(1) Docker Image 빌드
docker image build -t gnstkd/mymysql:1.0 -f Dockerfile-mysql .
docker image build -t gnstkd/myflask1.0 -f Dockerfile-flask .
docker image build -t gnstkd/myreact:1.0 -f Dockerfile-react .
(2) 빌드된 Docker Image 확인
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
gnstkd/myreact 1.0 c673b4d2fbd5 5 minutes ago 1.34GB
gnstkd/myflask 1.0 c0ab8f938e1d 17 hours ago 190MB
gnstkd/mymysql 1.0 a8d83556830d 17 hours ago 582MB
(1) 로컬에서 Docker Container를 실행
docker container run -d --name mymysql mymysql:1.0
docker container run -d -p 5000:5 --name myflask myflask:1.0
docker container run -d -p 3000:80 --name myreact myreact:1.0
(2) 실행 된 Docker Container 확인
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7a8b90123c72 gnstkd/myreact:1.0 "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 0.0.0.0:3000->80/tcp myreact
b1604da061ea gnstkd/myflask:1.0 "sh -c 'cron && pyth…" 26 seconds ago Up 25 seconds 0.0.0.0:5000->5000/tcp myflask
e6eb9c3f7c44 gnstkd/mymysql:1.0 "docker-entrypoint.s…" 47 seconds ago Up 45 seconds 3306/tcp, 33060/tcp mymysql
(3) 각 Docker Container들이 잘 작동하는지 확인
docker container exec -it e6eb9c3f7c44 /bin/bash
bash-4.4# mysql -u root -p
Enter password:1234
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| auction |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
mysql> show tables;
+-------------------+
| Tables_in_auction |
+-------------------+
| history |
| item |
| prehistory |
| user |
+-------------------+
(1) 로컬에 있는 Docker Image를 내 Docker Hub에 등록
docker login
docker image push gnstkd/mymysql:1.0
docker image push gnstkd/myflask:1.0
docker image push gnstkd/myreact:1.0
(2) 내 Docker Hub에 Image 등록된 것을 확인
💿 gnstkd/myreact:1.0
💿 gnstkd/myflask1.0
💿 gnstkd/mymysql:1.0
📁 k8s
├──── 📁 nfs
│ ├──── 📄 nfs-deployment-service.yaml
│ ├──── 📄 nfs-persistentvolume.yaml
│ └──── 📄 nfs-persistentvolumeclaim-pod.yaml
└──── 📁 service
├──── 📄 flask-deployment.yaml
├──── 📄 mysql-deployment.yaml
└──── 📄 react-deployment.yaml
(1) 가상머신 생성
c:\kubernetes\vagrant-kubeadm-kubernetes>vagrant up
c:\kubernetes\vagrant-kubeadm-kubernetes>vagrant status
master running (virtualbox)
node01 running (virtualbox)
node02 running (virtualbox)
(2) master node로 접속
c:\kubernetes\vagrant-kubeadm-kubernetes>vagrant ssh master
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-83-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sat Nov 4 05:01:54 AM UTC 2023
System load: 1.64306640625 Users logged in: 0
Usage of /: 23.9% of 30.34GB IPv4 address for eth0: 10.0.2.15
Memory usage: 22% IPv4 address for eth1: 10.0.0.10
Swap usage: 0% IPv4 address for tunl0: 172.16.77.128
Processes: 180
This system is built by the Bento project by Chef Software
More information can be found at https://github.com/chef/bento
Last login: Sat Nov 4 05:01:55 2023 from 10.0.2.2
vagrant@master-node:~$
(1) hostPath
- 파드가 실행된 호스트의 파일이나 디렉터리를 파드에 마운트
- 호스트에 있는 실제 파일이나 디렉터리를 마운트
- 파드를 재시작했을 때도 호스트에 데이터가 남아있음
- 파드가 재시작되어 새로운 노드에서 시작할 경우, 새로운 노드의 hostPath를 사용함 (이전 노드에서 사용한 hostPath 접근 불가)
(2) NFS
- NFS 서버를 이용해서 파드에 마운트
- 파드 하나에 안정성이 높은 외부 스토리지를 볼륨으로 설정한 후 해당 파드에 NFS 서버 설정
- 다른 파드는 NFS 볼륨으로 마운트
(1) MetalLB
- https://metallb.universe.tf/
- On-Premise 환경에서 LoadBalancer 타입의 서비스를 연동하기 위해서 필요
- metallb-system의 네임스페이스 확인
kubectl get namespace
NAME STATUS AGE
default Active 29d
ingress-nginx Active 23d
kube-node-lease Active 29d
kube-public Active 29d
kube-system Active 29d
metallb-system Active 24d
(2) NFS를 사용한 퍼시스턴트 볼륨(Persistent Volume) 생성
- 사용자(또는 클라이언트 디바이스)가 네트워크 서버에 접속하여 서버 내 파일에 액세스 가능
- 여러 사용자가 데이터 충돌없이 동일한 파일을 공유할 수 있도록 하는 규칙 설정 가능
- nfs server는
nfs-deployment-service.yaml
로 실행한 nfs-service의 ClusterIP
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-persistentvolume
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
nfs:
path: /
server: 172.17.60.101
(1) flask-deployment.yaml
- gnstkd/myflask:1.0 이미지 사용
- 5000번 포트 사용
- LoadBalancer 타입 서비스 및 IP 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-flask-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-flask
template:
metadata:
name: my-flask-pod
labels:
app: my-flask
spec:
containers:
- name: my-flask-container
image: gnstkd/myflask:1.0
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: my-flask-service
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.4
ports:
- name: my-flask
port: 5000
targetPort: 5000
selector:
app: my-flask
(2) mysql-deployment.yaml
- gnstkd/mymysql:1.0 이미지 사용
- 3306번 포트 사용
- ClusterIP 타입 서비스 설정
- ClusterIP를 사용한 이유:
- MySQL 서비스는 클러스터 외부에 노출할 필요가 없기 때문
- 외부에 노출하면 오히려 보안상의 문제가 됨
- 외부에 노출하려면 LoadBalancer 혹은 NodePort(On-premise 환경) 사용
- subPath
- volumeMount는 기본적으로 그 폴더를 덮어씌우기 때문에 기존 파일이 있을 시 파일이 사라지게됨
- 빈 폴더에 mount 설정 시 문제가 없지만 그렇지 않으면 파일이 날아가는 일이 발생
- subPath를 지정하게 되면 기존 파일은 유지하면서 subPath 하위에 새로운 파일이 저장됨
- 단, mountPath와 subPath는 이름은 동일하게 작성
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-mysql-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-mysql
template:
metadata:
name: my-mysql-pod
labels:
app: my-mysql
spec:
containers:
- name: my-mysql-container
image: gnstkd/mymysql:1.0
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-volume
mountPath: /var/lib/mysql
subPath: mysql # 중요함
volumes:
- name: mysql-volume
persistentVolumeClaim:
claimName: nfs-persistentvolumeclaim
---
apiVersion: v1
kind: Service
metadata:
name: my-mysql-service
spec:
type: ClusterIP
ports:
- name: my-mysql
port: 3306
targetPort: 3306
selector:
app: my-mysql
(3) react-deployment.yaml
- gnstkd/myreact:1.0 이미지 사용
- 80번 포트 사용
- LoadBalancer 타입 서비스 및 IP 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-react-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-react
template:
metadata:
name: my-react-pod
labels:
app: my-react
spec:
containers:
- name: my-react-container
image: gnstkd/myreact:1.0
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-react-service
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.5
ports:
- name: my-react
port: 80
targetPort: 80
selector:
app: my-react
(1) yaml 파일 실행
kubectl apply -f nfs-deployment-service.yaml
kubectl apply -f nfs-persistentvolume.yaml
kubectl apply -f nfs-persistentvolumeclaim-pod.yaml
kubectl apply -f mysql-deployment.yaml
kubectl apply -f flask-deployment.yaml
kubectl apply -f react-deployment.yaml
(2) 서비스 및 파드 생성 확인
(3) 10.0.0.5 로 접근 후 정상 작동 확인
(1) 사진 업로드
- 문제점: 다른 기능은 잘 작동하나 사진 업로드가 안 됨
...
...
@app.route('/create', methods=['POST'])
def create():
try:
file = request.files['itemImage']
filename = file.filename
itemName = request.form.get('itemName')
itemContent = request.form.get('itemContent')
itemPrice = request.form.get('itemPrice')
itemImage = filename
userId = request.form.get('userId')
endTime = request.form.get('endTime')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))
image_url = 'https://127.0.0.1:5000/resources/' + file.filename
print(image_url)
return database.addItemInfo( itemName, itemContent, itemPrice, image_url, endTime, userId)
except Exception as e:
print(e)
return jsonify({"message": "요청중 에러가 발생"}), 500, {'Content-Type': 'application/json'}
...
...
- 해결방안: 정적 파일이 위치할 디렉터리를 설정후 이미지 파일이 URL 형식으로 해당 경로에 저장되게 구현
...
...
from os import path
app = Flask(__name__, static_folder='./resources/')
UPLOAD_FOLDER = path.join('.', 'resources/')
@app.route('/create', methods=['POST'])
def create():
try:
file = request.files['itemImage']
filename = file.filename
itemName = request.form.get('itemName')
itemContent = request.form.get('itemContent')
itemPrice = request.form.get('itemPrice')
itemImage = filename
userId = request.form.get('userId')
endTime = request.form.get('endTime')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))
image_url = 'https://10.0.0.4:5000/resources/' + file.filename
print(image_url)
return database.addItemInfo( itemName, itemContent, itemPrice, image_url, endTime, userId)
except Exception as e:
print(e)
return jsonify({"message": "요청중 에러가 발생"}), 500, {'Content-Type': 'application/json'}
...
...
(2) JWT
- 문제점:
pip freeze >> requirements.txt
로 모듈 및 패키지 명시했으나 Docker Image를 빌드하면 JWT가 적용되지 않음
blinker==1.6.2
cffi==1.16.0
click==8.1.7
colorama==0.4.6
cryptography==41.0.5
distlib==0.3.7
filelock==3.12.4
Flask==2.3.3
Flask-Cors==4.0.0
Flask-JWT-Extended==4.5.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
mysql-connector==2.2.9
mysql-connector-python==8.2.0
platformdirs==3.10.0
protobuf==4.21.12
pycparser==2.21
PyJWT==2.8.0
PyMySQL==1.1.0
virtualenv==20.24.5
Werkzeug==2.3.7
- 해결방안: Dockerfile에
RUN pip install jwt
를 따로 명시
FROM python:3.11
RUN apt-get update && apt-get install -y cron
WORKDIR /app
COPY . .
# 이 부분 추가#
RUN pip install jwt
###############
RUN pip install --no-cache-dir -r requirements.txt
RUN crontab -l | { cat; echo "* * * * * /usr/local/bin/python /app/historyUpdate.py >> /var/log/cron.log 2>&1"; } | crontab -
CMD ["sh", "-c", "cron && python app.py"]
- JWT는 Python 버전때문에 따로 추가함
- 애초에 import JWT를 하면 됐음
(1) Backend
- API 명세서를 Swagger를 이용해 일목요연하게 표현
- 내부 통신 시 URL은 REST의 규격에 맞게 설정
- JWT을 브라우저 local storage에 저장하여 보안 상 취약
- Access Token의 탈취 위험을 감안해 유효기간을 짧게 설정하고, Refresh Token을 이용해 개선 가능
- test case 코드를 구현해 단위 테스트 코드 자동화 필요
- 짧은 시간에 빠르게 주먹구구식으로 개발을 하여 클린코딩이 전혀 되지 않음
- API 서버는 sidecar 등을 이용해 log를 수집하는 기능과 log 수집용 볼륨 등을 만들어야 함
(2) Database
- ER Diagram을 구성할 때, 외래키를 다르게 하면 조금 더 간편한 혹은 부가적인 구성 가능
- 기본 CRUD만을 구현하느라 참조목록이 너무 부족함
- 데이터베이스를 CronJob을 이용해 백업 용을 만들지 않음
- ORM을 사용했으면 구현이 더 편리할 수 있었음
(3) Frontend
- 회원가입 페이지 문자열 형식 (ex. 이메일) 지정
- 게시글 수정이나 삭제할 때 경고창을 한 번 더 나오게 해야함
- 웹 페이지 맨 아래에서 맨 위로 올리는 버튼이 필요
- 빌드한 이미지를 테스트할 때
console.log
등을 이용해 문제가 어디서 발생했는지 파악해야함
(4) Docker & Kubernetes
- ClusterIP는 서비스 이름으로 접근 가능하기 때문에 host IP 하드코딩 지양
- 가변적으로 바뀌어야하는 것들(IP주소)을 환경변수로 처리 필요
- configmap, secret 등을 이용해 설정을 분리하는 것이 필요
- 디플로이먼트 배포전략 및 업데이트전략 개선 필요
- 혼자서 React, Flask, MySQL, Docker, Kubernetes까지 할 수 있어서 상당히 배운 점이 많고 매우 뿌듯했습니다.
- 또한 웹페이지 구현 부분에서의 다양한 개선점과 서비스 배포 부분에서의 다른 기능을 이용하여 고객들에게 더 편리한 서비스를 제공하기 위해 노력이 필요함을 알게 되었습니다.
- PostgreSQL, SQLite 등 다양한 RDBMS가 있는데 MySQL을 왜 사용했는지에 대한 명확한 근거가 없음.
- NoSQL을 썼으면 안 됐나?
- server를 Python 기반으로 구현했으면 Flask 말고도 Django 있는데 Flask 사용 이유?
- 보안에 취약점 및 개선방법 더 생각할 거 없나?
- 공공 API 사용 및 가비아 도메인 사용할 생각 안 했나?
- monolithic 구조로 서비스를 구현했는데 규모가 커지면 MSA로 할 생각 해야됨
- 굳이 규모가 안 커도 기능 분배 용이 그러나 규모가 작은 서비스를 MSA로 분할할 때 단점도 분명 존재함
- 로컬에서 Docker, Kubernetes 말고 AWS나 Azure 등 퍼블릭 클라우드에 배포하려면 어떤 인프라를 구성해야 하나 미리 고민
- 배포 자동화하려면 GitHub Actions? Jenkins? 생각
- IaC를 위해서 Terraform 공부 필요