🤬 에러 천국 인프라 작업!
프로젝트가 마무리 되고, 오류와 무응답으로 점철된 나의 인프라 생존기도 끝이 났다.
백엔드로 시작하여 인프라로 끝났는데, DB보다 네트워크에 더 매달려있게 될 나의 운명을 미리 알았다면 Redis
와 JPA
보다 먼저 공부할걸 그랬다.
운영체제와 네트워크에 대한 기본적인 이해조차 전무했던 내가 면접을 위해 공부했던 CS 지식에 의지하여 풀어나가기엔 헤쳐나가야할 역경이 너무나도 많았다.
나처럼 EC2
서버 및 Jenkins pipeline
을 이용한 배포와 openvidu
를 활용한 WebRTC
작업을 하게 될 수많은 후배님들을 위해 오류일지를 남겨놓는다.
도움이 되셨다면 팔로우랑 댓글 한번씩 달아줘잉
🔷 인프라를 처음 맡게 되고 가장 먼저 닥친 첫 번째 과제는 Docker
활용이었다.
Linux
라는 운영체제가 너무 낯설었던 비전공자...
하지만 기초를 알고나면 대강의 이해가 쉬워진다.
🔷 Why Docker?
위의 링크에도 왜 Docker
를 사용하는지 나와있다.
다시 언급하자면, Docker
의 컨테이너 기술은 서버 운영과 배포에 있어서 필수적이면서도 유용한 기능들을 제공한다.
compose 파일 한 번의 실행으로 프로젝트가 필요로 하는 모든 서버가 한번에 열릴 수 있다는 점은 굉장한 장점으로 작용한다.
3대의 Database, 1대의 webSocket 서버, 1대의 시그널링 서버를 테스트환경에서부터 쉽게쉽게 구축하려면 필수적인 것이었고, IntelliJ를 사용할 때 스프링부트 프로젝트 최상단에 넣어두고 실행만 시켜도 프로젝트와 함께 컨테이너로 DB와 각종 서버도 한번에 열리니 편안하지 않을 수 없었다. 볼륨 파일 설정으로 테이블과 값까지 넣어둔 채로 시작이 가능하니 더더욱 그러했다.
다만...
🔷 컨테이너 빌드 시의 Health check 필요성
springboot-app을 Dockerfile화 하여 compose 파일 내에서 컨테이너로 빌드되게끔 설정한 후, 한 가지 오류가 발생했다.
자꾸만 springboot-app이 빌드도중에 DB에게서 정보를 받아오지 못하여 막히는 것.
사실, compose 파일 내에서의 컨테이너 빌드 순서와 상관없이, DB의 서비스 상태가 스프링부트 프로젝트 빌드 시에 완전하지 않을 수도 있다.
DB들의 서비스 상태가 완전하지 않은데 이를 읽으려 했으니 문제가 발생했던 것.
이를 해결하기 위해 우선 DB 별로 Health check
를 추가하고, depends_on 설정을 springboot-app에 추가하여 의존성을 넣어주었다.
# mysql, mongodb 생략
redis:
image: 'redis:latest'
ports:
- '6380:6379'
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 10s
retries: 5
springboot-app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
- mongodb
- mysql
- redis
restart: "no"
그럼에도 작동이 안되길래, springboot-app 실행 스크립트 내에 DB들의 서비스를 체크하는 코드를 넣었고 마침내 로컬 내에서 작동에 성공했다.
로컬 내에서는 분명 작동했다.
🔷 배포할 때는 왜 못읽는건데???
이번엔 같은 compose 파일인데 파이프라인을 통한 빌드 과정에서 읽지 못하는 상황이 발생했다.
분명 파일 빌드까지는 완벽하게 됐고, Nexus에서 받아온 파일이 위치하는 것까지 확인했다.
그럼에도 compose 파일 내에서 volume을 통해 DB를 생성하는 부분에서 볼륨 파일을 읽지 못했고...
스프링 프로젝트를 Dockerfile
을 통해 이미지화 시킨 것 역시 내부에서 빌드 스크립트는 정상 작동하면서 정작 배포를 위한 실행 스크립트는 읽지 못했다.
해당 오류들의 공통점은 바로 파일 내에서 다른 파일에 접근했다는 점.
이따 Jenkins
파트에서 설명하겠지만 Jenkinsfile
을 프로젝트 최상단에 넣어두고 해당 파일 스크립트를 읽어들이는 식으로 빌드와 배포를 진행했었다.
찻 번째 문제는 Jenkinsfile
내 권한 부여 문제였다.
chmod +x ./스크립트 등...
파일에 다른 파일에 대한 접근 권한을 부여해주지 않으면, 그 친구가 읽지를 못했던 것이다.
Springboot-app
은 script.sh
에 접근 권한이 없으니 실행을 못하고, compose 파일은 다른 볼륨파일에 접근 권한이 없으니 실행을 못한 것...
빌드 스크립트 내에 권한 부여를 위의 명령어로 넣어도 되고, 볼륨 마운트 시에 클라이언트가 해당 디렉토리 인증서를 사용하는데 필요한 서비스 및 빌드 컨테이너를 탑재해두는 방법도 있다.
😭 이 사실은 프로젝트가 끝난 후에 회고하다 알게되었다. 참으로 가슴아픈 일이다.
당시에 볼륨 파일은 이용하지 않는 식으로 어찌저찌 해결했다지만 스프링부트 앱 Dockerfile의 스크립트는 또 다른 문제였다. 이를 설명하기 위해선 Jenkins
파트로 넘어가야한다.
이곳에 초록불을 띄우려 100번을 try 했다...
쎄트렉아이 기업연계 플젝 덕에 빌드 및 배포 자동화를 찍어먹어보았던 필자.
젠킨스가 초면은 아니기에 익숙할줄 알았다.
docker를 통한 EC2 인스턴스 내부에서의 설치, 깃랩과의 연결까지는 완벽했다.
그런데 이 녀석, 배포에 들어서자 compose 파일을 읽지 못한다...?
Jenkins
를 컨테이너로 구동 시, 컨테이너 내에 docker가 없다면 이 녀석은 compose 파일을 읽지 못한다.
그 사실을 알자마자 머리가 뜨거워지는 것이 느껴졌다.
하지만 곧 DinD
(도커 안의 도커) 기술의 존재를 알게되었다.
💡 DinD
도커 바이너리를 설정하고 컨테이너 내부의 격리된 Docker 데몬을 실행하는 작업.
CI 측면에서 접근한다면 job을 수행하는 Executor가 Docker Client와 Docker Daemon 역할까지 하게 되어 도커 명령을 수행하는데 문제가 없어진다.
이미 도커로 올라간 컨테이너 내에 또 도커를 올려야한다는게 무슨 말인가 했었지만, jenkins가 직접 docker compose를 읽기 위해서 젠킨스 컨테이너 내의 docker를 이용해야한다는 것으로 이해하니 납득이 갔다. 이 외에도 DooD
등의 방법이 있었지만 일단 이해한 방법대로 진행하자면...
Jenkins
컨테이너 내에서 Docker
를 설치한다.이랬다.
실제로 compose 파일 실행에 성공하여 DB 컨테이너가 성공적으로 올라갔다.
하지만 우리 Jenkinsfile은 아직 수많은 오류들을 남겨놓고 있었으니....
권한을 줬는데 왜 읽질 못하니
이 녀석, 파일 실행을 위한 권한을 줬음에도 실행 파일(jar)을 못 읽는다.
빌드까지는 정상작동했는데, 실행을 못한다는게 이게 말이야 방구야?
이거 하나 때문에 인간젠킨스가 되어 배포파일 실행만 수동으로 하는 도중...
컨설턴트님:
배포때 권한 오류가 생기는 이유는 sudo 명령어를 잘못 써서 그렇습니다. sudo 명령으로 파일을 생성하게 되면 파일이 root 권한으로 생성되거든요. root 권한으로 생성된 파일은 jenkins 계정이나 ubuntu 계정으로 수정할 수 없기 때문에 권한 오류가 생기는 거죠.
???
root 권한으로 생성된 파일을 다른 계정으로 실행할 수 없다는 건 알겠다.
근데 나는 sudo 명령어로 빌드를 한 적이 없었는데, 어째서 root 권한으로 생성이 되는거지?
알고보니 사건의 전말은 이러하다.
아까도 말했다시피 프로젝트 최상단에 Jenkinsfile
을 넣고 이를 파이프라인으로 인식하여 읽게끔 하는 방식으로 빌드와 배포를 진행했었는데,
별 다른 설정을 안해두면 이 모든 과정이 root
권한으로 진행이 된다는 것...
젠킨스파일 안에서 현재 스크립트를 실행중인 유저를 확인하니 정말 root로 진행중이었다.
참으로 골때리는 일이었는데, 이는 젠킨스파일을 통한 빌드 대신, 젠킨스 GUI 내에서 파이프라인 스크립트를 직접 등록하는 식으로 해결했다.
직접 등록하면 유저가 jenkins로 시작하고, 실행까지 jenkins로 진행되니 성공할 수 있었다.
😃 와! 백엔드 배포가 성공적이면 프론트도 할만하겠는데요?
사실 지옥은 이제 시작이었다.
SSAFY인 대다수가 WebRTC 구현을 위해 트라이해보고,
"어 뭐야 쉽네?"
라고 생각하고 대수롭지 않게 사용하려다가 크게 피본다는 전설의 라이브러리이다.
Openvidu
에 대해 공부하며 이것저것 레퍼런스를 찾아본 결과 얻은 결론은,
"늪에 빠져 사경을 헤매는 사람은 많은데, 건져주는 사람은 단 한 명도 없다."
였다.
Openvidu
가 자체적으로 제공하는 서비스들을 그대로 사용하려하면 반드시 피를 볼 수 밖에 없다. 개인적으로, 인프라를 맡은 사람이 앵간한 고인물이거나, 백엔드와 프론트엔드에서 WebRTC 부분을 맡은 사람이 기술과 Openvidu
에 대한 이해도가 높은 것이 아니라면 그냥 안쓰는 것을 추천한다.
🔷 Why?
1. 레퍼런스가 너무 적다.
Openvidu
를 내부적으로 전부 뜯어 커스텀했다는 한 썩은물의 기록(...)2. 충돌의 여지가 많다.
Openvidu service
의 이미지를 그대로 사용하면 내부 compose 파일을 통해 서비스 nginx
서버, coturn
,kurento media 서버
, app
이 따라온다. 이들은 사용자들의 프로젝트의 기존의 서비스 포트와 부딪히는 경우가 굉장히 잦은데, Openvidu Service
내부 compose 파일을 직접 건드리는 것에 대해 Openvidu
가 경고하고 있다...nginx
포트가 기존의 nginx
와 겹치거나, java
포트가 겹치는 등의 이슈가 꾸준히 보고되고 있다. 애초에 포트를 돌렸다 쳐도 이해도 없이 서비스를 그대로 사용하려하면 기존 nginx
에서 사용하던 도메인을 여기 등록했다가 난처해질 수 있다...3. 상당히 폐쇄적이다.
nginx
를 커스텀해도 프로젝트를 읽지 못하는 경우에 대한 글이 꽤 있는데 대부분이 이런 이유에서이다... 그 누구도 이렇다할 해결책은 못찾은 듯 했다.4. Service 대신 시그널링 서버만 쓰겠다면, 사실 쓸 이유가 없다.
coturn
이나 media
서버에 대한 설정은 일절 없다.STUN/TURN
서버와 media
서버 활용에 있는데 이를 하지 못하면 그냥 웹 소켓 기술과 별반 다를게 없다.call 서비스
등을 사용하려다 실패하고 튜토리얼 image를 응용하여 WebRTC라고 하는 경우가 잦은데, 기술 명세서에 따로 STUN/TURN
서버나 Media
서버에 대한 언급이 없다면 WebRTC라고 부르기 너무나도 민망한 것...우리는 그래서 Peer.js
를 이용하여 시그널링 서버를 구축하였고,
추후에 STUN/TURN 서버와 Kurento media 서버를 붙여 이용하려 했으나 시간 상 이를 놓쳤다.
Openvidu에 대한 미련을 더 빠르게 내려놓았다면 어땠을까 하는 생각에 아쉬움이 뒤따른다.
하지만 한편으로는 이 Openvidu 덕에 나름대로 익히고 가는 것도 있었다.
같은 포트끼리의 충돌을 막기 위해 서비스를 다른 포트로 여는 법이라던지,
여기서 언급할 Nginx
의 리버스 프록시
설정에 대해서도 알고 가게 되었다.
💡 Nginx
웹 서버, 리버스 프록시 서버, 메일 프록시 서버 및 일반적인 TCP/UDP 프록시 서버로 사용할 수 있는 고성능의 오픈 소스 소프트웨어
💡 Reverse proxy(리버스 프록시)
클라이언트와 웹 서버 간의 중개자 역할을 하는 서버, 클라이언트로부터의 요청을 대신 받아 웹 서버에 전달하고, 웹 서버의 응답을 클라이언트에게 전달하는 역할을 한다.
리버스 프록시는 로드밸런싱, 보안 강화 등의 장점을 가지고 있다.
필자는 프로젝트 초창기에 EC2 인스턴스에 Nginx를 설치하면서 certbot
을 이용해 인증서 관리를 하며 https
로 모든 요청이 돌아오게끔 설정했었다.
하지만 인프라의 바램과는 달리 팀원들이 개발한 다양한 서비스들이 http로 들어오기 시작했고...
이를 받아들이기 위해서는 서버 설정을 필수적으로 해야만 했다.
location /api/ {
proxy_pass http://localhost:8084/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
certbot에 의해서 모든 http 요청(80 port)은 https 요청(443 port)으로 들어오게 되어있다.
인증서 관리 역시 자동으로 해주었고, 덕분에 이를 크게 신경쓸 필요는 없었다.
중요한건 http로 보내야하는 요청에 대해 https로 보내는 요청으로 변환하여 주어야 한다는 것.
이런 식의 location 설정을 통해 https://도메인/{path}
요청으로 변환되어 넘어간다.
보안에 큰 도움을 주고 서버 부하를 분산시켜주는데에 큰 이점을 가질 수 있어 좋았지만, 설정에 있어서 많은 지식을 요구했던 분야였다.
인프라 구축에 있어서, 이 외에도 다양한 오류가 있었다.
대부분 자잘자잘한 오류였음에도 쉽게 해결되는 경우는 거의 없었다.
네트워크와 OS에 대한 이해는 인프라 작업을 위한 필수적인 것이라는 점을 깨달았다.
비전공자로서 부딪힌 많은 난제들을 있는 힘을 다해 해결했다는 점에서 이번 프로젝트는 팀적인 측면에서도, 개인 성장의 측면에서도 제법 성공적인 프로젝트였다고 생각한다.
ps. Nexus에 대한 다른 글은 따로 게시할 예정