목차

GitHub Actions 으로 배포 단계 세분화 하기

🗓️

이전에 쓴 GitHub Actions 글이 조회수가 좋아 업데이트 겸 추가 글을 써본다.

사실 저번에 올린 글은 여기저기 많이 돌아다니는 흔한 자바 프로젝트 배포를 위한 Actions 스크립트를 갖다 쓴것이고, 저번 8월 즈음 시간이 생겨 몇가지 더 챙겨봤다. 해당 전체 코드는 GitHub 링크를 첨부한다.

고려해야 할 것은 두가지 정도로 압축된다. 첫번째는 빌드를 어떻게 할 것인가. 두번째는 배포를 어떻게 할 것인가. 기존의 스크립트는 빌드를 호스트에서 하는것으로 빌드 시간이 호스트에서 발생한다. 그래서 호스트가 여러대라면 N대 만큼 빌드 시간이 발생하는 것이다. JVM 특성상 아키텍처가 뭐든 아티팩트만 잘 만들어지면 되니 빌드 역시 Actions에서 무료로 컴퓨팅 리소스를 빌려서 하자. 두번째는 첫번째에서 따라오는 자연스러운 이야기인데 빌드를 호스트에서 하지 않으니 당연히 아티팩트를 전송해야하는 부분이다.

바뀐점 보기

기존의 Actions가 git clone → build → jar starup 모두의 과정이 담긴 스크립트를 원격에서 실행하는게 전부였다. 이번에 바뀐것은 크게 빌드, 배포, 서비스 재기동 세부분으로 나뉜다.

jobs 내부를 보자.

 ## 1. 빌드
  build:
    runs-on: ubuntu-latest
    steps:
      ## 형상 다운로드
      - name: Checkout repository
        uses: actions/checkout@v2
      ## 빌드
      - name: Build package
        run: ./gradlew bootJar
      ## 빌드 아티팩트 확인
      - name: Show structure of artifact files
        run: ls -ahl ./build/libs/*.jar
      ## 빌드 아티팩트 업로드
      - name: Upload build artifact for job
        uses: actions/upload-artifact@v3
        with:
          ## 업로드 키 값 (임의지정)
          name: project_artifact
          ## Gradle의 경우
          path: "./build/libs/*.jar"

gradle의 예를 가지고 왔다. git clone 이후 gradlew bootJar 를 하고 아티팩트를 업로드 한다. upload-artifact를 통해 업로드 된 아티팩트는 일정 기간이 지나면 삭제되는 것으로 알고있다. 정확한 기간은 기억나지 않는다. 다음은 배포다.

## 2. 배포
  distribute:
    ## 빌드가 선행되야함.
    needs: [ build ]
    runs-on: ubuntu-latest
    steps:
      ## 아티팩트 전송을 위한 임시 공간
      - name: Create distribute
        run: mkdir ~/dist
      ## 빌드 한 아티팩트 다운로드
      - name: Download build artifact for job
        uses: actions/download-artifact@v3
        with:
          name: project_artifact
          path: dist
      ## 아티팩트 확인
      - name: Show structure of downloaded files
        run: |
          ls -alh dist

새로운 actions로 download-artifact 를 이용해 업로드한 아티팩트를 받는다. 이걸 바로 전송하게 하는 방법은 또 다시 호스트에서 요청을 던져야 하는 방법이라 어쨋든 전송이 목적이라면 scp로 바로 밀어넣는게 좋을듯 하다. 계속 가자

      ## 서버에 아티팩트 전송
      - name: Transfer artifact
        uses: appleboy/scp-action@master
        with:
          username: ${{ secrets.USERNAME }}
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          key: ${{ secrets.KEY }}
          rm: true
          source: "dist/*.jar"
          target: ${{ secrets.DIST_PATH }}
          ## 작업에 사용했던 dist 디렉토리를 경로상에서 제거
          strip_components: 1
      ## 전송된 아티팩트 확인
      - name: Show structure of transfer files
        uses: appleboy/ssh-action@master
        with:
          username: ${{ secrets.USERNAME }}
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          key: ${{ secrets.KEY }}
          script: |
            cd ${{ secrets.DIST_PATH }}
            ls -alh

적절한 전송 방법을 찾다보니 appleboyssh-action말고 scp-action 가 있어서 바로 활용했다. 전송 이후에 아티팩트 확인하는 부분은 없어도 되지만, 호스트 ssh 열어놓고 파일 떨어지나 안떨어지나 볼게 아니라면 달아놓으면 여러모로 손이 덜간다.

  ## 3. 서비스 재기동
  service_restart:
    ## 앞 작업이 선행되고 성공해야 함
    needs: [ build, distribute ]
    runs-on: ubuntu-latest
    steps:
      - name: Call service restart
        uses: appleboy/ssh-action@master
        with:
          username: ${{ secrets.USERNAME }}
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          key: ${{ secrets.KEY }}
          script: |
            cd ${{ secrets.DIST_PATH }}
            echo "서비스 종료 스크립트. pgrep 부분 개선 및 루프.."
            echo "서비스 시작 스크립트"

세번째는 기존의 appleboy/ssh-action를 이용해 형상에 포함된 스크립트를 실행하는 부분과 같다. 나는 아래와 같이 활용하고 있다.

      - name: Call service restart
        uses: appleboy/ssh-action@master
        with:
          username: ${{ secrets.USERNAME }}
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          key: ${{ secrets.KEY }}
          ## 배포 스크립트를 저장소로부터 항상 최신으로 가져오고, 실행.
          script: |
            cd ${{ secrets.DIST_PATH }}
            rm -rf repo
            git clone https://github.com/platanus-kr/plata-board.git repo
            mv ./repo/scripts/deploy.sh ./deploy.sh
            sh deploy.sh

어짜피 형상을 받는다는 점이 좀 마음에 들진 않지만 따로 deploy용 스크립트를 버전관리 하는게 더 귀찮아서 그냥 이렇게 했다. 당연히 배포용 스크립트 gradle/maven 빌드내용이 전부 제거되고 아래와 같이 간결해진다.

#!/bin/bash

source ~/.bash_profile

APP_NAME=plata-board
REPOSITORY=`pwd`
RELEASE=production

# .jar 파일 타겟팅
JAR_NAME=$(ls $REPOSITORY | grep jar)

# 현재 실행중인 서버가 있으면 잡아서 종료 시킵니다.
CURRENT_PID=$(pgrep -f $APP_NAME)
if [ -z $CURRENT_PID ]
then
  echo ">>>> java process not found."
else
  echo ">>>> PID: $CURRENT_PID kill."
  kill -15 $CURRENT_PID
  sleep 45
fi

# .jar 파일 java 실행합니다.
echo ">>>> $JAR_NAME java execute."
echo "nohup java -jar ./$JAR_NAME --spring.config.location=classpath:/application.properties --spring.profiles.active=$RELEASE > /dev/null 2> /dev/null < /dev/null &"
nohup java -jar ./$JAR_NAME --spring.config.location=classpath:/application.properties --spring.profiles.active=$RELEASE > /dev/null 2> /dev/null < /dev/null &
sleep 20
CURRENT_PID=$(pgrep -f $APP_NAME)
echo ">>>> New PID: $CURRENT_PID"

내가 사용하고 있는 재시작용 스크립트

굳이 같는 내용을 echo로 찍는 이유는 첫번째로 환경변수가 제대로 할당되는지고 두번째로는 이것들이 actions 로그에 남아서 원격지를 들어가보지 않아도 로그를 볼 수 있다는 점이다.

빌드하기

master 브랜치 PR등으로 트리거가 일어나면 아래와같이 시작된다. 수동으로도 실행 할 수 있다.

그리고 중간중간 일부러 ls를 찍는것도 다 아래와 같이 따로 확인할 필요 없이 로그에 남기 때문이다.

서비스 재시작 역시 마찬가지다.

배포가 완료되면 각 단계별로 시간이 얼마나 걸렸는지도 알 수 있다.

더 해볼만한 생각

이 3단계에서 아티팩트 전송 단계를 더 세분화해 롤링 배포 등을 고려해볼 수 있겠다. 블루그린 전략은 L4 API까지 손을 댈 수 있어야 가능해 보인다. 이 글에서는 GitHub Actions로 아티팩트를 어떻게 들고 움직이고 배포에 활용할 수 있는지 엿볼 수 있는 과정을 보여주려고 했다.