본문 바로가기
날것 그대로의 공부기록/날 것 그대로 일지

github action 자동배포

by 20j 2025. 8. 4.

0. 서론

단일 EC2 인스턴스에서 WAS와 DB를 함께 구동해야 하는 요구사항이 발생하였다. 수동 배포 과정에서 발생하는 여러 오류(테스트 실패, 환경변수 누락 등)를 줄이고, 배포 자동화에 대한 니즈가 커졌다.

AWS CodeDeploy + S3 방식도 존재하나, 현재 환경에서는 GitHub Actions를 활용한 단순 배포 방식이 유지보수에 유리하다고 판단되어 이를 선택하였다.

위와 같은 이유로 Docker 또한 사용하지 않도록 하였다.


1. 배포 플로우 정의

배포 시나리오

  1. 최초 EC2 인스턴스 생성 및 서버 구동
  2. 기존 EC2 인스턴스에 대한 서버 업데이트

초기 setting이 잡혀있지않는 (DB 설정 및 java download) 경우는 추후 쉘 스크립트 등을 이용하여 초기설정을 해줄 수 있도록 하고, 현재는 이미 배포중인 서버를 종료한 뒤 새로운 서버를 배포할 수 있는 시나리오만을 생각하겠다.

현 트래픽 예상 상 단일 인스턴스 서버로 충분할 것이며, DB서버 또한 하나의 인스턴스 내부에 들어있고, 인스턴스에 문제가 생길 경우 새로운 인스턴스를 통해서 복구하는 과정을 자동화하기에는 비용 대비 효율이 적게 나오며 어쩔 수 없이 사람의 개입이 필요해질 수 있다고 생각되기 때문이다.


2. 전체 Workflow 요약

  • 트리거 조건: release 브랜치에 push 발생 시
  • job 구성:
    • build: 애플리케이션 빌드 및 아티팩트 업로드
    • deploy: EC2 서버로 파일 복사 및 실행

→ 즉, 새로운 기능 추가 등의 업데이트에서 자동 배포될 수 있도록 하는 것이 목적이다.

 

3. 워크플로우 상세 설명

GitHub Actions 설정 파일 (예시)

name: cicd

on:
  push:
    branches:
      - release

permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Gradle permission
        run: chmod +x gradlew

      - name: 애플리케이션 시크릿 파일 생성
        run: echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.yml

      - name: Gradle Build (테스트 제외)
        run: ./gradlew build -x test

      - name: JAR 파일명 변경
        run: mv ./build/libs/*SNAPSHOT.jar ./build/libs/app-server.jar

      - name: Build 결과 업로드
        uses: actions/upload-artifact@v4
        with:
          name: app-server.jar
          path: build/libs/app-server.jar

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: 아티팩트 다운로드
        uses: actions/download-artifact@v4
        with:
          name: app-server.jar

      - name: EC2로 복사
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_KEY }}
          source: "app-server.jar"
          target: "/server"

      - name: 서버 실행
        uses: appleboy/ssh-action@v1.2.2
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_KEY }}
          script: |
            echo "기존 서버 프로세스 종료 시도..."
            PID=$(ps -ef | grep 'app-server.jar' | grep -v grep | awk '{print $2}')
            if [ -n "$PID" ]; then
            echo "종료할 PID: $PID"
            sudo kill -15 $PID
            sleep 5
            echo "강제 종료 확인..."
            PID2=$(ps -ef | grep 'app-server.jar' | grep -v grep | awk '{print $2}')
            if [ -n "$PID2" ]; then
            echo "아직 살아있는 프로세스가 있어 kill -9 실행"
            sudo kill -9 $PID2
            fi
            else
            echo "실행 중인 서버가 없습니다."
            fi
			      echo "서버 재시작..."
            sudo nohup java -Dspring.profiles.active=release -jar /server/app-server.jar > /server/output.log 2>&1 &
            echo "서버 시작 완료"


4. 주요 구성 요소 설명

on.push.branches

  • release 브랜치에 push 시 워크플로우가 실행된다.

jobs

  • build: JDK 설정, Gradle 빌드, 아티팩트 업로드
  • deploy: EC2 파일 복사 및 서버 실행

주요 Actions

Action 설명

actions/checkout 리포지토리 체크아웃
actions/setup-java JDK 설치
gradle/actions/setup-gradle Gradle 캐시 세팅
appleboy/scp-action EC2로 파일 전송
appleboy/ssh-action EC2 내부 명령 실행

왜 upload와 download 과정이 존재하는가?

GitHub Action을 사용하면 기본적으로 runner 서버를 할당받아 작업을 진행하게 된다.

이때 job마다 각 작업은 격리된 환경에서 실행되며, 이전 job이 종료되면 해당 job에서 생성된 데이터는 따로 보관하지 않는 이상 모두 삭제된다.

빌드 이후 바로 scp로 전송해도 되지만, 빌드와 배포 과정을 분리해 관리하고자 jobs을 나누었고, 이 과정에서 빌드 파일을 보관하기 위해 upload-artifact 및 download-artifact를 사용하였다.

  • upload-artifact 사용 시
    • name에는 업로드할 아티팩트의 이름을 지정하고,
    • path에는
      • 경로만 입력하면 해당 디렉토리 내부의 모든 파일이 업로드되며,
      • 특정 파일을 입력하면 해당 파일만 업로드된다.
  • download-artifact는 별도의 경로를 지정하지 않으면 기본 워킹 디렉토리에 다운로드된다.

5. Secrets 구성

GitHub Repository → Settings → Secrets and variables → Actions 에서 등록

이름 설명

EC2_HOST EC2 퍼블릭 IP 혹은 DNS
EC2_USER EC2 접속 사용자명 (예: ubuntu)
EC2_KEY EC2 접속용 private key (BEGIN, END 포함)
APPLICATION_SECRET 앱 내부에서 사용하는 시크릿 설정값 등

EC2_KEY : EC2에서 발급받은 .pem 파일의 모든 내용을 복사하여 입력해주어야한다.

APPLICATION_SECRET : 테스트용도로 현재와 같이 구성하였지만, 조금 위험한 방식이다.

각각 필요한 값들을 환경변수로 설정한 뒤, ssh-action에서

script: |
      export APP_SECRET_KEY=${{ secrets.APP_SECRET_KEY }}
      export DB_USERNAME=${{ secrets.DB_USERNAME }}
      export DB_PASSWORD=${{ secrets.DB_PASSWORD }}

이런 방식으로 사용할 수 있도록 해주자!


6. 문제 해결 및 팁

1. 빌드 실패 (캐시 관련)

  • Cache service responded with 503 오류는 캐시 서버 이슈이므로 무시.

2. 권한 오류 (Permission denied)

  • scp-action을 사용하면서 생각보다 많은 오류를 겪었다.

처음에는 권한을 부여하면 된다고 생각하여

sudo mkdir -p /server
sudo chown ubuntu:ubuntu /server
sudo chmod 755 /server

을 설정해주었지만 계속하여

remote server os type is unix
scp file to server.
create folder /home/***/server
untar file ivQxFGiTcG.tar.gz
error:  tar: app-server.jar: Cannot open: Permission denied
tar: Exiting with failure status due to previous errors
drone-scp error:  Process exited with status 2
drone-scp rollback: remove all target tmp file
remove file ivQxFGiTcG.tar.gz
2025/06/23 02:24:45 Process exited with status 2

해당 오류가 발생하였다.

원인은 /home/ubuntu/ 디렉토리에서 작업을 하도록 설정해두었는데, 해당 디렉토리의 권한이 없어서 생기는 문제였다.

- name: EC2로 복사
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_KEY }}
          source: "app-server.jar"
          target: "/server"

target을 /server 로 변경하여 root에 server 디렉토리 생성 이후 작업하도록 변경하였더니 문제가 해결되었다.

3. JAR 파일 중복 문제

  • build/libs 내부에 -plain.jar 파일도 함께 생성됨
  • 명확한 실행 JAR 파일명을 정해 mv 명령으로 이름 통일
  • application.xml에 jar { enabled = false } 설정을 추가하게 되면 plain.jar 가 생성되지 않게 된다.

7. 깃허브 액션 비용

기본적으로 한달에 2000분의 사용시간이 제공되며,

깃허브에서 setting → billing and licensing 탭에 들어가면 현재 비용을 확인할 수 있다.

 

8. 참고 자료


9. 향후 개선사항

서버 장애 순간에서 복구를 위한 로직이 추가되면 좋을 것 같다.

또한 slack을 통한 알림 연동 등 모니터링도 추가되면 좋을듯…?