개요
산보실록 서비스를 올릴 물리 서버가 들어오면서, 드디어 운영 단계를 앞두게 되었다.
이후, 운영과 유지보수를 위해 Spring으로 전환하는 과정에서 수월한 유지보수를 위해 CI/CD를 구축하기로 결정했다.
지원 받은 물리 서버의 인프라 구축이 완료되기 전, 프론트엔드 개발자와 원활한 협업을 위해 EC2와 RDS를 이용해 사전 배포를 진행하게 되었다.
그래서 물리 서버에 CI/CD를 구축하기 전 CI/CD 구축과정을 익히고, 개발을 원활히 하기 위해 EC2 서버에 CI/CD를 구축해보고자 한다.
Jenkins와 달리 별도 서버가 필요하지 않고 상대적으로 구축이 간편한 Github Actions을 활용하기로 했다.
사용 기술 스택 및 버전
- Spring Boot 3.2.5
- openjdk 17.0.3
- Gradle 8.7
- MySQL 8.1.0
구축 과정
우선, .github
디렉토리에 workflows
폴더를 생성한 후 CD.yml
파일을 만들어 주었다.
이제 CD.yml에 build
, deploy
스크립트를 작성해주면 된다.
1. 이름 설정
해당 workflow의 이름을 설정해준다.
name : Sanbo-Sillok-Server
설정한 이름은 Github Actions에 다음과 같이 표현된다.
2. branch 설정
CI/CD가 적용될 branch를 설정해준다.
on: push: branches: - develop
위 스크립트에서는 develop
branch에 push
또는 merge
될때 해당 workflow가 실행된다.
3. jobs 설정
Github Actions에서 처리할 Task들을 설정해준다.
jobs: build: runs-on : ubuntu-latest deploy: needs: build runs-on: ubuntu-latest
runs-on
: 해당 Task가 실행될 환경의 OS와 버전 선택needs
: 해당 Task가 실행되기 전, 완료되어야 할 Task 선택
설정한 내용은 Github Actions에 다음과 같이 표현된다.
4. build
먼저 build Task에 대한 스크립트를 설정해보자.
build: runs-on : ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Set up JDK uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: permission run: chmod +wrx gradlew - name: Build with Gradle uses: gradle/gradle-build-action@v2.6.0 with: arguments: build - name: Upload build artifact uses: actions/upload-artifact@v2 with: name: Sanbo-Sillok-Server path: build/libs/sanbosillokserver-0.0.1-SNAPSHOT.jar
각 스크립트의 세부 내용은 다음과 같다.
Checkout
CI 서버에 원격 저장소의 코드를 내려받고, 2번에서 설정한 branch로 checkout하는 과정이다.Set up JDK
JDK 설정 작업이다.
범용적으로 사용되는 오픈소스 JDK인Eclipse Temurin
을 사용하겠다.permission
프로젝트 빌드를 위해 gradlew에 권한을 부여한다.Build with Gradle
프로젝트를 빌드한다.Upload build artifact
CI 서버의 지정 경로의 빌드 결과물을 Github 저장소에 업로드한다.
보안, 가용성, 관리 용이성 등을 고려하여, CI서버가 아닌 별도의 Github 저장소를 이용한다.
5. deploy
다음으로 deploy Task에 대한 스크립트를 설정해보자.
deploy: needs: build runs-on: ubuntu-latest steps: - name: Download build artifact uses: actions/download-artifact@v2 with: name: Sanbo-Sillok-Server path: build/libs/ - name: Deploy to EC2 run: | sshpass -p ${{ secrets.EC2_PASSWORD }} scp -o StrictHostKeyChecking=no build/libs/sanbosillokserver-0.0.1-SNAPSHOT.jar ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/home/${{ secrets.EC2_USERNAME }}/ sshpass -p ${{ secrets.EC2_PASSWORD }} ssh -T -o StrictHostKeyChecking=no ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} "pgrep java | xargs kill -9; nohup java -DATABASE_HOST=${{ secrets.DATABASE_HOST }} -jar sanbosillokserver-0.0.1-SNAPSHOT.jar > nohup.out 2>&1 &"
각 스크립트의 세부 내용은 다음과 같다.
Download build artifact
Github 저장소에 업로드된 결과물을 다운로드한다.Deloy to EC2
다운로드한 결과물을 운영 서버에 옮기고, 실행한다.
이때,.pem
키 없이 서버에 원격으로 명령어를 실행하기 위해sshpass
를 사용했다.
EC2 password 등 공개돼선 안되는 값들은 github secrets를 활용하여 처리했다.
github secrets
github actions를 활용한 CI/CD 구축 과정에서 드러나서는 안되는 값들을 secrets으로 관리할 수 있는 시스템.Settings → Secrets and variables → Actions 에서 설정할 수 있다.
설정한 secret 목록을 아래와 같이 확인할 수 있다.
단, 그 값에 대해서는 수정은 가능하지만 조회할 수는 없다.
마지막으로, 원래는 nohup &
를 사용해 프로세스를 백그라운드에서 실행하면 실행 로그를 nohup.out으로 redirect한다.
하지만 sshpass
를 사용했을 경우, 실행 로그가 터미널에 출력되어 ssh shell이 정상 종료되지 않고 timeout 에러가 발생한다.
따라서, > nohup.out 2>&1
을 명시적으로 추가하여 실행 로그를 nohup.out으로 redirect시키고 ssh shell이 정상 종료 될 수 있도록 해주었다.
실행 로그 redirect 명령어
> nohup.out
: 실행 로그를 redirect할 파일 이름2>&1
: 표준 에러를 표준 출력으로 취급
Trouble shooting
deploy 과정에서 application.yml
에 ${환경 변수}
형태로 작성된 부분에 EC2 서버의 .bashrc
에 정의한 환경 변수가 제대로 들어가지 않는 문제가 발생한다.
ssh로 직접 접속 했을 때는 해당 문제가 발생하지 않는 것으로 보아, sshpass
명령어를 사용했을 경우에 발생하는 문제로 보인다.
해당 문제의 원인을 파악하기 위해 다음과 같은 과정을 시도해보았다.
1. 환경 변수 출력 시도
source ~/.bashrc
로 .bashrc
를 로드한 후, echo $DATABASE_NAME
명령어를 workflow에 추가해 환경 변수 출력을 시도해보았다.
아무것도 출력되지 않았다.
프로젝트 빌드 파일에 문제가 있는 것이 아닌, 애초에 .bashrc
의 환경 변수에 접근하지 못하는 것으로 보인다.
2. 권한 확인
환경 변수에 접근하지 못하는 이유가 권한 문제인가 싶어 .bashrc
의 권한을 확인해보았다.
권한은 정상적으로 부여되어 있는 것을 확인할 수 있었다.
3. 환경 변수가 정의된 파일 내용 출력
다음으로, workflow에서 환경 변수가 정의된 파일의 내용을 읽을 수 있는지 확인하기 위해 .bashrc
의 내용을 출력하는 스크립트를 추가해보았다.
잘 출력되는 것으로 보아, 환경 변수가 정의된 파일에 접근하는 것에는 문제가 없었다.
4. 시스템 전체 환경 변수로 변경
Linux 환경 변수는 동작 범위에 따라 3가지로 나뉜다.
- 로컬 환경 변수
- 현재 세션에서만 동작.
- 사용자 환경 변수
- 특정 사용자에 대해 정의된 환경 변수로, 로그인할 때마다 로드됨.
.bashrc
,.bash_profile
,.bash_login
,.profile
등에 정의된다.- 시스템 전체 환경 변수
- 해당 시스템의 모든 사용자가 사용 가능.
/etc/evironment
,/etc/profile
,/etc/profile.d
,/etc/bash.bashrc
등에 정의된다.
기존에는 .bashrc에 사용자 환경변수를 정의했었다.
sshpass를 이용하여 원격 접속할 때와 같은 사용자로 접속했기 때문에 문제가 없을 것이라 생각했지만, 혹시 몰라 시스템 전체 환경 변수로 다시 환경 변수를 작성해보았다.
/etc/environment
에 정의한 환경변수를 다시 sshpass로 출력해보았지만, 제대로 출력되지 않았다.
5. login shell
sshpass
는 대화형 터미널을 사용하지 않으므로, Non-login Shell에서 사용하는 .bashrc
에 환경 변수를 저장하는 것은 문제가 없다고 생각했었다.
'하지만, 혹시 sshpass가 Non-login Shell 방식이 아니라면 ?' 이라는 생각이 스쳐지나갔다.
그래서 아예 Login Shell 방식을 사용해보기로 했다.
우선, 환경 변수를 .bashrc
가 아닌 .bash_profile
에 다시 정의했다.
그리고 마지막 deploy 스크립트를 다음과 같이 수정했다.
bash -l -c 'pgrep java | xargs kill -9; nohup java -jar sanbosillokserver-0.0.1-SNAPSHOT.jar &'
bash 명령어 옵션
1. -l : Login Shell을 사용하는 옵션
2. -c : 사용할 명령어를 설정하는 옵션
결과적으로 Login Shell 방식을 사용하여, 배포에 성공했다.
결론
sshpass
가 아예 Non-Login Shell을 사용하지 않는 것인지, Non-Login Shell임에도 .bashrc
의 환경 변수를 읽어오지 못하는 다른 이유가 있는 것인지 알아볼 필요가 있어보인다.
추후, Non-Login Shell과 Login Shell에 대한 개념을 정리하며, 해당 원인에 대한 내용을 조사해봐야겠다.