본 게시물은 넥스터즈 23기 짠지 사이드 프로젝트에서 진행한 내용입니다.
자세한 내용: https://github.com/Nexters/zzanji-server
들어가기 앞서
예전 사이드 프로젝트에서는 Jenkins, Docker를 이용해서 자동 배포 파이프라인을 구성했지만,
이번에는 새로운 툴을 사용해보고 싶어 Github Action를 이용하여 CI/CD 파이프라인을 구성해보았습니다.
(기본적으로 Docker를 이용했습니다. JIB와 같이 구글에서 자바 어플리케이션 전용 컨테이너를 생성해주고 배포해주는 쉬운 툴도 존재하던데, 다음에 기회가 되면 사용해봐야겠다.)
총 CI, CD로 나누어 파이프라인을 구성했습니다.
"develop, feat/*" 브랜치에 PR 혹은 Push 작업 발생 시, 지속적인 통합(CI)이 되도록 구성했고,
"main, develop" 브랜치에 Push 작업 발생 시, 자동 배포(지속적인 배포, CD)가 되도록 구성했습니다.
동작 원리
CI 과정
1. feat/*에 push or PR, develop에 PR 가 되었을 때 Workflow를 시작(Trigger)
2. Github Action에서 스프링 어플리케이션의 jar 파일 빋드(build + test) -> local 환경에서 진행
3. 결과 출력
CD 과정(자동 배포)
- main, develop 브랜치 Merge가 됬을 때 Workflow 시작
- Github Action에서 스프링 어플리케이션 jar 파일 빌드(build + test)
- 실제 배포 환경(prod), 개발 환경(dev) 환경에서의 Test 진행
- 본 프로젝트에서는 Rest docs를 사용하기 때문에 Test 과정이 필요
- 생성된 jar파일을 Dockerfile로 빌드(이미지 생성)
- Docker hub 로그인 및 이미지 push
- Github action에서 SSH 접속 방식으로 NCP 서버에 접근, Docker hub에서 이미지 pull, docker-compose 실행
- 배포 완료
CI.yml
자세한 설명은 생략하고, 주석을 통해 설명하겠습니다.
참고로, 짠지 사이드 프로젝트는 Spring boot 3.1.1 을 사용하고 있어, JDK 17을 설치했습니다.(Gradle 8.1.1)
name: CI
# feat/*, develop 브랜치에 push or pr 작업 요청시 작동
on:
push:
branches:
- feat/*
pull_request:
branches:
- develop
- feat/*
permissions: write-all
# 실행할 작업들
jobs:
CI:
runs-on: ubuntu-latest
#Build test를 위한 테스트 DB Setting
services:
mysql:
image: mysql
env:
MYSQL_DATABASE: jjanji
MYSQL_ROOT_PASSWORD: 1234
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
#JDK Setting
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
# Gradle 권한 부여
- name: Grant Execute permission for gradlew
run: chmod +x gradlew
shell: bash
#Build & Test
- name: Build With Gradle
run: ./gradlew clean build
shell: bash
#테스트 결과 출력
- name: Publish Unit Test Result
uses: EnricoMi/publish-unit-test-result-action@v1
if: ${{ always() }}
with:
files: build/test-results/**/*.xml
Dockerfile
FROM openjdk:17-alpine
EXPOSE 8080
ARG JAR_FILE=/build/libs/jjanji-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "/app.jar"]
짠지 사이드 프로젝트는 Spring boot 3.1.1 을 사용하고 있어, JDK 17 이미지를 세팅해주었습니다.
jar파일이 생성되는 /build/libs 경로를 JAR_FILE에 초기화하고, app.jar 로 복사해주었습니다.
또한, 개발 서버 환경의 yml 파일이 사용될 수 있도록 profile 옵션을 주었습니다.
(현재는 개발 서버 환경에 CD를 진행하기 때문에 "-Dspring-profiles.active=dev"을 사용했습니다. 실제 배포 환경에서는 별도의 Dockerfile를 사용하고 "-Dspring-profiles.active=prod"만 차이가 있습니다.)
Docker-compose.yml
version: "3" #버전 지정
services: #컨테이너 설정
web:
container_name: zzanz-dev
image: rnqhstlr2297/zzanz-dev
restart: always
ports:
- "8080:8080"
docker hub에서 pull할 이미지명을 지정해준다. "${도커 허브 계정명}/${도커 허브 레포지토리 명}"
요청이 들어오는 host의 포트와 도커 컨테이너의 포트를 매핑 시켜준다.
CD.yml
name: CD
# main, develop 브랜치에 push 작업 요청시 작동
on:
push:
branches:
- main
- develop
permissions:
contents: read
# 실행할 작업들
jobs:
CD:
runs-on: ubuntu-latest
steps:
#JDK setting
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
#dev, prod 환경에 맞는 .yml 생성
# create application-dev.yml
- name: make application-dev.yml
if: contains(github.ref, 'develop') # branch가 develop인 경우
run: |
cd ./src/main/resources
touch ./application-dev.yml
echo "${{ secrets.PROPERTIES_DEV }}" > ./application-dev.yml
shell: bash
# Gradle 권한 부여
- name: Grant Execute permission for gradlew
run: chmod +x gradlew
shell: bash
# develop build(with test)
- name: Develop Build With Test
if: contains(github.ref, 'develop')
env:
SPRING_PROFILES_ACTIVE: dev
# 오류 발생시 오류 추적을 위해 --stacktrace 추가
run: ./gradlew clean build --stacktrace
shell: bash
# docker build & push
# 개발 서버용(develop) Docker build & push
- name: Docker build & push to dev
if: contains(github.ref, 'develop')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_REPO }}/zzanz-dev .
docker push ${{ secrets.DOCKER_REPO }}/zzanz-dev
# SSH 접속 및 배포
# deploy to develop
- name: Deploy to Develop
uses: appleboy/ssh-action@master
id: deploy-dev
if: contains(github.ref, 'develop')
with:
host: ${{ secrets.DEV_SERVER_IP }}
username: ${{ secrets.DEV_SERVER_USER }}
password: ${{ secrets.DEV_SERVER_PASSWORD }}
port: ${{ secrets.DEV_SERVER_PORT }}
script: |
# 배포 서버 접속시 위치인 "/root" 위치에 docker-compose 파일 존재
sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
sudo docker rm -f $(docker ps -q -a)
sudo docker pull ${{ secrets.DOCKER_REPO }}/zzanz-dev
docker-compose up -d
docker image prune -f
이전 CI.yml 과 다르게 각 환경별(dev, prod) application.yml 파일을 생성하였고, 해당 환경의 yml파일을 사용해 build 및 테스트를 진행했습니다.
참고로, ${{ secrets.~ }} 로 지정한 시크릿 변수들은 아래와 같이 미리 정의해두었습니다.
결과
결과를 보시게 되면 정상적으로 동작하는 것을 볼 수 있습니다.
트러블 슈팅(삽질....)
앞서 말했듯이 짠지 사이드 프로젝트는 Rest docs를 사용합니다.
Github action을 사용해서 CI/CD 과정을 마치고 서버에 정상적으로 배포가 되었는데, Rest docs의 결과물인 index.html에 접속하지 못하는 문제가 발생했습니다.
서버에 접근하지 못하는 것이 아니라, 서버에 index.html이 없는것이 문제였습니다.(Local에서는 있었는데?......)
서버에 배포된 jar파일에 index.html이 있는지 확인해보았는데..어?...왜없지??....
GitHub Action CD.yml workflow에서 build시 제대로 test -> asciidoctor -> copyDocument -> bootJar 순으로 동작되고, 그 결과과 정상적인지 확인하기 위해서 build 시 --info 옵션을 통해 로그를 찍어보았습니다.
asciidoctor 동작시 아래와 같이 정상적으로 build.gradle 파일 설정 정보인 build/docs/asciidoc 하위 디렉토리 위치에 index.html이 복사된것을 로그로 볼 수 있었습니다.
그리고, bootJar 동작시 로그를 살펴보았는데, 앞서 생성한 경로에 html 파일을 찾지 못한다고 출력되었습니다. 왜?.....
아래 프로젝트 build.gradle 설정 정보에 따르면,
booJar시 build/docs/asciidoc 하위 디렉토리 위치의 html 파일을 static/docs 로의 copy 태스크로 넣어주고 jar 파일에 직접 문서 파일을 넣어주게 됩니다. (즉 build 패키지에 들어가게 된다.)
공식 문서에서 이렇게 나와있는데 왜?.....안돼????.....
혹시나 Github Action에서 asciidoctor 플러그인이 생성한 HTML5 문서를 인식하지 못하나? 싶어서 아래와 같이 바꾸어보았습니다.
즉, asciidoctor 플러그인이 생성한 모든 변환 결과물을 복사하도록 바꾸었습니다.
그 결과.. 정상적으로 index.html 파일이 들어온것을 볼 수 있습니다.
당연하지 asciidoctor 플러그인이 생성한 모든 파일을 가져오는 거니깐...!!!
근데 이유가 도대체 뭐야?....왜 HTML5 문서만 가져오는건 안되는거지?.... 아직 이유를 정확히 찾지 못했지만..일단 해결..