회사의 협력사가 회사가 힘들어져 개발팀도 모두 퇴사한 상황에
운영 중인 프로젝트에 개발이슈가 있어 도움요청이 왔고 디버깅했던 프로젝트 경험담입니다.
그동안 학습하며 배운 지식을 토대로 부딪히면서 겪은 경험위주의 작성으로
작성된 프로젝트를 현재 운영하고 있지는 않지만 잘못된 점은 말씀해 주시면 학습 및 수정하겠습니다.
0. 어떤상황인가?
문제인식
- 협력사에 사정이 있어 기존 개발자들이 퇴사한 상황에서 프로젝트를 인수받음
- 협력사가 필요로 하는 특정 이슈를 디버깅해야 함(java spring boot 기반의 코드수정, 데이터 추출 등)
- 현재 SSL 사설인증서를 사용 중인데 만료시점이 다되어 이를 갱신해야 함
현황
- 협력사 내부에서 사용 중이던 내부 피씨 맥미니(intel chip)에 모든 자료들이 들어있음
- 인수인계용 계정 및 비밀번호등은 다행히 pc내부 작성은 되어있음
- 프로젝트파악
- 협력사의 프로젝트는 총 3개로 모두 스프링부트 2.5.4
- 프론트는 freemaker의 템플릿엔진으로 제작
- 2개의 프로젝트 모두 하나의 AWS EC2, 1개 프로젝트는 cafe24에 호스팅 되어있음
- AWS 프로젝트 Database는 mariadb로 AWS RDS에 dev, production2개 분리배포되어 있고,
cafe24로 호스팅 되어있는 프로젝트는 cafe24 db서버에 배포되어 있음 - 이 프로젝트를 갑작스레(?) 인수받게 된 나는 Node 개발자로,
일전에 약식으로 얕게 학습한 스프링부트 프로젝트를 AWS Elasticbeanstalk으로 배포한 경험이 전부.
개발환경파악 → 인프라파악(접근권한) → 소스코드확인 순으로 업무계획
1. 개발환경파악
어떤 프로젝트가 진짜인지 모르겠다…
- 협력사의 프로젝트는 3개 프로젝트를 운영 중이며 모두 같은 개발환경이다.
- 원격 Repo에서 최신 프로젝트를 pull받아서 작업하면 복잡하게 생각할 문제가 아니지만, 원격Repo에서 운영환경으로 ci/cd pipeline이 구축되어있지 않았으며 인수인계내용을 살펴보면 배포내용은 아래의 순서로 진행되었음
- 로컬프로젝트에서 작업된 스프링부트 jar파일 만들기
- 운영 EC2 서버에 리눅스 scp 프로토콜로 빌드된 jar 파일 업로드
- 운영 EC2 서버에 작성된 배포 스크립트파일 실행
- 로컬디렉터리에 회사명으로 다양한 프로젝트가 생성되어 있는데, 프로젝트명이 디렉토리명과 정확히 일치하지 않았음
예를 들면 projectA는 projectPlus 이런 식인데 유사이름으로 생성된 프로젝트가 상당히 많고 수정일이 비슷비슷하다... - 디렉터리 네이밍이 불분명해 추론해 가며 찾은 프로젝트를 우선 실행함
누군가 쫓기듯이 인수인계해야 하는 게 아니라면 로컬디렉터리먼저 알아볼수있도록 미리 정리하자..
1. 이게 번거롭다면 적어도 프로젝트의 로컬 디렉토리 경로라도 남겨야겠다.
2. 프로젝트를 한다면 적어도 개발환경은 pipeline을 구축해 어떤 작업자라도 원격 Repo에서 최신프로젝트를 받아 이어나갈 수 있도록 알려주자
DB접근권한확인
스프링부트 프로젝트를 실행하니 아래와 같은 사유로 실행이 되질 않았다.
Caused by: java.sql.SQLNonTransientConnectionException: Could not connect to address=(host=localhost)(port=3306)(type=master) : Connection refused (Connection refused)
여러 가지 이유가 있겠으나 우선 db연결문제인 것으로 보아 ip허용 관련 이슈일 것으로 생각되어 각 db서버와 연결될 보안그룹에 인바운드 룰트래픽에 현재 내 아이피를 추가해 주었다.
- 로컬 mysql 서버상태확인
mysql.server stop -> mysql.server start -> mysql.server status SUCCESS! MySQL running (53266) - RDS or Cafe24의 인바운드 트래픽허용에 현재 사용 중인 아이피추가
- RDS> 보안그룹 > 인바운드규칙에 tcp에 내 ip 추가
- cafe24 나의 서비스관리> MySQL 외부 IP접근 설정에서 [설정하기] > 내 아이피 등록
프로젝트 의존성 재구성
"Cannot load driver class: org.mariadb.jdbc.Driver
ip 등의 커넥션이 정상적으로 연결되었더라도 dependency가 제대로 구성되어있지 않다면,
관련 드라이버가 로드되지 않을 수 있으므로 다시 빌드 후 실행해 주었다
./gradlew clean build
2. 배포환경 확인
협력사의 요구사항 검토결과 디버깅이 필요한 요소는
백엔드의 대대적인 로직수정보다는 일부 예외케이스 추가등이었고
프런트는 스크립트상의 수정(jquery부분)이어서 어렵지 않게 해결할 수는 있었다.
문제는.. 이를 바로 운영환경에 배포해야 한다는 것.. 매우 부담 스러웠다
늘 dev-stage-prod로 분리해서 배포되는 git flow 환경에서 작업하다가
release에서 별도의 QA단계 없이 바로 production에 이를 바로 배포해야 하는 상황이 상당히 부담스러웠다.
익숙한 환경이라도 부담스러운데 처음 해보는 자바환경을 가지고 실제 운영 중인 프로덕트에 바로 배포한다는 게...ㅜㅜ
더군다나 내가 지금 작업한 게 최신이 아니면 어쩌지, 그사이 중요한 로직의 수정이 있지는 않았을까 부담스러웠다.
local / dev / integration / qa / staging / production 등 꼭 모든 환경을 갖춰서 개발해야 하는 건 아니겠지만
적어도 qa가 가능한 개발 테스팅환경정도는 구축되어 최종확인 후 실제 서비스에 배포되는 게 좋을 것 같다.
물론 이전 개발자분의 배포 관련 세팅이 잘되어있을 수도 있었지만 인수인계되어 있지 상태에서 진행하다 보니
최대한 관련 코드나 단서를 찾는 게 중요했다.
ec2에 다행히 백그라운드에서 실행할 수 있게 하는 nohup 명령어를 사용하여
서버를 유지하는 방법으로 이미 스크립트 파일이 찾아 배포방식을 확인했고,
현재작업환경이 최신작업물인지 디버깅이 잘 해결된 새 프로젝트 파일이 운영환경에 정상적으로 잘 배포되었다.
하지만 현재 배포상의 문제상 스프링부트앱이 내려지고 다시 올라가는 동안의 약간의 downtime이 발생한다.
다행히 유저가 많지 않아 휘리릭 하고 넘어가긴 했지만(?)
현 인프라 구조상 무중단배포방식이 구현되어있지 않아 개선점이 필요해 보였다.
AWS Lambda를 사용하면 중단 시간 없는(zero-downtime에 가까운) 원활한 배포에 익숙해져있다 보니,
이러한 환경이 아닌 상황에서는 무중단 배포전략이 ux에 큰 영향을 끼칠 수 있다.
3. 인프라 뜯어고치기
사설인증서 만료
현재 프로젝트 환경은 ec2로 배포된 프로젝트 여기에 https가 적용되어 있는데 AWS를 사용함에도 불구하고 SSL인증서를
ACM이 아닌 외부 사설인증서 업체를 사용해 적용하고 있었다.
기존에 사용 중인 certkorea의 외부인증서로 비용을 없애고 ACM 통합서비스와 연동해서 쓰면 무료인증서를 발급받을 수 있으므로,
ALB를 두고 ACM을 적용하기로 했다.
시스템 설계 현황
AWS를 기반으로 설계된 현황은 아래(AS-IS)와 같으며 개선계획(TO-BE)을 수립했다
기존 ec2에 설치된 nginx 웹서버가 로드밸런서 역할을 수행하고 있었고 이를 앞단에 alb를 두고 acm을 적용할 경우
현사설인증서 비용보다는 훨씬 적은 거의 없다시피 한 비용으로 나올 거라 판단했다.
현 설계 문제점
- 외부 인증서 사용으로 인한 비용발생
- 단일인스턴스 설계로 인한 문제
- 재해 복구(DR; Disaster Recovery) 대응 미지원 : 해당 AZ문제 발생 시 서비스 지원불가(SK C&C 판교 데이터센터 화재로 인한 인터넷 서비스 장애 사건)
- 트래픽 증가에 따른 스케일아웃 미지원
- 업데이트 및 배포관리의 어려움: 업데이트 다운타임발생, 수동으로 업데이트를 수행하는데 불편
진행프로세스
협력사의 디버깅 요청의 우선순위는 아래의 순서와 같으므로
- 사설인증서 교체
- 요구사항 수정 등의 hotfix
- 가용성증대
오토스케일링등의 구조적 문제해결은 우선순위에서 미뤄두고 아래 작업을 위주로 수행.
1. 인증서교체
개발용 ec2 구축 환경 복제
- 개발전용 테스트베드가 존재하지 않아 운영 인스턴스로 바로 테스트하기 부담
- 별도의 프리티어 EC2 개발용 인스턴스에서 현재 운영환경 복제 후 테스트 진행
개선 계획에 따른 구조설계
개발 인스턴스 생성 후 개선계획 구조에 따른 설계진행
- 개발계정 초기 세팅 테스트환경
- 로드밸런서(ALB생성)
- 서브도메인 생성
- Nginx 역할 분리
- EC2에 설치된 nginx = 캐싱기능으로 두고 ssl 설정값 삭제
- 로드밸런서와 nginx연결
- 개발환경 프로젝트 빌드 및 배포 테스트
- 생성된 ec2 접속(ec2)
- ssh -i "mykey.pem" ubuntu@ec2-3-3-3-3.ap-northeast-2.compute.amazonaws.com
- JDK 설치(ec2)- 스프링부트환경설치
- sudo apt-get update sudo apt-get install openjdk-11-jdk
- 스프링 프로젝트 빌드 (로컬)
- //프로젝트 폴더로 이동후./gradlew booJar
- ec2키 폴더로 이동후 배포파일 업로드 후 ec2로 복사 (로컬)
scp -i "키.pem" "빌드된 .jar 경로" ubuntu@{경로}:~/파일명무중ec2-3-3-3-3
- scp는 로컬에서 원격으로 파일을 전송하는 파일전송 프로토콜로
- 빌드파일실행(ec2)
- nohup java -jar "스냅샷.jar" &
- nohup은 백그라운드로 지속적으로 실행하는 프로세스
- 포트확인(ec2)
- netstat -tuln 포트 확인 8080
- Nginx 연결
- 설치
- sudo apt-get update sudo apt install nginx nginx -v sudo service nginx status #상태 확인 sudo service nginx start # 실행 sudo service nginx restart # 재실행 sudo service nginx reload #수정된 파일을 적용해 연결을 끊지 않고 재실행 sudo service nginx stop #nginx 중지
- 실행 80 포트에는 nginx서비스가이제 nginx를 이용해 80 포트로 접속했을 때 스프링부트 웹서버인 tomcat의 8080 포트로 리다이렉트 되도록 연결해야 한다
- sudo service start # 실행
- 8080 포트로는 스프링부트 내장 웹서버가 활성화되었다
- 실행할 경우 포트 80 연결된다
- nginx와 tomcat 연결
- 리벅스 프락시 기능제공, 캐싱
- 프로젝트가 저장되어 있는 codecommit에서 프로젝트를 클론 해서 진행해도 되지만 마지막으로 수정한 사항이 반영돼야 하므로 현재 작업 중인 로컬 프로젝트에서 빌드한 파일 전송프로토콜로 ec2에 업로드
설계진행프로세스
- 사전 네트워크 구성
- vpc 생성
- 서브넷 4개 퍼블릭 2 프라이빗 2
- 서브넷설정 편집
- 퍼블릭 서브넷 설정 편집→ 주소 자동 할당 활성화
- Subnet에 EC2 인스턴스 넣기
- ACM으로 SSL인증서 생성 후 인증
- ELB 생성 및 리스너 세팅( 2개의 프로젝트 각각 등록) 네트워크매핑 2개 이상 public 서브넷리스너라우팅 443 포트 리스너 라우팅 대상그룹 → 인스턴스 https 443 포트각 alb route53에서 연결하고자 하는 a레코드로 연결
- 보안리스너 acm으로 golteam
- 80 포트 리스너 라우팅 대상그룹 → 인스턴스 http 80포트
- 보안그룹 80,443 포트 열어둔 보안그룹
- 로드밸런서 기본구성 my-dev-alb-golteam
# EC2에 nginx 설치 및 세팅
cd /etc/nginx/sites-available/
sudo rm default
80 포트로 접근되는 트래픽을 8083과 8085로 포워딩
server {
listen 80;
server_name [dev.프로젝트A도메인.co.kr]
# redirect https setting
if ($http_x_forwarded_proto != 'https') {
return 301 https://$host$request_uri;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header HOST $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass ;
proxy_redirect off;
}
}
sudo vi /etc/nginx/sites-available/dev.프로젝트A도메인.co.kr
server {
listen 80;
server_name dev.프로젝트A도메인.co.kr;
# redirect https setting
if ($http_x_forwarded_proto != 'https') {
return 301 https://$host$request_uri;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header HOST $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass <http://127.0.0.1:8085>;
proxy_redirect off;
}
}
sudo ln -s /etc/nginx/sites-available/dev.프로젝트A도메인.co.kr.conf /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/dev.프로젝트B도메인.co.kr.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
sudo service nginx status
로드밸런서 설정에서 엄청난 삽질을 했다
기존 ec2를 건드리지 않고 새로운 환경으로 구성하려면 우선 vpc를 먼저 생성하는 것이 좋다
로드밸런서와 ec2간의 보안그룹 간의 소통인데 기존 운영환경에 영향을 주지 않으면서
새로운 프로젝트로 빌드하려면 다른 vpc환경에서 보안그룹을 새로 생성해 매핑하는 것이 안전하다
기존환경에서는 ec2보안그룹의 80 인바운드 규칙이 전체 리소스지만
alb를 적용한 80 포트의 ec2 인바운드 규칙은 alb가 되어야 하기 때문
이 설정을 이해하기 위해 vpc세팅부터 nginx설정까지 꽤 오랜 작업시간이 소요되었다.
vpc생성을 마쳤다면 alb생성 시 네트워크매핑 alb생성시
네트워크 매핑에서 내가 연결한 ec2가 있는 vpc 환경을 선택해야 하고
해당 vpc환경에 대상그룹에 vpc에 속해있는 어떤 ec2인스턴스를 고를지 선택해야 한다
보안그룹은 기존에 ec2를 위해 만들어둔 보안그룹을 선택하고
리스 너 및 라우팅 http리다이렉트를 의도하고자 할 경우 리스너의 80과 리스너의 443을 등록하고
80 포트의 대상그룹을 리다이렉트로 수정해야 한다. 대상그룹으로 전송하는 건 443만 허용한다는 뜻이다.
여기에 추가로 443 포트에 서버에서 멀티개의 프로젝트를 사용 중이므로
SSL인증서등록을 복수개 해야 한다.
80 포트의 경우 https로 리다이렉트 하는 것으로 수정한다
Nginx 재기동
nginx.conf 환경설정 파일을 변경하고 재기동할 때 두 가지 방법이 있다.
service nginx restart
service nginx reload
- restart
- 서버를 shutdown 후 재기동, 서비스가 종료되는 시간이 존재
- 설정 파일에 문법적 에러가 존재할 경우, 서버는 죽게 된다.
- reload
- 새로운 설정 파일을 반영할 때 서버는 살아서 동작.
- 설정 파일에 문법적 에러가 존재할 경우, reload는 실패하지만 서버는 기존 설정을 기반으로 정상 동작.
운영 중인 서비스를 재기동하는 방법으론 reload 사용이 적합할 것으로 생각된다.
2. 개발환경구축
프리티어 기준으로 스프링부트 프로젝트 구동
m2.micro인스턴스로 프로젝트 2개를 돌릴 수 있다면
신규 aws 계정을 생성해서 요금이 과금되지 않는 개발계정 운용이 가능함으로
새로운 계정을 생성하여 시도해보려 했다.
하지만 결과는 실패했다.
메모리부족으로 인한 application 미실행
문제발생
- 기존 운영환경 인스턴스는 t3. 미디엄으로 메모리가 여유 있지만 프리티어기준으로 빌드작업 시도 시 서버가 자꾸 폭발
- 프리티어 구간 내에서 메모리를 최대한 확보할 수 있는 방법이 있는지 강구해 보고 없으면 인스턴스 클래스를 올려야 하는데 프리티어가 벗어난 인스턴스면 의미 없음→ 클라이언트는 더 이상의 비용지출을 원하지 않음
문제해결
- 리눅스에는 SWAP메모리를 지정할 수 있어 RAM공간이 부족할 경우 HDD일정 공간을 마치 RAM처럼 사용할 수 있음(마른수건 쥐어짜듯 메모리크기 증설가능)
적용 전
적용 후
프로젝트 A/B 총 2개 스프링프로젝트 빌드 후
속도가 현저히 낮아지기는 하지만 우선 개발환경 구축을 위한 프리티어를 최대한 활용하기 위해 삐걱대긴 하지만 돌아가는 것 확인
- [ ] 별도의 개발계정으로 프리티어로 진행하려 했으나 프리티어 기준 성능 저하 차이가 극심해 기존 운영환경 그대로 진행하기로 결정
- 디비 마이그레이션, 인프라 세팅등 개발비용 대비 dev환경 구축은 효율이 크게 의미가 없다 판단하여 리젝 되었다.
개선으로 인한 결과
인프라 재구성: 절반 성공
사설인증서 뜯어내고 ACM에서 발급된 인증서가 적용된 ALB로 외부트래픽을 받아 ec2인스턴스에 구동 중인 2개의 프로젝트에 포워딩 성공, 가용성확보와 완벽한 무중단배포를 위한 ASG-ALB는 진행 못함
Release용 Dev환경 구축시도 : 실패
별도의 개발계정을 구축하려 했으나 프리티어계정 1개에서 2개의 스프링부트앱을 돌릴 수가 없었음
결론
- 요구사항 유지보수 해결
- 인프라변경으로 인한 사설인증서 비용절감
- 기존: nginx(트래픽, SSL, 로드밸런서) + EC2 + 스프링부트(프로젝트 A, 프로젝트 B)
- 변경: ELB(ALB) + ACM + nginx + EC2 + 스프링부트(프로젝트 A, 프로젝트 B)
- 프리티어 dev계정 개설로 production 전 릴리즈 환경 구축 → 백지화
- alb-asg를 활용한 무중단배포(블루그린) → 미구현
- 운영오버헤드 감소(codecommit ci/cd pipeline구축) → 미구현
원하는 설계대로 마무리짓지 못해 아쉬웠다.
부족한 부분도 많이 알게 되고, 새로 학습한 내용도 많아
다시 경험하고 싶은 상황(?)은 아니지만 값진 경험이었다.