이 글은 작년에 쓴 글 [육군훈련소에서 인터넷 편지 편하게 받기] 이후의 이야기입니다.
어느새 훈련소를 다녀온지도 1년이 지났다. 인터넷 편지 사이트를 운영했던 서버에 들어가 보니 cron 으로 돌려놓았던 인편 프로그램은 여전히 자신의 본분을 다하며 하루에 한 줄씩 로그를 꾸준하게 찍고 있었다. 그동안 참 많은 일이 있었다.
내가 훈련소에 다시 들어갈 일은 특별히 없을 것이기에, 솔직히 말하면 잊고 지냈던 프로젝트이긴 하다. 급하게 만들기도 했고, 시간을 갖고 개발했다면 그렇게 하지는 않았을 것 같다. 그런데 잊을만하면 주변에서 이 사이트에 대해 물어보는 연락이 오곤 했다. 당장 기억나는 것만 5번 정도이다. 사이트 구축 방법이 궁금해서 물어보는 경우도 있었고, 자신이 훈련받고 있는 동안 서버 관리를 담당해 달라는 부탁을 받기도 했다.
최근 훈련소 가기 너무 좋은 날씨가 되어서 그런지 입소한 지인들이 굉장히 많아졌다. 고등학교 동기, 과 동기, 과 후배들이 입소하게 되었고 그중에는 친한 사람도 굉장히 많았기에 여력이 되는 한 인터넷 편지 사이트를 운영해 볼 생각이었다.
이 생각을 실현으로 옮기게 해 준 것은 결정적으로 과 후배님이었다. 1년 전 나의 글을 보고 아이디어를 얻어서 자신도 인터넷 편지 사이트를 구축했다는 연락을 받았다. 확인해 보니, 인터넷 편지뿐만 아니라 나는 시간이 없어서 하지 못했던 뉴스 크롤링까지 구현해 두었다. 마침 도커와 쿠버네티스를 한창 배우던 중이었기에, 인터넷 편지 사이트와 뉴스 크롤링 앱을 컨테이너화 하면 좋겠다는 생각을 하게 되었다.
후배님 글은 아래 링크에서 확인할 수 있다. 훈련소에서 심심하지 않았으면 좋겠다는 사리사욕을 채우고자 만들었던, 어찌 보면 관종 같은 프로젝트에 많은 관심을 가져주시니 감사할 따름이다.
기획하고 구현하기 vs 구현하고 기획을 수정하기
다만 도커와 쿠버네티스를 사용하려고 하다 보니, 프로젝트를 어떻게 설계 해야할지 고민을 굉장히 많이 했다. 퇴근길에 버스에 앉아서 컨테이너화는 어떻게 할 것이고, 어떤 쿠버네티스 리소스를 사용해서 서비스하면 좋을지 아이패드를 꺼내 그림을 그리며 생각했다. 그렇게 해서 아래 설계 도면(?) 을 얻었다.
다 하고 나서는 공부한 내용을 적용할 수 있어서 이렇게 할 수 있다면 정말 좋을 것 같아 정말 뿌듯하다고 생각했고 기대가 많이 되었는데, 막상 구현을 하다 보니 변수가 생겨서 이 설계를 100% 따라가지는 못했다. 그래도 설계를 했던 것을 감안하여 구현했고 일부 형태는 비슷하게 사용했다.
공부하기 전에 꼭 책상 치우는 스타일
약 1년 만에 다시 방문한 인터넷 편지 레포는 그야말로 엉망이었다.
커밋 내역을 보니 퇴소한 이후 파일 정리하기 귀찮아서 zip 으로 압축해버린 다음 레포에 툭 던져 놓은 것 같다.
그래서 모든 파일을 하나씩 확인하고 필요 없는 파일은 전부 제거했다. 그리고 코드가 해야 하는 일에 따라 폴더를 나누었으며, 코드도 어느 정도 정리했다. 필요 없는 주석과 쓰지 않는 함수들을 전부 제거했으며, 함수가 지나치게 길어 살짝만 리팩터링 했다.
기존 인터넷 편지 사이트는 사용자들이 입력한 메시지를 수신하여 저장하는 receiver (django) 와 저장된 편지들을 일정한 주기마다 더캠프로 전송하는 sender (node.js) 로 이루어져 있었다. 여기에 뉴스를 크롤링할 crawler (python script) 가 추가되었다. 또한 이 순서대로 작업을 할 생각이었다.
Message Receiver
사실 이 부분은 크게 건드릴 부분이 많이 없었다. Django 를 다시 너무 세세하게 들여다보고 싶지 않기도 했어서, 컨테이너 위에 올렸을 때 돌아갈 정도로만 손봐주면 됐다.
Django 프로젝트를 만들게 되면 settings.py
에 SECRET_KEY
가 있다. 이 값을 그대로 컨테이너에 담을 수는 없으니 python-dotenv
를 사용하여 환경 변수로 분리했다. python-dotenv
를 선택한 이유는 Docker / Kubernetes 모두 volume 을 mount 할 수 있기 때문에 .env
파일로 앱의 설정을 넘겨줄 생각이었기 때문이다.
Message Sender
다음으로는 메시지를 보내는 부분을 정리할 차례였다. Sender 도 사실 어렵지 않았다. 대부분 필요 없는 코드들을 쳐내야 했기 때문에 코드를 짜기보다 코드를 많이 지웠다.
Sender 또한 컨테이너화 했고, 두 컨테이너를 함께 실행한 뒤 receiver 쪽에서 메시지를 받아 mount 된 volume 에 저장해 주고, sender 는 volume 에 있는 메시지를 읽어서 전송하도록 했다.
한 번에 되면 오히려 수상해
원래 일이 쉽게 끝나면 놓친 게 없나 돌아봐야 한다. 1년 전에는 됐지만 지금은 안 돌아간다. the-camp-lib
를 사용해서 인터넷 편지를 보내려고 하니 어두컴컴한 터미널 속에는 소리 없는 에러 메시지만 반복될 뿐이었다.
에러 메시지를 보니 육군 카페만 지원된다고 하는데, 더캠프에서 카페를 이용해 보내야 요청이 처리되는 것인가 추측만 할 수 있었다. 어쩔 수 없다고 생각하며 결국에는 the-camp-lib
를 참고하여 만들어진 python 라이브러리 thecampy
를 사용하기로 했다.
결국 django 도 python 이니까 오히려 통합할 수 있다면 쉬울 것 같았기에, sender 와 receiver 를 따로 컨테이너로 분리할 필요가 없어졌다.
Message Receiver - (2)
thecampy
라이브러리 자체가 굉장히 직관적으로 되어있기 때문에 메시지를 보내는 부분은 오히려 구현하기 더 쉬워졌다. SECRET_KEY
를 위해서 dotenv
를 사용했으므로 더캠프 계정 정보와 훈련병의 정보만 따로 빼주면 매우 간단하게 전송 모듈을 구현할 수 있었다. 또한 Sender 가 node.js 였을 때는 crontab 을 이용해 정해진 시간마다 메시지를 보내야 했지만, 이제 sender 도 python 이다 보니 인터넷 편지가 들어오는 족족 더캠프로 보내버리면 됐다.
from .models import Message
from dotenv import load_dotenv
import thecampy
import os
load_dotenv()
ENV = os.environ
EMAIL = ENV['EMAIL']
PASSWORD = ENV['PASSWORD']
NAME = ENV['NAME']
BIRTH = ENV['BIRTH']
ENTER_DATE = ENV['ENTER_DATE']
UNIT_NAME = ENV['UNIT_NAME']
SOLDIER = thecampy.Soldier(
NAME, BIRTH, ENTER_DATE, UNIT_NAME
)
client = thecampy.client()
def send_message(message: Message):
title = f'({message.sender}) {message.title}'
message = thecampy.Message(title, message.content)
client.login(EMAIL, PASSWORD)
client.get_soldier(SOLDIER)
client.send_message(SOLDIER, message)
그럼 여기서 문제는, '전송에 실패한 메시지들은 어떻게 처리하는가'이다.
Message
모델에 전송되었는지 상태를 나타내는 boolean field sent
를 추가했으며, 전송이 성공하면 True
를, 실패하면 False
를 저장하도록 했다. node 로 구현했을 때 전송 성공 여부에 따라 저장되는 파일의 위치가 달랐던 것을 고려하면, 전송에 실패한 메시지를 가져오기 굉장히 쉬워졌다.
그리고 재전송 시도는 주기적으로 해야 했어서, 조금 찾아보니 django-crontab
라는 패키지가 감사하게도 있었다. settings.py
에 아래 내용을 추가하여 테스트했다.
CRONJOBS = [
('* * * * *', 'letter.cron.send_failed_messages', '>> /logs/cronjobs.log'),
('* * * * *', 'letter.cron.test', '>> /logs/t'),
]
테스트 목적이므로 매 분 실행되어야 하며, 두 번째 letter.cron.test
는 cron 이 동작하고 있는지 확인하기 위해서 추가했다.
컴퓨터는 잘못이 없다
보통 그렇다. 잘못 구현한 내가 잘못이다.
django-crontab
은 위와 같이 settings.py
에 cronjob 을 추가하면 python manage.py crontab add
를 했을 때 cronjob 을 crontab 의 문법으로 변환하여 crontab 에 추가해준다. 그리고 cron 이 알아서 crontab 에 추가된 명령을 실행하는 방식이다.
그런데 컨테이너를 실행했음에도 이상하게 로그 파일에 아무것도 찍히지 않았다. 찾아보니 비슷한 문제를 겪었다는 사람들이 있어서 1차적으로 django-crontab
의 기능을 의심했다. 왜 cron 이 안되나 한참을 고민하고 이것저것 건드려 봤지만 동작하지 않길래 이 패키지는 내가 쓸 수 없겠다고 결론을 내리고 다시 node.js sender 때처럼 전송 성공/실패 여부를 디렉터리로 구분하고, django-crontab
이 아닌 진짜 cron 을 이용해 메시지를 재전송하도록 했다.
전에도 그렇게 했으니, 구현 자체는 할 수 있었는데 왠지 패배한 것 같고, 찝찝한 기분을 떨칠 수 없었다. 그래서 다시 한번, 컨테이너 내부에서 django-crontab
을 설치해서 print
한 줄 있는 간단한 함수를 실행하도록 했다.
그래도 동작하지 않았다. 결국 예전에 컨테이너 내에서 cronjob 을 돌리겠다고 삽질했던 컨테이너의 Dockerfile 과 cronjob 파일을 다시 꺼내 보며 똑같이 따라 해 보기로 했다.
그렇게 여러 방법을 시도하던 도중 치명적인 실수가 있음을 알게 되었다. 컨테이너 안에서 cron 이 동작하고 있지 않았던 것이다. 분명 Dockerfile 에서는 service cron start
를 명시했는데, service cron status
는 빨간 글자를 뱉었다.
cron 을 켜고 조금 기다리니 django-crontab
에는 문제가 없음을 알게 되었고, 전송 성공/실패 여부에 따라 디렉터리로 구분하는 방식은 바로 버리고 sent
field 를 이용해 재전송을 구현했다.
컨테이너 내에서 cron 을 자동으로 실행하게 하려면ENTRYPOINT
를cron && ...
로 지정해줘야 하는 것 같다.
이렇게 해서 message-server 라는 컨테이너가 탄생했다!
FROM python:latest
RUN apt update && apt install cron -y && mkdir logs
ADD requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt && mkdir /letters
COPY mysite /letter-server
COPY run.sh /run.sh
ENTRYPOINT cron && bash /run.sh
참고로 run.sh
의 내용은 다음과 같다.
#!/bin/bash
python3 /letter-server/manage.py makemigrations
python3 /letter-server/manage.py migrate --run-syncdb
python3 /letter-server/manage.py crontab add
python3 /letter-server/manage.py runserver 0.0.0.0:80 >> /logs/django.logs 2>&1
Django 의 로그가 저장되지 않는 문제도 있었는데, 이는 stderr 를 stdout 으로 리다이렉트 하여 해결할 수 있었다.
News Crawler
진짜 삽질하던 부분은 다 지나가서 오히려 이 부분은 공놀이 하는 기분이었다.
후배가 만들었던 SyphonArch/news-relay 의 코드를 참고하여 사용했다. 후배님께 '짜신 코드 도커 컨테이너로 말아서 올리고 PR 날릴게요' 라고 했고 후배님이 OK 했었는데 조금 더 정리하면 PR 날려볼 수 있을 것 같다.
가자! Docker Hub 로!
빌드한 도커 이미지는 Docker Hub 에 올려서 calofmijuck/message-server, calofmijuck/news-crawler 로 pull 받을 수 있게 되었다. 그리고 레포가 훨씬 깔끔해졌고, README 도 읽기 편해졌다.
로컬 머신에서 잘 작동하는 것을 확인했다.
On the Road to Kubernetes
쿠버네티스로 가는 길은 조금 험난하다. 험난할 수밖에 없는 게, 굳이 이 프로젝트에 적용할 필요는 없기 때문이다. 인터넷 편지 사이트가 대용량 트래픽을 견딜 필요는 없었고, 롤링 업데이트와 같이 컨테이너를 관리할 일 또한 없었기 때문에, 순전히 배운 것을 적용해 보고자 했던 마음이었다.
그래서, deployment 를 이용해 아래와 같이 구성했다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: letter-server # modify here
spec:
replicas: 1
template:
metadata:
name: letter-server-name # modify here
labels:
trainee: NAME # modify here
spec:
volumes:
- name: env
secret:
secretName: secret-name # modify here
- name: letters
hostPath:
path: /letters/NAME # modify here
containers:
- image: calofmijuck/message-server
name: message-server
ports:
- name: http
containerPort: 80
volumeMounts:
- name: env
mountPath: /env
readOnly: true
- name: letters
mountPath: /letters
- image: calofmijuck/news-crawler
name: news-crawler
volumeMounts:
- name: env
mountPath: /env
readOnly: true
selector:
matchLabels:
trainee: NAME # modify here
# modify here
가 적혀있는 부분은 수신할 훈련병 한 사람마다 deployment 를 띄워야 하기 때문에 수정해야 하는 부분이다.
Pod specification 에 적힌 내용을 하나씩 살펴보자.
- Replica 는 1개로 충분하다.
- Pod 의 label 은 훈련병의 이름으로 주게 될 것이다.
python-dotenv
를 사용했기 때문에 volume 을 mount 하여 파일을 넘겨줘야 한다. 그런데 이 파일 안에는 더캠프 계정 정보가 들어있고, 훈련병의 생년월일 등 개인정보가 들어있기 때문에 Secret 을 생성하여 사용하도록 했다. 그리고 이 volume 은 앱의 설정을 가지고 있기 때문에 읽기 전용으로 mount 한다.- 발송된 편지는 나중에 파일로 받을 수 있게 JSON 으로 저장된다. Pod 가 삭제되면 volume 도 함께 삭제되므로 hostPath volume 을 사용하여 편지가 저장될 수 있도록 했다.
- minikube 에서 테스트했기 때문에, single node cluster 를 가정해서 hostPath 를 사용했다.
- 참고로 minikube 에서 hostPath 를 사용하려면
minikube mount <local_dir>:<node_dir>
와 같이 직접 mount 명령을 입력해야 하고, 이 명령이 실행 중이어야 노드 내 디렉터리가 로컬 디렉터리와 연결된다.
- 컨테이너는 80 번 포트로 요청을 받기 때문에 80 번 포트를 개방하고, 이름을 http 로 붙여서 나중에 Service 에 연결했을 때 포트 이름을 이용해 reference 할 수 있도록 했다.
위 deployment 를 실행하면 ReplicaSet 이 생성되고 pod 는 알아서 관리된다. 이제 service 를 붙여줘야 한다. minikube 에서 가장 간단하게 사용하기 위해서는 NodePort service 를 사용하면 된다. 클라우드였다면 로드 밸런서도 한 번 붙여봤을 것이다.
apiVersion: v1
kind: Service
metadata:
name: letter-server-svc-name # modify here
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: http
nodePort: 30123 # modify here
selector:
trainee: NAME # modify here
이렇게 설정하여 service 를 생성한 뒤 minikube service list
로 실행 중인 service 목록을 조회하면 minikube node 의 IP 와 service 에서 설정한 nodePort 로 이루어진 URL 을 얻을 수 있다. 접속하면 앱이 잘 동작하고 있음을 확인할 수 있다.
네트워크는 너무 어려워
모든 것이 잘 돌아갔다. 하지만 큰 문제가 있었는데, 바로 minikube 는 로컬에서 테스팅 목적으로 쓰이기 때문에 이를 실제로 서비스하기 무척 까다롭다는 것이다. 공유기에서 포트 포워딩을 시도했으나 minikube node IP 는 같은 서브넷이 아니라며 공유기에서 IP 를 올바르게 입력하라고 했다. 또 minikube tunnel 과 같은 방식을 이용해 로드 밸런서를 만들고 external IP 를 부여할 수 있었으나 이 또한 로컬 머신에서만 접속 가능한 IP 였고, minikube tunnel 프로세스가 계속 실행 중이어야 했다.
On premise Kubernetes 에 대해서도 간단히 검색해 봤는데, 세팅이 무척 까다로워 보여 당장 할 겨를이 없다고 판단했다. 마지막으로 하나 해보지 않은 것은 kubectl port-forward
를 이용해 컨테이너의 80 포트로 직접 포워딩하는 방법인데, 이 방법 또한 minikube 의 명령들과 마찬가지로 해당 프로세스가 실행 중일 때만 유효해서 시도조차 하지 않았다.
이런저런 이유로 로컬에서 잘 돌아간다는 것을 확인하긴 했지만, 현 상황에 Kubernetes 를 적용하기는 어렵겠다는 판단을 했고 Docker 로 서비스하기로 했다.
서버에 컨테이너 8개?
현재 훈련소에 있는 지인 2명, 그리고 들어갈 지인 2명까지 고려하여 총 8개의 컨테이너를 서버에 실행했다. 각 사람별로 env 폴더 만들어서 계정 정보 넣어주고, 컨테이너만 실행하면 끝이었기 때문에 띄우는 것 자체는 간단했다.
서버를 띄운 뒤 고등학교 친구들에게 링크를 공유했더니, 신기해하며 훈련소에 있는 친구에게 안부를 전했다. 인터넷 편지 사이트를 만들겠다고 그 친구에게 사전에 얘기하지 않았어서 아마 막사 안에서 신기해할 것이다. 친구가 조금 덜 심심했으면 좋겠다.
구현을 마치고 배포까지 다 하고 나니 1년 동안 참 많은 경험을 했다는 걸 깨닫게 되었다. 인터넷 편지 사이트라는 게 사실 대단한 것도 아니긴 하지만, 1년 전의 나라면 절대 지금처럼 생각하고 구현하지 못했을 것 같다.
많이 늦은 편이지만 이제라도 컨테이너 기술에 대해서 공부하고 사용해 볼 수 있어서 유익한 경험이었다. 물론 주말을 통째로 부었지만 남는 게 많다고 생각한다. 기술적인 부분 외에도 앞으로 훈련소에 들어갈 지인들을 위해 내 서버를 굴릴 수 있다면 나는 기꺼이 그렇게 하겠다. 기술적으로 뛰어나지 않을지라도, 그게 곧 의미 있는 일이고 그게 남는 것이라 생각한다.
특별히 각자의 위치에서, 먼 곳에서도 항상 저를 응원해 주시는 분들께 감사하다는 말씀 드리고 싶습니다. 언제든 편하게 연락 주세요.
Repository Link
'Computer Science > Web' 카테고리의 다른 글
육군훈련소에서 인터넷 편지 편하게 받기 (0) | 2020.06.13 |
---|---|
Docker 기초 (0) | 2020.02.27 |
Tistory 이주 (0) | 2020.01.10 |
github.io 사이트 설정 - 2 (0) | 2020.01.10 |
github.io 사이트 설정 (0) | 2020.01.10 |
댓글