해당 문서에서는 웹 서버와 flask 기반 REST API 서버를 쿠버네티스 클러스터에서 MSA(Micro-Service Architecture) 방식으로 구축한 과정을 기술합니다. 본 프로젝트는 도커/쿠버네티스 온라인 부트캠프 with 카카오엔터프라이즈의 최종과제 제출을 목적으로 제작되었습니다.
또한, 해당 프로젝트의 웹사이트는 해당 링크의 프로젝트를 바탕으로 개발되었습니다.
웹 애플리케이션을 MSA 방식으로 구축하기 위하여 clone한 웹 프로젝트 폴더에서 변경한 점에 대해 설명하도록 하겠습니다.
위가 기존 폴더의 구조이고, MSA 구현을 위하여 변경된 폴더 구조입니다. flask 서버를 기반으로 UI를 구동하기 위해 전반적인 폴더의 구조를 변경하고, 일부 파일을 추가하였습니다.
app.py
파일을 생성하여 각각의 웹페이지에 대한 라우팅 처리와 REST API와의 통신을 할 수 있도록 구현하였습니다.from flask import Flask, render_template
import requests
import json
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/about')
def about():
return render_template('about.html')
@app.route('/upload')
def upload():
return render_template('upload.html')
@app.route('/movies/<title>')
def movie(title):
request = requests.get('http://172.30.6.154:31141/ns_movies/movies/' + title)
response = request.text
movie_info = json.loads(response)
title = movie_info['data']['title']
director = movie_info['data']['director']
year = movie_info['data']['release_year']
time = movie_info['data']['running_time']
rating = movie_info['data']['rating']
return render_template('movie_info.html',
title=title,
director=director,
year=year,
time=time,
rating=rating)
if __name__ == "__main__":
app.run(host='0.0.0.0', port="4000", debug=True)
RestAPI가 구동중인 서버에 GET method를 통해 요청을 보내 영화 정보를 받아옵니다. 영화 정보는 영화 제목, 감독, 개봉 년도, 상영 시간, 평점으로 구성되어 있습니다. 받아온 정보는 movie_info.html
에 인자 형태로 전달됩니다.
movie_info.html
를 추가하였습니다.<h1>Movie Information</h1>
<br><h3>영화 제목: {{title}}</h3>
<h3>영화 감독: {{director}}</h3>
<h3>개봉 년도: {{year}}</h3>
<h3>상영 시간: {{time}}</h3>
<h3>평점: {{rating}}</h3>
index.html
에서 변경된 파일 구조에 부합하도록 이미지 파일 경로를 변경하고 URL을 RestAPI의 형식에 맞게 수정하였습니다.<div class="cards">
<a href="movies/Evil Dead"><div class="row">
<div class="column nature">
<div class="content">
<img src="{{url_for('static', filename='img/1.jpg')}}" alt="Mountains">
<h4>Evil Dead 2013</h4>
</div>
</a>
</div>
<a href="movies/Annabelle Creation">
<div class="column nature">
<div class="content">
<img src="{{url_for('static', filename='img/2.jpg')}}" alt="Lights">
<h4>Annabelle Creation (2017)</h4>
</div>
</div>
해당 웹서버를 파드로 구성할 때 필요한 이미지를 빌드하기 위한 dockerfile을 작성하였습니다.
FROM python:3
WORKDIR /source
COPY . .
RUN apt-get update
RUN pip install flask
EXPOSE 4000
CMD ["python", "app.py"]
flask 서버 구동에 필요한 python 모듈을 설치하고 포트 번호는 기존에 설정한 4000번과 일치시켰습니다. 그리고 python app.py
를 통해 파드 실행시 자동으로 웹서버가 구동될 수 있도록 설계하였습니다. docker build
명령을 통해 이미를 생성하고, dockerhub에 push하여 쿠버네티스 클러스터 내부의 yaml 파일이 해당 이미지를 인식할 수 있게 하였습니다.
해당 이미지를 기반으로 하는 파드를 배포하기 위한 ui-deploy.yaml
파일을 작성하였습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: ui-deploy
name: ui-deploy
spec:
replicas: 1
selector:
matchLabels:
app: ui-deploy
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ui-deploy
spec:
containers:
- image: jkseo50/ui:1.5
name: ui
ports:
- containerPort: 4000
resources: {}
status: {}
사용하고자 하는 이미지와 컨테이너포트를 입력한 뒤 kubectl apply
명령을 통해 deployment를 생성하였고, kubectl expose
명령을 바탕으로 외부에서 접근할 수 있도록 서비스를 생성하였습니다.
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2022-01-19T10:50:15Z"
labels:
app: ui-deploy
name: ui-svc
namespace: default
resourceVersion: "1436047"
uid: 412ea785-c433-4982-92da-681845afa9fc
spec:
clusterIP: 10.97.207.60
clusterIPs:
- 10.97.207.60
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- nodePort: 30782
port: 4000
protocol: TCP
targetPort: 4000
selector:
app: ui-deploy
sessionAffinity: None
type: NodePort
status:
loadBalancer: {}
containerPort와 targetPort를 일치시켜 외부에서 접근 가능한 nodePort를 통해 웹서버에 정상적으로 접속할 수 있도록 서비스의 내용을 수정하였습니다.
flask 기반의 REST API 서버를 구성하기 위한 폴더의 구조는 다음과 같습니다.
flask 서버 실행을 위한 app.py
파일과 이미지 빌드를 위한 Dockerfile로 구성하였습니다. app.py
파일의 내용은 다음과 같습니다.
from flask import Flask, request, Response
from flask_restx import Resource, Api, fields
from flask import abort
app = Flask(__name__)
api = Api(app)
ns_movies = api.namespace('ns_movies', description='Movie APIs')
movie_data = api.model(
'Movie Data',
{
"title": fields.String(description="movie title", required=True),
"director": fields.String(description="movie director", required=True),
"release_year": fields.String(description="release year", required=True),
"running_time": fields.String(description="running time", required=True),
"rating": fields.String(description="IMDb Rating", required=True)
}
)
movie_info = {}
number_of_movies = 0
@ns_movies.route('/movies')
class movies(Resource):
def get(self):
return {
'number_of_movies': number_of_movies,
'movie_info': movie_info
}
@ns_movies.route('/movies/<string:title>')
class movie_title(Resource):
# 영화 정보 조회
def get(self, title):
if not title in movie_info.keys():
abort(404, description=f"Title {title} doesn't exist")
data = movie_info[title]
return {'data': data}
@api.expect(movie_data)
def post(self, title):
if title in movie_info.keys():
abort(404, description=f"Title {title} already exists")
params = request.get_json()
movie_info[title] = params
global number_of_movies
number_of_movies += 1
return Response(status=200)
def delete(self, title):
if not title in movie_info.keys():
abort(404, description=f"Title {title} doesn't exist")
del movie_info[title]
global number_of_movies
number_of_movies -= 1
return Response(status=200)
@api.expect(movie_data)
def put(self, title):
global movie_info
if not title in movie_info.keys():
abort(404, description=f"Title {title} doesn't exist")
params = request.get_json()
movie_info[title] = params
return Response(status=200)
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=3000)
RestAPI 서버에서 두 가지의 url 패턴으로 라우팅을 진행하였습니다. /movies
를 통해 서버에 존재하는 영화의 개수와 영화 정보 데이터를 조회할 수 있고, /movies/<title>
를 통해 영화 정보에 대한 CRUD 작업을 수행하거나 특정 영화의 정보를 받아올 수 있도록 구현하였습니다. 영화 정보는 영화 제목, 영화 감독, 개봉 년도, 상영 시간, 평점으로 구성된 json 형태의 데이터로 구성하였습니다. 해당 서버는 3000번 포트를 통해 실행됩니다.
FROM python:3
WORKDIR /source
COPY . .
RUN apt-get update
RUN pip install flask
RUN pip install flask_restx
EXPOSE 3000
CMD ["python", "app.py"]
UI 이미지를 빌드할 때 사용한 dockerfile과 동일한 방식으로 서버 구동에 필요한 모듈을 추가적으로 설치하고, 3000번 포트로 호스트와 연결할 포트를 설정하였습니다. python app.py
명령을 통해 파드 실행시에 자동으로 서버가 구동될 수 있도록 하였습니다. 이전과 마찬가지로 해당 dockerfile을 기반으로 이미지를 생성하였고, dockerhub에 push하였습니다.
해당 이미지를 기반으로 하는 파드를 배포하기 위한 movie-info-deploy.yaml
파일을 작성하였습니다.
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: movie-info-deploy
name: movie-info-deploy
spec:
replicas: 1
selector:
matchLabels:
app: movie-info-deploy
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: movie-info-deploy
spec:
containers:
- image: jkseo50/movie-info:1.1
name: movie-info
ports:
- containerPort: 3000
resources: {}
status: {}
dockerhub에 배포했던 REST API 서버 이미지 기반의 pod를 배포하기 위한 deployment를 생성합니다.
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2022-01-19T10:41:37Z"
labels:
app: movie-info-deploy
name: movie-info-svc
namespace: default
resourceVersion: "1434385"
uid: ebd34234-9499-4094-9bb5-ead8d8ff9c82
spec:
clusterIP: 10.108.251.244
clusterIPs:
- 10.108.251.244
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- nodePort: 31141
port: 3000
protocol: TCP
targetPort: 3000
selector:
app: movie-info-deploy
sessionAffinity: None
type: NodePort
status:
loadBalancer: {}
이전과 마찬가지로 nodePort, targetPort를 수정하여 외부에서 접속할 수 있도록 합니다.
$ kubectl apply -f ui-deploy.yaml
$ kubectl apply -f movie-info-deploy.yaml
$kubectl expose deployment ui-deploy --name ui-svc --type=NodePort
$kubectl expose deployment movie-info-deploy --name movie-info-svc --type=NodePort