예전 사이드 프로젝트에서는 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
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 문서만 가져오는건 안되는거지?.... 아직 이유를 정확히 찾지 못했지만..일단 해결..