개요
배포 스크립트에 현재 실행중인 프로세스의 PID를 kill -15로 종료시키는 명령을 추가했었다. 하지만 PID를 찾지 못하여 프로세스를 종료시키지 않아 포트충돌이 발생하였다. 왜 이러한 원인이 발생했는지 알아보자. (참고로 리눅스는 Ubuntu를 사용하였다.)
쉘 스크립트 란?
쉘 스크립트(Shell Script)는 리눅스에서 사용되는 스크립트 언어로, "특정한 명령어들을 순차적으로 실행하도록 한 스크립트 파일"이다.
쉘 스크립트 파일은 일반적으로 텍스트 파일로 작성되고 .sh 확장자를 가지고 있다. 또한 #!과 같은 shebang(셔뱅)으로 시작하여 현재 스크립트를 실행하기 위한 쉘 프로그램을 지정할 수 있는데, 우리가 사용할 Bash 스크립트는 첫번째 라인에 #!/bin/bash를 적어 지정할 수 있다.
쉘 스크립트를 이용한 배포의 장점
쉘 스크립트를 사용하지 않고 서버를 배포해 본 경험이 있는 사람에게는 아래의 말이 매우 공감될 것이다. (스프링부트 서버를 기준으로 설명을 할 것이다.)
배포를 할 때에는 서버에서 여러 줄의 Unix 커맨드를 통해 서버를 빌드하고 배포해야 한다.
git에서 소스를 끌어 오고, gradlew로 빌드 하고, 만약 서버가 실행중이라면 배포를 위해 종료하고, nohup 명령어로 백그라운드에서 실행시키는 등 여러 줄의 커맨드를 배포할 때마다 매번 반복해서 입력해야 한다.
이렇게 매번 반복해서 입력하면 시간이 많이 소요되고, 어디까지 입력했는지 까먹는 문제점이 발생할 수 있다. 하지만 쉘 스크립트에 커맨드를 정리해 놓는다면, 단 한 줄의 명령어로 여러 줄의 명령어를 실행시킬 수 있다.
쉘 스크립트 (SpringBoot - Gradle)
#!/bin/bash
# 변수 설정
REPOSITORY=/home/ubuntu/app/git
PROJECT_NAME=Fling-BE
# 레포지토리로 이동
cd $REPOSITORY/$PROJECT_NAME/
# .env 파일에서 환경 변수 로드
echo "> .env 파일에서 환경 변수 로드"
if [ -f .env ]; then
export $(cat .env | xargs)
fi
echo "> Git Pull"
git pull
echo "> 프로젝트 Build 시작"
./gradlew clean build -x test
echo "> Build 파일 복사"
cp ./build/libs/*.jar $REPOSITORY/
echo "> 현재 구동중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -f $PROJECT_NAME)
echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"
# 현재 실행 중인 애플리케이션이 있는지 확인
if [ -z $CURRENT_PID ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/ | grep '.jar' | tail -n 1)
echo "> JAR Name: $JAR_NAME"
nohup java -jar $REPOSITORY/$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
위 코드는 SpringBoot - Gradle 서버 배포를 자동화 할 수 있는 명령어들을 모은 "deploy.sh"스크립트이다. 각 줄의 명령어 대한 의미는 다음과 같다.
- REPOSITORY=/home/ubuntu/app/git
- 프로젝트 디렉토리 주소
- PROJECT_NAME=Fling-BE
- 프로젝트 이름
- cd $REPOSITORY/$PROJECT_NAME/
- 제일 처음 git clone 받았던 디렉토리로 이동
- if [ -f .env ]; then export $(cat .env | xargs) fi
- .env 파일은 “변수명=값”의 형식으로 환경변수가 저장되어 있는 파일(위치는 cd $REPOSITORY/$PROJECT_NAME/)
- .env 파일에 정의된 환경 변수들을 현재 쉘 세션에 로드
- git pull
- 디렉토리 이동후, main 브랜치의 최신 내용을 받음
- ./gradlew clean build -x test
- 이전 빌드 산출물을 정리하고 테스트를 제외한 새 빌드를 실행
- cp ./build/libs/*.jar $REPOSITORY/
- build의 결과물인 jar파일을 복사해 jar파일을 모아둔 위치로 복사
- CURRENT_PID=$(pgrep -f $PROJECT_NAME)
- $PROJECT_NAME을 포함하는 프로세스의 PID를 찾아 CURRENT_PID에 할당
- if ~ else ~ fi
- 현재 구동중인 프로세스가 있는지 없는지 여부를 판단해서 기능 수행
- PID값을 보고 프로세스가 있으면 해당 프로세스를 종료
- JAR_NAME=$(ls $REPOSITORY/ |grep '.jar' | tail -n 1)
- 새로 실행할 jar 파일명 검색
- 여러 jar파일이 생기기 때문에 tail -n로 가장 나중의 jar파일(최신 파일)을 변수에 저장
- nohup java -jar $REPOSITORY/$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
- nohup 명령어를 사용해 백그라운드에서 Java 애플리케이션을 실행하고, 모든 출력을 로그 파일로 리다이렉트
- 외장 톰캣을 설치할 필요 없이 내장 톰캣을 사용해서 jar 파일만 있으면 바로 웹 어플리케이션 서버가 실행 가능
쉘 스크립트 실행
스크립트 실행 권한 추가
$ chmod 755 ./deploy.sh
# 스크립트 실행
$ ./deploy.sh
스크립트를 저장하고 스크립트가 존재하는 디렉토리에서 위 명령어들을 입력하면 빌드/배포 작업이 한번에 이루어지게 된다.
PID 미발견으로 인한 포트충돌
문제점
대부분은 문제 없이 위의 배포 스크립트를 실행하면 기존에 작동중인 프로세스를 종료하고 빌드/배포가 성공할 것이다. 하지만 나는 빌드는 성공했지만, PID를 발견하지 못하여 기존 프로세스를 종료하지 않아 8080포트에서 포트 충돌이 발생했다.
탐구과정
Build Successful 줄 기준 5번째 줄인 "현재 구동 중인 애플리케이션 pid : " 줄을 보면 PID가 적혀있지 않은 것을 볼 수 있다.
CURRENT_PID=$(pgrep -f $PROJECT_NAME) 줄에서 CURRENT_PID 변수에 PID가 할당되지 않아 빈 값이 나오는 것이다. 그러면 직접 pgrep -f $PROJECT_NAME 명령을 입력해서 pid가 나오는지 안나오는지 확인해보자.
$PROJECT_NAME의 값은 Fling-BE이기에 "pgrep -f Fling-BE"를 입력하였다. 하지만 아무것도 출력되지 않았다. 현재 실행중인 프로세스가 없는 것일까...?
"lsof -i :8080"는 8080 포트를 사용하는 네트워크 연결을 보여주는 명령어이다. 현재 8080 포트에서 실행중인 프로세스가 존재하고 PID도 적혀 있는 것으로 보아 $PROJECT_NAME의 변수 값인 Fling-BE의 문제로 보인다.
하지만 git clone 해온 프로젝트의 레포지토리명은 Fling-BE이고 그 안에 필요한 파일들이 들어있는 것을 볼 수 있다. 무엇이 문제인 것일까? 아까 문제라고 판단했던, PROJECT_NAME에 대해서 조금만 더 조사해보자
문제점을 찾았다. 현재 실행중인 프로젝트의 이름은 Fling-BE가 아니라 Fling이였던 것이다. 찾아보니 프로젝트 이름은 Spring 프로젝트를 만들 때 기본 스프링 프로젝트 명으로 생성된다고 한다. 따라서 Fling으로 프로젝트를 생성하고 Fling-BE로 레포지토리를 생성해서 발생한 문제였다!
쉘 스크립트 수정 (SpringBoot - Gradle)
#!/bin/bash
# 변수 설정
REPOSITORY=/home/ubuntu/app/git
PROJECT_NAME=Fling-BE
PORT=8080
# 레포지토리로 이동
cd $REPOSITORY/$PROJECT_NAME/
# .env 파일에서 환경 변수 로드
echo "> .env 파일에서 환경 변수 로드"
if [ -f .env ]; then
export $(cat .env | xargs)
fi
echo "> Git Pull"
git pull
echo "> 프로젝트 Build 시작"
./gradlew clean build -x test
echo "> Build 파일 복사"
cp ./build/libs/*.jar $REPOSITORY/
echo "> 현재 구동중인 애플리케이션 pid 확인"
# CURRENT_PID=$(pgrep -f $PROJECT_NAME) Bash shell에서 #으로 주석처리를 할 수 있다.
CURRENT_PID=$(lsof -ti tcp:$PORT)
echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"
# 현재 실행 중인 애플리케이션이 있는지 확인
if [ -z $CURRENT_PID ]; then
echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
echo "> kill -15 $CURRENT_PID"
kill -15 $CURRENT_PID
sleep 5
fi
echo "> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/ | grep '.jar' | tail -n 1)
echo "> JAR Name: $JAR_NAME"
nohup java -jar $REPOSITORY/$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
PID를 찾을 수 있도록 쉘 스크립트를 조금 수정하였다.
수정한 부분은 두 부분이다. 첫 번째로, PORT라는 변수를 추가하였고 8080값을 할당해 주었다. 마지막으로 현재 "레포지토리 이름 ≠ 프로젝트 이름" 이므로 프로젝트 이름으로 PID를 찾는게 아닌, 8080포트를 사용하는 프로세스를 파악해 PID를 찾도록 재구성하였다.
수정한 쉘 스크립트를 실행하니 정상적으로 서버를 배포할 수 있었다.
대안
bootJar{
archivesBaseName = '프로젝트이름'
archiveFileName = '원하는파일명.jar'
archiveVersion = "0.0.1"
}
해당 코드를 build.gradle에 추가해 jar 파일의 이름을 변경할 수도 있다고 한다. 다시 같은 상황이 발생한다면 해당 방법으로 해결을 시도해 볼 것 같다.
결론
💡 개발 블로그들에 적혀있는 대부분의 배포 스크립트들은 프로젝트 이름과 레포지토리 이름이 동일하다고 판단하고 작성된 스크립트이다. 따라서 배포 스크립트를 작성하기 전에 먼저 프로젝트 이름과 레포지토리 이름이 동일한 지 판단하자!
참고
[AWS] AWS EC2 & RDS Free Tier 구축
AWS EC2 & RDS Free Tier 구축 System Architecture . AWS EC2 & RDS 구축 AWS EC2 & RDS 구축 방법은 향로님의 블로그 가 참고하기 좋은 것 같다. 2023년 10월 기준 UI 가 블로그 이미지와 약간 다르긴 하지만 기본적인
data-make.tistory.com
jar 파일 이름 생성 기준 - 인프런
안녕하세요.강의를 보던 중 jar파일의 이름 생성 기준이 어떻게 되는지에 대한 궁금증이 들어 이렇게 질문드리게되었습니다.강의 내용에서 나오는 jar파일 이름 중 hello-spring 으로 되어 있는 부분
www.inflearn.com