0. 서론
단일 EC2 인스턴스에서 WAS와 DB를 함께 구동해야 하는 요구사항이 발생하였다. 수동 배포 과정에서 발생하는 여러 오류(테스트 실패, 환경변수 누락 등)를 줄이고, 배포 자동화에 대한 니즈가 커졌다.
AWS CodeDeploy + S3 방식도 존재하나, 현재 환경에서는 GitHub Actions를 활용한 단순 배포 방식이 유지보수에 유리하다고 판단되어 이를 선택하였다.
위와 같은 이유로 Docker 또한 사용하지 않도록 하였다.
1. 배포 플로우 정의
배포 시나리오
- 최초 EC2 인스턴스 생성 및 서버 구동
- 기존 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을 통한 알림 연동 등 모니터링도 추가되면 좋을듯…?
'날것 그대로의 공부기록 > 날 것 그대로 일지' 카테고리의 다른 글
| python에서 멀티스레드 적용하기 (1) | 2025.08.04 |
|---|---|
| 컨트롤러만 테스트하기 (2) | 2025.08.04 |
| 동시성 환경에서 낙관적 락 + 재시도 전략 테스트 (참치캔 등록) (4) | 2025.08.04 |
| 23년 6월 18일 생각들 (0) | 2023.06.18 |