DayOne 서비스의 모든 기능이 완성된 것은 아니지만, 회원이 이용하는 데 필수적인 독서 기록 남기기, 독서 기록 확인하기, 좋아요 표시하기 기능이 구현되었습니다. 이에 빠르게 배포하여 회원들의 피드백을 받고자 했습니다.
그래서 이번 시간에는 DayOne 서버 배포 과정을 글로 정리해 두고자 합니다.
현재 수입이 없는 상황이므로 최소한의 비용으로 서버를 구축하고자 했습니다. 그럼에도 불구하고 Spring과 DB를 분리하여 EC2에 배포하기로 결정했습니다.
그 이유는 DB 장애가 Spring 애플리케이션에 영향을 주지 않도록 하기 위함이며 또한 EC2 프리 티어를 최대한 활용할 계획이었기에 하나의 인스턴스에서 서버와 DB를 함께 운영하면 리소스 부족이 발생할 것으로 예상했기 때문입니다.
이에 따라 설계한 시스템 구성도는 다음과 같습니다.
먼저 DB와 Spring을 배포하기에 앞서서 가상의 컴퓨터인 인스턴스를 생성하는 과정이 필요합니다.
인스턴스를 생성하는 과정은 구글에 검색을 하면 잘 나타나기에 생략하겠습니다. 이번에 배포를 하면서 느낀 거지만 spring은 적어도 메모리가 2GB인 인스턴스를 선택해야 한다는 것을 느꼈습니다. 처음에는 t2.micro 인스턴스(메모리 1GB)를 생성하고 그 위에서 build를 시도했을 때 메모리가 부족해서 build가 온전히 끝나지 못하고 중간에 멈추는 현상이 발생했습니다.
결론적으로 DB 서버는 프리티어인 t2.micro 인스턴스를 활용했고 운영 서버는 t2.small 인스턴스를 활용했습니다.
인스턴스를 생성할 때 가장 중요한 부분은 인바운드 규칙을 생성하는 부분이라고 생각합니다. 인바운드 규칙은 해당 인스턴스가 외부에서 수신할 수 있는 트래픽을 제어하는 규칙을 의미합니다. 조금 더 쉽게 이야기하면 어떤 요청들이 ec2에 접근할 수 있을지 설정하는 것입니다.
현재 인스턴스 간의 요청처리는 다음과 같습니다. 외부에서 api 요청이 발생하면 운영 서버는 DB 서버에 요청을 보내 필요한 데이터를 받는 방식입니다. 즉, 여러 사용자는 운영서버로 api 요청을 보낼 수 있기에 운영 서버
는 외부에서 여러 IP로 넘어오는 요청에 대해서 접근하는 것을 허용해주는 것이 필요한 반면 DB 서버
는 모든 요청을 허용하는 것이 아닌 운영 서버의 요청만 접근할 수 있게 구성해야했습니다.(DB는 민감한 정보를 포함할 수 있기에 외부에서 접근할 수 있게 허용하는 것은 위험하다고 판단했습니다.)
운영 서버 인바운드 규칙
DB 서버 인바운드 규칙
탄력적 IP는 AWS에서 제공하는 고정 공인 IP 주소입니다. ec2 인스턴스의 경우 기본적으로 public ip가 자동으로 할당되지만 인스턴스를 재시작할 경우 ip 주소가 변경되게 됩니다. ip 주소가 변화하게 된다면 외부에서 접속하는 과정이 불편하기에 고정 Ip 주소를 설정해주는 것입니다.
위의 캡쳐화면 과 같이 운영서버와 DB 서버 인스턴스 각각 탄력적 ip를 설정해주었습니다.
지금까지 서버와 DB를 돌리기 위한 컴퓨팅 자원을 생성하는 과정을 마무리 지었고 이제는 해당 컴퓨팅 자원에 우리가 원하는 프로그램을 돌리기 위해 필요한 파일들을 구성해주어야 합니다. 먼저 mysql부터 구성해보겠습니다.
처음 ec2가 생성되면 새로운 컴퓨터에 OS만 설치되어 있는 상태라고 보면 됩니다. 그렇기에 아무런 파일이 존재하지 않고 우리의 입맛에 맞게 설정해 주면됩니다. 지금부터는 명령어를 위주로 DB를 설정하는 방법에 대해 알아보겠습니다.
// mysql 다운로드
sudo apt update
sudo apt install -y mysql-server
mysql을 설치하기 앞서서 sudo apt update
를 실행해 패키지를 최신화 해줍니다. 그리고 아래 명령어를 통해서 mysql server를 설치합니다.
mysql --version
mysql이 잘 설치되었는지 확인하기 위해서는 위의 명령어를 수행하면 됩니다. 잘 설치되었다면 mysql이 버전 정보와 함께 나타날 것입니다.
Spring에서 MySQL과 통신하기 위해서는 yml 혹은 properties 파일에 설정 정보를 추가해주어야 하는데요 이 때 username과 password를 입력해주어야 합니다. root 사용자를 이용하는 방안이 있겠지만 root 사용자의 경우 최상위의 권한을 가지고 있기 때문에 악의적인 요청(다른 데이터베이스에 OO 테이블을 drop 하는 요청 등)에 대해서도 처리될 수 있는 문제가 발생할 수 있습니다.
그래서 저는 MySQL 내 새로운 사용자를 만들고 특정 데이터 베이스에 대한 권한만 가질 수 있도록 구성했습니다.
// mysql 접속
sudo mysql -u root -p
// mysql 사용자 만들기
create user '사용자 명칭'@'접근할 ip 주소' IDENTIFIED BY '비밀번호';
// mysql user 목록 보는 명령어
SELECT user, host FROM mysql.user;
create user '사용자 명칭'@'접근할 ip 주소' IDENTIFIED BY '비밀번호';
해당 명령어를 입력하면 mysql 내부에 새로운 사용자가 만들어집니다. '접근할 ip 주소'
부분에는 해당 사용자를 이용하는 ip 주소로 설정하면 됩니다. (’%’ 입력하면 모든 ip 주소에서 해당 사용자를 이용할 수 있지만 이는 보안 문제가 발생할 수 있기에 저는 운영서버 인스턴스 ip 주소를 활용했습니다.)
SELECT user FROM mysql.user;
명령어를 입력하면 위의 사진처럼 사용자의 목록이 나타나며 이번에 추가 dayone이 추가된 것을 볼 수 있습니다.
dayone 사용자는 root 사용자가 아니기 때문에 모든 database에 접근할 수 있는 권한이 없습니다. 그렇기에 dayone 사용자가 database에 접근하고 쓰기 작업이 가능할 수 있도록 권한을 주어야 합니다.
// 사용자에게 database에 권환 제공
GRANT '권한 정보' ON 'database 명칭'.* TO '사용자 명칭'@'접근할 ip 주소';
FLUSH PRIVILEGES;
GRANT 명령어를 통해 사용자에게 권한 정보를 제공해주면 됩니다. 서비스를 운영하는데 있어서 데이터를 쓰고 수정하고 삭제하고 조회하는 기능이 필요하여 저는 새로 만든 사용자에게 SELECT, INSERT, UPDATE, DELETE 권한을 주었습니다.
mysql을 설치하면 기본적으로 내부(localhost)에서만 접근한 허용하도록 설정파일이 구성되어 있습니다. 현재 우리는 운영 서버 인스턴스에서 db 인스턴스로 접근하는 구조를 가지고 있기에 외부에서도 mysql에 접근할 수 있게 허용해주어야 합니다.
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
위의 명령어를 ec2에 입력하면 mysql의 세팅파일을 볼 수 있습니다. 거기서 bind-address
를 원하는 ip 주소로 설정하여 접근할 수 있게 합니다. (0.0.0.0으로 설정하면 모든 ip에서 접근이 가능합니다.
지금까지의 과정을 순차적으로 처리하면 mysql에 대한 설정을 마무리가 된 것입니다. 우리는 이제 운영서버를 설정하는 과정에 대해서 살펴보겠습니다.
spring을 인스턴스에 구성하는 것은 DB에 비해 간단합니다. 대신 CD(Continuous Deployment)에 사용할 수 있는 배포스크립트를 구성하는데 집중했습니다.
sudo apt install -y openjdk-필요한 버전-jdk
spring을 실행하기에 앞서서 java가 필요하기 때문에 자신이 필요한 버전에 맞는 자바를 설치하면 됩니다. 저는 17 버전이 필요하여 17버전을 다운로드 하였습니다.
git clone <repo 주소>
자신의 프로젝트를 로컬 환경에 클론하듯이 ec2 서버에도 해주면 됩니다.
로컬 환경에서는 인텔리제이나 이클립스 ide를 통해서 실행을 하지만 ec2 내부에는 해당 툴이 존재하지 않게 jar 파일을 build를 통해 생성하고 해당 jar 파일을 실행주어서 동작시켜야 합니다.
./gradlew clean test
./gradlew test
.. 등
jar 파일이 생성되었다면 이를 실행하면 비소로 우리가 원하던 작업을 배포 과정을 마무리 할 수 있습니다.
DayOne 프로젝트는 네이버 책 검색 api, jwt 사용 등 민감한 정보를 따로 분리해서 관리하고 있었습니다. submodule를 이용하는 방법도 있겠지만 전 프로젝트에서 활용했을 때 버전이 맞지 않은 문제가 종종 발생해 속을 썩인 경험이 있어서 이번에는 아예 따로 분리해서 관리했습니다.
nohup java -jar /build/libs/*SNAPSHOT.jar --spring.config.location=/home/ubuntu/application-prod.yml
프로젝트를 실행하려면 해당 정보들이 필요하기에 해당 정보를 포함하는 yml 파일을 ec2에 생성하고 jar파일을 해당 yml로 실행시키면 비로소 우리가 원하는 장면이 등장하게 됩니다.
배포는 마무리가 되었지만 새로운 기능이 추가될 때마다 ec2에서 접근해서 최신 버전을 pull 받고 기존의 jar 파일을 삭제하고 다시 build하고 기존의 서버를 내리고 새로운 서버를 올리는 과정을 반복하는 것은 비효율적인 방법이라 느껴져서 한번에 실행할 수 있는 배포스크립트를 구성해보았습니다.
# 1. /home/ubuntu 에 있는 .jar 확장자를 가진 파일 삭제
rm -f /home/ubuntu/*.jar
# 2. 프로젝트 이름/build/libs에 있는 모든 .jar 파일 삭제
rm -f ~/'프로젝트 이름'/build/libs/*.jar
# 3. 프로젝트 디렉토리로 이동
cd ~/'프로젝트 이름' || exit
# 4. GitHub에서 최신 코드 가져오기 (pull)
git pull origin main
# 5. 프로젝트 빌드 (Gradle)
./gradlew clean build
# 6. 실행 가능한 JAR 파일만 /home/ubuntu 로 이동
mv build/libs/*SNAPSHOT.jar /home/ubuntu/
# 7. 기존 8080 포트에서 실행 중인 프로세스 종료
PID=$(lsof -t -i:8080)
if [ -n "$PID" ]; then
echo "🔹 Killing process with PID: $PID"
kill -9 $PID
else
echo "🔹 No process found on port 8080"
fi
# 8. 새로운 JAR 파일 실행 (application-prod.yml 적용)
nohup java -jar /home/ubuntu/*.jar --spring.config.location=/home/ubuntu/application-prod.yml
총 8단계가 진행되며 각 단계에 대해서 간단하게 설명하겠습니다.
해당 스크립트를 구성한 이후로는 ./deploy.sh
만 입력하면 해당 과정을 순차적으로 처리해주어서 배포효율성을 높일 수 있었습니다. (deploy.sh에 실행권한을 주어야 합니다.)
지금껏 항상 배포를 하게 되면 구글에 사람들이 배포한 과정을 찾아보면서 오랜 시간이 걸렸는데 오늘 작성한 글을 통해서 앞으로는 배포할 때 제 글을 참고하는 날이오면 좋겠다는 생각을 했습니다. 그리고 그전에는 서비스를 배포 한다라는 것에 집중해 그동안 명령어가 무엇을 의미하는지 잘 모른 채 이용했다면 이번에는 각각 정리해보면서 어떤 의미를 가지는지를 파악하는 시간이 되었습니다.
다음에는 배포 과정에서 구성한 배포스크립트를 이용해서 CD(Continuous Deployment) 과정을 수행해보도록 하겠습니다.