사건의 발단

마지막 배포가 언제더라? 제가 총대 메고 하겠습니다!

42 서울에서 AWS 크레딧 지원이 종료되면서 잘 사용하던 EC2와 RDS를 사용할 수 없게 되었고, 이에 배포는 수동으로 서비스 중인 서버에 들어가서 해야 했으며 개발 DB는 로컬로 사용해야하는 일이 벌어졌다.
그 환경에서 갖가지 리팩터링과 테스트들, 그리고 일부 치명적인 버그 픽스 내용들이 쌓여 있었고 이를 배포해야 했다.
마지막 배포일을 살펴보니 작년 9월이어서 이건 좀 심하다 싶어 내가 총대를 메고 배포하기로 했다.

어… 총이 안 나갑니다?

배포를 위해 서버에 접속하고 가장 먼저 한 일은 서버에 있는 프로젝트를 최신으로 업데이트 하는 것이었다. 어차피 백엔드 컴파일 자체는 문제가 없는 건 깃허브 액션으로 확인했으니 죽이되든 밥이되든 일단 돌아는 갈 거라 생각하고 나는 아무 생각 없이 docker compose up --build -d를 했다.
그러자 어김없이 나타난 이미지 빌드 단계의 에러. 나는 이때까지만 해도 그럴 수 있지 하면서 에러 내용을 살펴봤다.

고난과 역경의 4일

첫 번째 에러: 이미지 빌드가 안 된다

내용은 대충 “파이썬 패키지를 설치할 때, 외부에서 관리되는 파이썬을 알파인에서 설치하려 함”이었다.
그래서 python3 -m ensurepip 명령어를 실행하라 했는데 이미 Dockerfile에 적혀있었고, 실행까지 된 상태였다.
pip를 실행하는 명령어랑 &&로 묶어서 실행도 해보고 이것저것 해봐도 안 되길래 구글링을 해보니 가상환경 안에서 pip를 실행하라는 글을 보았다.
따라서 이 에러는 가상환경을 만들어서 해결했다.
하지만 더 큰 난관이 기다리고 있었으니…

컨테이너가 Created이긴 한데 뭔가 이상하다

컨테이너가 생성은 되었지만, 서비스가 제대로 동작하지 않았다. 웹사이트는 ERR_CONNECTION_REFUSED를 띄웠고 설마 컨테이너가 아직 뜨지 않았나 싶어서 docker ps로 확인해보니 컨테이너는 떠있었다.
에이 아닐거야 싶어서 docker logs로 로그를 확인해봤는데, 백엔드와 데이터베이스 컨테이너는 잘 up이 된 것을 확인했다.
그렇다면 문제는 nginx.

두 번째 에러: index.html을 못 찾겠어! (nginx)

nginx 컨테이너의 에러 내용은 대충 아래와 같았다.

nginx     | 2024/01/28 05:26:23 [error] 29#29: *1 "/etc/nginx/html/index.html" is not found (2: No such file or directory), client: 222.239.104.219, server: server.42library.kr, request: "GET / HTTP/1.1", host: "42library.kr", referrer: ""

앞으로 구르고 뒤로 굴러도 저건 index.html을 못 찾겠다는 에러였다. 그러면 찾게 만들어주면 되지.
이 에러는 단순히 docker-compose.yaml 파일에서 volumes를 프론트엔드 필드 폴더와 연결하여 해결했다.
대충 아래와 같은 내용이었다.

volumes:
  - ./frontend:/etc/nginx/html

그럼 이제 돌아가니? 라고 생각했지만, 이것도 아니었다.

일단 배포 이전으로 돌려야겠어

이제 index.html도 잘 찾는데, 왜 아직도 ERR_CONNECTION_REFUSED가 뜨는 걸까. 이때가 지난 주 일요일 저녁 즈음이었을텐더 저녁에 약속이 있었던지라 일단 원상복구는 시켜놔야겠다 싶어서 서버의 코드를 이전 버전으로 돌렸다.
이후 컨테이너를 다시 띄우는데…

“왜 또 똑같은 에러가 뜨는 거지?”

이때 들었던 생각이 “컨테이너 내부에서 뭔가 작업을 했구나” 였다. 코드 상에는 반영되지 않은, nginx 컨테이너 내부에서 이뤄진 작업들.
그래서 나는 급하게 깃허브에 이슈를 만들어 내가 겪은 에러들과 해결 방법을 적어놨다. 뭔가 딱 봐도 다시 볼 일이 있을 것 같았다.

그리고 두 번째 에러까지의 내용들을 전부 다시 적용했고, 이 시점에서 나는 약속을 취소했다…(엉엉)
docker compose logs nginx로 nginx 컨테이너의 로그를 확인했다.

세 번째 에러: 이게 뭐지? 파악조차 안 된다

nginx     | 2024/01/28 09:33:51 [error] 29#29: *32 open() "/etc/nginx/html/cdn-cgi/trace" failed (2: No such file or 
directory), client: 139.59.101.104, server: server.42library.kr, request: "GET /cdn-cgi/trace HTTP/1.1", host: 
"speed.cloudflare.com"
nginx     | 139.59.101.104 - - [28/Jan/2024:09:33:51 +0000] "GET /cdn-cgi/trace HTTP/1.1" 404 153 "-" "Mozilla/5.0" "-"
nginx     | 175.193.40.212 - - [28/Jan/2024:09:35:10 +0000] "GET / HTTP/1.1" 200 803 "-" "Mozilla/5.0 (iPhone; CPU 
iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1" "- 
"

도저히 이 에러는 무슨 말인지 모르겠어서 구글링을 해봤다. 그런데 이 에러는 구글링을 해도 나오지 않았다.
이때 살짝 느낌이 오기 시작한게 “이건 내 손을 떠난 문제인 것 같은데?” 였다. 그래도 cdn-cgi가 뭐지 하고 구글링 해보니까 Cloudflare가 나오고 도메인과 관련된 문제인 것 같았다.

이때 내가 설마 싶어 nginx의 설정 파일을 열어서 server_name을 살펴봤는데 에러에서 나온 도메인과 같은 도메인이 있었다. 그래서 대충 nginx에서 도메인에 뭔가 연결을 하려는데, 실패했다는 감 정도는 잡았다.

선조의 지혜를 빌리자

sos

이때 나는 집현전 동료들에게 도움을 요청함과 동시에 기존에 있던 다른 백업용 docker-compose.yaml 파일을 찾아보았다. 뭔가 거기에 힌트가 있을 것 같았다. 그러자 docker-compose.yaml__ 파일에서 내가 놓친 부분을 찾았다.

  1. nginx 컨테이너의 포트는 80만 열려있고, 443은 열려있지 않았다.
  2. nginx 설정 파일의 server_nameserver.42library.kr이 아닌 42library.kr이라고 적혀있었다.

그리고 폴더 구조를 뒤적이다가 certbot이라는 폴더를 발견했는데, 이전에 Inception 과제 하면서 SSL 인증에 관련된 것임을 알았고 설마 집현전에서 이걸 사용하나 했다. 다시 nginx 설정 파일을 뒤져보니 집현전은 내가 알기로는 https를 사용하는데 그 어디에도 ssl 설정이 없었다.
이때 나는 “이거 SSL 문제 아니면 난 진짜 원인 모르겠다.” 라고 생각했다. 이때가 아마 새벽 2시 쯤이었을 것이다. 그래서 자고 일어나서 다시 해결해보기로 했다.

2차전 시작: 목표는 SSL 설정

자고 일어나서 2차전 시작.
이때 불현듯 든 생각이 “curl 명령을 써볼까?” 였다. 그래서 나는 curl 명령어를 사용하여 42library.kr에 요청을 보냈다.

curl --insecure -v https://42library.kr

당연히 에러. 여기서 나는 혹시나 하는 마음에 명령어를 하나 더 실행해봤다.

curl --insecure -v http://42library.kr

결과는 index.html이 응답으로 왔다. 이때 나는 거의 90%의 확신을 가지고 “이건 SSL 문제다” 라고 생각했다 (10%는 혹시 모르니까ㅎ).

이제 나는 SSL 설정을 위해 certbot을 사용하기로 했다. 서버 내부 그 어디에도 인증서 파일이 없었기 때문에 이러면 nginx 컨테이너 내부에서 인증서 작업을 했을 거라 생각했다. 나는 nginx 컨테이너에 들어가서 certbot을 설치했다.

apk add python3
python3 -m venv .venv
. .venv/bin/activate
pip install certbot certbot-nginx

자, 여기서 컨테이너를 내렸다가 다시 올리면 어떻게 된다? 저걸 다 다시 해야한다. 이래서 스크립트 파일을 쓰는 걸 뼈저리게 깨닫고 지금은 급하니 나중에 스크립트로 만들어야겠다고 생각했다.

그 다음부터는 nginx 컨테이너 내부에서 certbot을 사용하여 인증서를 발급받고, nginx 설정 파일을 수정하여 ssl을 사용하도록 설정했다. 그러면 이제 행복해질 수 있는 걸까?

한결같은 에러: ERR_CONNECTION_REFUSED

도대체 뭐가 문제인 걸까. 이제 나는 정말로 모르겠다고 생각했다. certbot에서 Congratulations도 보고 했는데 뭐가 문제인 걸까. 이때 난 슬랙의 에러 스레드에 아래와 같이 글을 적었었다.

find_clue

(저때가 2차전 시작 때 certbot 뭣도 모르고 계속 삽질하다가 인증서 재발급 막혀버린 시점이었다)

빛이 보인다

저 글이 달리고 얼마 안 있어서 집현전 동료분의 답변이 왔다.
그분은 나에게 아래와 같이 답변을 해주셨다.

“인증서 갱신하는 크론 작업 설정 안 했다.”

난 이때 “마참내..!” 라는 생각과 내가 제대로 짚고 있었구나 하는 생각이 들었다.
그리고 dns로 인증서를 받는 방법이 있다고 말씀해주셔서 한 번 해보겠다 말씀드리고 방법을 구글링 한 뒤, 나는 다시 certbot을 사용하여 인증서를 받았다.

여담이긴 한데 저 19시 38분이 UTC 기준이었다는 걸 뒤늦게 깨달았다. KST 말 없으면 무조건 UTC라고 생각하면 되나..?

3차전 시작: DNS로 인증서 받기

이제 나는 certbot을 사용하여 DNS로 인증서를 받기로 했다. 구글링을 해보니 manual 모드로 인증서를 받아야 했기 때문에 certbot을 사용하여 인증서를 받을 때 manual 모드로 받기로 했다.

처음에는 Enter를 그냥 눌러버려서 DNS에 등록하기도 전에 인증서를 받아버려 실패했다.
두 번째 시도 때는 Enter를 누르지 않고 KEY TXT를 받은 후에 이걸 집현전 동료분께서 DNS에 등록하실 때까지 기다리고 있었고, 등록이 끝났다는 메시지를 받고 Enter를 눌러 진행했다.
제발 이번에 본 Congratulations 메시지가 마지막이길 빈다..!

난 이제 행복해질 수 있어!

나는 다시 컨테이너를 내렸다가 올렸다. 그리고 결과는 성공!

끝나고 돌아보니 생각나는 것들

이 글을 쓰면서 돌아보니 드는 생각인데 Inception 하고 나서도 느낀 거였지만 진짜 로그만 잘 봐도 뭔가 많이 알 수 있었다.
웬만한 내용은 다 로그에 나와있었고, 그걸 보고 어떻게 해결해야 할지도 알 수 있었다.

그리고 이런 문제들은 인공지능보다는 차라리 사람들이 겪은 내용을 정리해둔 블로그가 더 도움이 많이 됐었다.

또 한 가지 더. 난 급하게 문제 해결한답시고 호스트에다가 인증서 받아서 볼륨 연결해두고 했는데, 아예 인증서만 따로 취급하는 컨테이너를 사용하는 방법도 있다고 한다. 실제로 집현전 동료 중 한 분이 그걸 시도하고 계셨던 흔적이 있었다.

아마 다음 배포 관련 글은 인증서 컨테이너를 사용하는 방법에 대한 글이 될 것 같다. 다음 배포 전에는 꼭 해봐야겠다.

댓글남기기