
42seoul 과정 중 공통 과정 마지막 프로젝트인 tail-passenger(과제명은 transcendence)를 마쳤다. 이 프로젝트를 시작할 때 하고 싶었던 DevOps 카테고리 중에 ELK stack을 시간 상 못했기 때문에 프로젝트가 끝난 지금 도전해봐야겠다고 느꼈다.
우리 프로젝트는 프로젝트 명세에 따라 docker compose로 배포한다. 또한 DevOps 카테고리에서 elk stack을 배포하는 방법 역시 docker compose로 하는 것을 전제한다. 하지만 docker compose는 이미 사용해봤고 docker와 함께 자주 거론되는 kubernetes를 사용해보기 위해서 쿠버네티스를 사용하기로 결정했다. 이를 위해 udemy에서 Kubernetes 실습: AWS 클라우드에 마이크로서비스 배포하기라는 강의를 듣고 나서 변환 작업을 시작했다.
도커에 대한 정의는 컨테이너를 다루는 도구 인 반면에, 쿠버네티스에 대한 정의는 컨테이너를 오케스트레이션 하는 도구 라고 되어 있다. 정의에서 나타나듯이 도커는 컨테이너를 쉽게 내려받거나 공유하고 구동할 수 있도록 해주는 도구인 컨테이너 런타임 인 반면에 k8s는 컨테이너 런타임을 통해 컨테이너를 오케스트레이션 하는 도구 라고 할 수 있다.
오케스트레이션은 여러 IT 자동화 태스크 또는 프로세스를 조정하여 실행하는 것이라 정의되어 있는데 이를 쿠버네티스에 적용하여 다시 정의하면 쿠버네티스는 여러 서버(노드)에 컨테이너를 분산해서 배치하거나, 문제가 생긴 컨테이너를 교체하거나, 컨테이너가 사용할 비밀번호나 환경 설정을 관리하고 주입해 주는 일 등 을 한다고 정의할 수 있다.
쿠버네티스에 대한 자세한 설명은 여러 강의나 삼성 SDS 블로그에 올라온 쿠버네티스 알아보기 1편, 2편, 3편을 참고하면 더 이해하기가 쉽다. (3편 말미에 4편에 대한 언급이 있는데 안 올라온건지 찾을 수가 없다...)
먼저 변환할 때 어떻게 시작해야 할 지 막막했다. 배우긴 했지만 직접 하려니 역시 손이 잘 안 떼지는 느낌. 다행이도 쿠버네티스에서 나같은 사람을 위해 이미 도커 컴포즈 파일을 쿠버네티스 리소스로 변환하는 도구가 이미 존재했다. Kompose 라는 도구인데 사용법 역시 간단했다. 자세한 것은 공식 문서를 참고하면 된다.
예를 들어 변환 전 우리 web 컨테이너는 아래와 같이 정의되어 있다.
# docker-compose.yml
web:
build:
context: ./backend
image: web
container_name: web
volumes:
- ./backend/back/:/app
expose:
- "443"
command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py loaddata test_user.json && daphne -b 0.0.0.0 -p 443 back.asgi:application"
env_file:
- ./.env
depends_on:
- db
networks:
- ts-network
이 파일이 담긴 위치에서 kompose convert 명령어를 실행하면 아래와 같이 두 개의 파일이 생성된다.
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: web
name: web
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: web
strategy:
type: Recreate
template:
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: web
spec:
containers:
- args:
- sh
- -c
- python manage.py makemigrations && python manage.py migrate && python manage.py loaddata test_user.json && daphne -b 0.0.0.0 -p 443 back.asgi:application
env:
- name: BASE_IP
valueFrom:
configMapKeyRef:
key: BASE_IP
name: env
- name: CLIENT_ID
valueFrom:
configMapKeyRef:
key: CLIENT_ID
name: env
- name: CLIENT_SECRET
valueFrom:
configMapKeyRef:
key: CLIENT_SECRET
name: env
- name: DJANGO_SECRET_KEY
valueFrom:
configMapKeyRef:
key: DJANGO_SECRET_KEY
name: env
- name: POSTGRES_NAME
valueFrom:
configMapKeyRef:
key: POSTGRES_NAME
name: env
- name: POSTGRES_PASSWORD
valueFrom:
configMapKeyRef:
key: POSTGRES_PASSWORD
name: env
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
key: POSTGRES_USER
name: env
- name: REDIRECT_URI
valueFrom:
configMapKeyRef:
key: REDIRECT_URI
name: env
image: web
name: web
ports:
- containerPort: 443
protocol: TCP
restartPolicy: Always
# web-service.yaml
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: web
name: web
spec:
ports:
- name: "443"
port: 443
targetPort: 443
selector:
io.kompose.service: web
위에서 kompose 관련 항목들은 app 과 같은 다른 필드로 대체해도 무관하다.

변환된 파일을 바로 배포하면 위와 같은 결과를 확인할 수 있다. 이를 자세히 살펴보기 위해 describe 명령어를 사용하여 살펴보니 다음과 같은 에러가 발생했다.
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 70s default-scheduler Successfully assigned default/web-5f9f5df857-2jkm6 to docker-desktop
Normal Pulling 24s (x3 over 68s) kubelet Pulling image "web"
Warning Failed 21s (x3 over 63s) kubelet Failed to pull image "web": Error response from daemon: pull access denied for web, repository does not exist or may require 'docker login'
Warning Failed 21s (x3 over 63s) kubelet Error: ErrImagePull
Normal BackOff 10s (x3 over 63s) kubelet Back-off pulling image "web"
Warning Failed 10s (x3 over 63s) kubelet Error: ImagePullBackOff
web 포드 말고 다른 두 포드 역시 같은 문제였다. 우리 배포 형식은 직접 만든 dockerfile 을 이용해서 이미지를 생성한 뒤, 사용하는데 kompose를 통해 변환된 yaml파일은 docker hub에서 이미지를 가져오려 하기에 가져올 이미지가 없어서 생기는 문제였다.
dockerfile을 직접 사용하여 배포하는 방법을 찾아보았으나 생각보다 자료가 많지 않고 대부분 docker hub에 올린 이미지를 사용하기에 나 역시 이미지를 만들어 docker hub에 올려 사용하기로 결정했다. 이에 따라 이미지를 올리고 deployment 파일을 아래와 같이 수정했다
# web-deployment.yaml
...
image: kmj951015/tail-passengers_web:1.0.1
name: web
...
frontend 와 middleware 역시 위와 같이 docker hub에 이미지를 올리고 이미지 이름을 적어서 수정을 완료했다.
다시 실행했으나 이번에도 역시 문제가 발생했다.

frontend 는 한 번만 실행되고 종료되어야 하는데 계속해서 재시작되고 있었고 web 은 아래와 같이 db를 찾지 못하고 있었다.
...
django.db.utils.OperationalError: could not translate host name "db" to address: Name does not resolve
먼저 web 포드의 문제부터 살펴보았다. 이미 도커에서 실행되는 것을 확인했기 때문에 백엔드 자체의 문제는 아니다. 그렇기에 현재 web 포드가 db 포드를 못 찾는다고 보는게 더 맞는 접근인 것 같았다.
살펴보니 kompose가 만든 파일 중에 db의 service 파일이 없었다. docker-compose.yml 파일에서 db에 따로 포트를 지정하지 않아서 만들지 않은 것으로 보인다. 그렇기에 아래와 같이 db 포드에 접근할 수 있도록 정의된 서비스 파일을 하나 생성한 뒤, 실행했다.
apiVersion: v1
kind: Service
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: db
name: db
spec:
ports:
- port: 5432
selector:
io.kompose.service: db
이제 frontend 포드를 살펴볼 차례이다. 기존 프로젝트 설계는 frontend 컨테이너는 한 번 생성되어 npm build를 마친 뒤, 종료된다. 그렇기에 frontend 포드 역시 마찬가지로 completed 상태에서 멈춰있어야 한다. 하지만 계속해서 CrashLoopBackOff 가 발생하고 다시 실행되어 completed 상태가 되는 것을 반복했다.
그래서 frontend 의 작업은 한 번만 실행되면 되기에 포드에서 Job 으로 변경하고 포드에서만 사용하는 필드들을 삭제했다.
# frontend-deployment.yaml
apiVersion: batch/v1
kind: Job
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: frontend
name: frontend
spec:
template:
metadata:
annotations:
kompose.cmd: kompose convert
kompose.version: 1.34.0 (HEAD)
labels:
io.kompose.service: frontend
spec:
containers:
- image: kmj951015/tail-passengers_frontend:first
name: frontend
volumeMounts:
- mountPath: /app/dist
name: dist
restartPolicy: Never
volumes:
- name: dist
persistentVolumeClaim:
claimName: dist
이러니 더 이상 에러도 발생하지 않고 모두 잘 동작했다. 아직 nodePort를 설정하진 않았기에 middleware 포드에 포드 포워딩을 걸어서 443 포트를 30443으로 접근할 수 있게 하고 접속하였다.
kubectl port-forward service/middleware 30443:443
페이지 자체는 접근이 가능하지만 nginx가 403 에러를 발생시켰다. 아직 에러가 끝나지 않았다.

이에 대해 고민하고 있을 때, 같은 팀은 아니지만 이 프로젝트를 진행한 다른 친구가 frontend가 끝나는 것 자체가 조금 이상하다며 middleware 부분과 합칠 것을 제안했다. middleware를 통해 접근하는 것 자체가 이상하다고 말이다. 듣고 보니 일리가 있었다. 또한 규모가 커지고 우리처럼 frontend가 종료되지 않는다면 합치는 것이 더 좋지 않겠지만 분리된 상태에서 nginx 조정하는 것의 번거로움과 frontend가 종료되고 끝나는 점, 프로젝트의 규모가 작은 점을 고려하여 두 포드를 합치기로 하였다.
그렇기에 job을 다시 pod로 바꾸고 middleware 포드의 부분을 frontend 포드에 병합하고 도커 이미지도 새로 생성하여 실행하였다. 또한 frontend 의 서비스에서 type으로 NodePort를 지정하여 포트포워딩 작업 없이 바로 접근할 수 있도록 수정하였다.
# frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
labels:
app: frontend
name: frontend
spec:
ports:
- name: "80"
port: 80
targetPort: 80
- name: "443"
port: 443
targetPort: 443
nodePort: 30443
type: NodePort
selector:
app: frontend
변환 과정을 해보면서 정말 설계가 중요함을 다시 한 번 느낀다. docker-compose.yaml 이 좀 더 짜임새있게 만들어졌다면 kompose와 이미지 문제만 해결하여 마무리 될 수도 있었을 것 같다.
그래도 쿠버네티스로 바꾸고 나니 프론트, 백 코드가 바뀌지 않는다는 전제 하에 실행하고 종료하는 것, 그리고 목적인 elk 스택을 추가하기엔 더 쉬워졌다. 다음 포스트에선 kompose 없이 직접 전환하며 쿠버네티스를 더 공부할 예정이다.
위 코드들은 모두 여기에 있다.