도입 배경
예전에 Spring boot를 공부하며 만들었었던 암호화폐 모의투자 개인 프로젝트는 배포를 수동으로 진행 중이다. 프로젝트에 수정이 있을 때마다 빌드 & 배포 과정을 수동으로 진행하는 것에 시간이 지속적으로 투자된다. 이 과정을 자동화하여 빌드 & 배포에 소요되는 시간을 줄여서 효율적으로 개발할 수 있도록 개선해 보자.
기존 배포 방식
빌드부터 배포까지 직접 수동으로 진행. 프로젝트가 잦은 수정이 있다면 이 과정을 계속 반복해야 한다. 비효율적이라고 할 수 있다.
- 로컬 IntelliJ 에서 개발
- github 레포지토리에 push
- 오라클 클라우드(Ubuntu) ssh 접속
- 레포지토리 git clone 후 gradle 사용하여 직접 빌드
- 빌드 후 jar 파일 -> Dockerfile 기반으로 도커 이미지 생성
- 생성한 이미지로 Spring boot 컨테이너 실행
개선할 배포 방식
Jenkins도 고려했지만, CI 서버를 따로 구축해야 하는 점 때문에 Github Actions를 채택하였다. Github에서 CI서버를 제공해 주기 때문에 쉽게 도입이 가능하다.
- 로컬 IntelliJ 에서 개발
- github 레포지토리에 push
- (자동화) github actions 감지 -> 프로젝트 빌드 -> 도커 image build -> 도커 허브에 image push -> 오라클 클라우드(Ubuntu)에서 image pull -> Spring boot 컨테이너 실행(배포)
✔️ Github Actions Workflow 작성
아래는 워크플로우 전체 코드이다. Github Actions를 활용하여 CI(빌드)를 진행하고, Docker로 서버에 배포를 진행한다. Dockerfile은 이전 인프라 개선에서 추가해 뒀었다.
name: Java CI with Gradle & CD with Docker
on:
push:
branches: [ "main" ]
jobs:
# 1. Build
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# JDK setting - github actions에서 사용할 JDK 설정
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# yml 파일 생성 & SSL 인증서 복사
- name: make application.yml
run: |
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.APPLICATION_YML }}" > ./application.yml
touch ./keystore.p12
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.p12
shell: bash
# gradle 세팅
- name: Setup Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# 프로젝트 빌드
- name: Build with Gradle
run: ./gradlew build -x test # 테스트 코드 제외
# Docker image build & Push
- name: Docker build & push to Docker hub
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }} .
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}
# 2. Deploy Job (의존성: Dockerize Job이 끝난 후 실행)
deploy:
runs-on: ubuntu-latest
needs: build # dockerize job이 완료된 후 실행
steps:
# docker image pull & deploy
- name: Docker image pull & Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.INSTANCE_HOST }} # 인스턴스 퍼블릭IP
username: ${{ secrets.INSTANCE_USERNAME }}
password: ${{ secrets.INSTANCE_PASSWORD }}
# 도커 이미지 pull & deploy 작업
script: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker rm -f $(docker ps -q --filter "name=spring-app")
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}:latest
docker run -d --name spring-app --network bitrun -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}:latest
docker image prune -f
application.yml / SSL 인증서 복사
주요한 코드를 살펴보자. 배포 application.yml 파일을 github secrets 변수에 저장해 두었다. 빌드 전에 yml 파일 내용을 복사해 온다. 그리고 무료 SSL인증서를 Certbot(https://certbot.eff.org/) 프로그램으로 발급받아 사용 중이다. 인증서 파일을 base64로 인코딩하여 secrets 변수에 저장해 두었고, base64로 디코딩하여 프로젝트에 포함시켜 빌드한다.
# yml 파일 생성 & SSL 인증서 복사
- name: make application.yml
run: |
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.APPLICATION_YML }}" > ./application.yml
touch ./keystore.p12
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.p12
shell: bash
Dockerfile 기반으로 이미지 생성 및 Docker Hub에 push
프로젝트에 포함되어 있는 Dockerfile을 기반으로 이미지를 만들고 도커 허브에 이미지를 push 한다. 도커 계정명, 비밀번호, 레포지토리명은 secrets 변수에 저장해 두었다.
# Docker image build & Push
- name: Docker build & push to Docker hub
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }} .
docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}
배포(deploy)
현재 오라클 클라우드 프리티어를 사용하여 배포하고 있다. 클라우드 서버에 ssh 접속을 하여 docker hub에 push 했던 이미지를 가져와 Spring boot 컨테이너를 실행시킨다. 기존에 실행 중인 컨테이너는 docker rm -f $(docker ps -q --filter "name=spring-app")를 사용하여 컨테이너를 제거한다.
# docker image pull & deploy
- name: Docker image pull & Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.INSTANCE_HOST }} # 인스턴스 퍼블릭IP
username: ${{ secrets.INSTANCE_USERNAME }}
password: ${{ secrets.INSTANCE_PASSWORD }}
# 도커 이미지 pull & deploy 작업
script: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker rm -f $(docker ps -q --filter "name=spring-app")
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}:latest
docker run -d --name spring-app --network bitrun -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO_NAME }}:latest
docker image prune -f
✔️ 빌드 및 배포 확인
프로젝트 github에서 Actions 탭에 들어가면 작업이 잘 완료되었는지 확인할 수 있다.
Docker Hub 이미지 업로드 확인
docker hub에 이미지가 바뀌었는지 확인한다.
서버 ssh 접속 후 Spring boot 컨테이너 실행 확인
ssh 접속하여 docker ps 명령어로 서버에서 Spring boot 서버가 잘 떴는지도 확인해 보자. 최신 시간으로 새롭게 컨테이너가 실행된 모습이다.
사이트 접속
배포 사이트 https://coinrun.kr/ 에 잘 접속이 되는지까지 확인해보자.
✔️ 빌드/배포 자동화 도입 후 최종 아키텍처
빌드/배포 자동화 도입 후 아키텍처 모습을 그려보았다. 아래 순서는 최종적으로 적용되는 순서이다.
(수동) IntelliJ 개발 -> Github 레포지토리에 push -> (자동화) Github Actions 감지 -> gradle build -> Dockerfile 기반으로 도커 이미지 생성 -> docker hub에 push -> Oracle Cloud ssh로 docker hub에서 이미지 pull -> Spring boot 컨테이너 실행
결과적으로 Github Actions 도입 후, 프로젝트 수정이 있을 때마다 빌드 및 배포가 자동으로 진행된다. 추후에 수정이 자주 일어나더라도 시간적으로 효율적인 개발이 기대된다.
📌 추가 개선해야 할 일
지금 구축된 빌드/배포 자동화는 배포되는 동안 Spring boot 서버가 실행되지 않는다. 배포되는 동안 사용자들의 요청을 받을 수 없다는 얘기이다. 서버가 중단되지 않는 무중단 빌드/배포 자동화를 도입해야 한다.
- 프로젝트 Github
- 프로젝트 배포