쉽게 쉽게

[AWS] AWS에 프로젝트 배포하기 (3) - 배포 간략화 본문

인프라 & 배포/AWS

[AWS] AWS에 프로젝트 배포하기 (3) - 배포 간략화

곱마2 2026. 6. 19. 15:58
반응형

📌 핵심 요약
  • 수동 배포 5단계 → 명령어 1줄로 축소 — 빌드 후 bash ~/deploy.sh 한 번으로 전송·교체·재시작·헬스체크까지 자동 처리
  • 빌드는 IntelliJ, 배포는 WSL에서 분리 실행 — WSL에 Java가 없어도 문제없고, 컴파일 오류를 IDE에서 바로 확인 가능
  • 자동 백업으로 롤백 대비 — 배포할 때마다 이전 JAR를 app-prev.jar로 보관해 문제 발생 시 즉시 복구 가능

1 자동화 개요

이전 배포과정을 통해 Spring Boot 프로젝트를 AWS EC2에 배포하는 데 성공했을 겁니다.

다만 배포 및 재기동은 단 한 번만 필요한 것이 아니라 주기적으로 발생할 수 있는 일입니다.

때문에 복잡한 수동 배포 과정을 자동화하여 향후 프로젝트 관리에 도움이 될 수 있도록 하려고 합니다.

2 왜 자동화가 필요했을까?

자동화 스크립트를 만들기 전, 코드를 수정할 때마다 아래 5단계를 손으로 직접 입력해야 했습니다.

자동화 전 — 수동 배포 5단계

 
Bash — 1단계: IntelliJ 터미널에서 빌드
./gradlew clean bootJar
 
Bash — 2~5단계: WSL에서 전송 후 서버 접속하여 교체·권한·재시작
# 2. 서버로 파일 전송
scp -i "~/.ssh/your-key.pem" "/mnt/c/.../your-app.jar" ubuntu@YOUR_IP:~/app.jar

# 3. 서버 접속
ssh -i "~/.ssh/your-key.pem" ubuntu@YOUR_IP

# 4. 파일 교체 및 권한 설정
sudo mv ~/app.jar /opt/your-app/app.jar
sudo chown appuser:appuser /opt/your-app/app.jar

# 5. 서비스 재시작
sudo systemctl restart your-app
⚠️
이런 문제가 반복됩니다
명령어가 길어서 한 글자라도 틀리면 오류가 납니다. 특히 파일 경로, 권한 설정, 계정명은 실수하기 쉬운 포인트입니다. 배포할 때마다 "내가 맞게 입력한 게 맞나?" 라는 불안감도 생깁니다.

3 전체 스크립트 한눈에 보기

먼저 완성된 스크립트 전체를 보여드립니다.

대문자로 표시된 설정 변수 블록만 본인 환경에 맞게 수정하면 바로 사용할 수 있습니다.

각 단계가 무엇을 하는지는 바로 아래 표와 이후 섹션에서 자세히 설명합니다.

 
Bash — deploy.sh (전체 완성본)
#!/usr/bin/env bash
set -euo pipefail

# ── 설정 변수 (이 블록만 수정하세요) ──────────────
SERVER="YOUR_SERVER_IP"
SSH_USER="ubuntu"
PEM="$HOME/.ssh/your-key.pem"
APP_DIR="/opt/your-app"
SERVICE_NAME="your-app"
APP_USER="appuser"
JAR_PATH="/mnt/c/deploy/app.jar"
# ──────────────────────────────────────────────────

# ── 색상 출력 헬퍼 ────────────────────────────────
info()  { echo -e "\033[34m[INFO]\033[0m  $*"; }
ok()    { echo -e "\033[32m[OK]\033[0m    $*"; }
error() { echo -e "\033[31m[ERROR]\033[0m $*"; exit 1; }

# STEP 1. JAR 파일 존재 확인
info "STEP 1 — 빌드 파일 확인"
if [[ ! -f "$JAR_PATH" ]]; then
    error "JAR 파일이 없습니다. IntelliJ에서 './gradlew clean bootJar' 먼저 실행하세요."
fi
ok "JAR 확인 완료"

# STEP 2. 서버로 전송
info "STEP 2 — 서버로 JAR 전송"
scp -i "$PEM" "$JAR_PATH" "${SSH_USER}@${SERVER}:~/app.jar"
ok "전송 완료"

# STEP 3. 백업 · 교체 · 권한 · 재시작
info "STEP 3 — 서비스 교체 및 재시작"
ssh -i "$PEM" "${SSH_USER}@${SERVER}" bash << EOF
    set -e
    if [[ -f "${APP_DIR}/app.jar" ]]; then
        sudo mv "${APP_DIR}/app.jar" "${APP_DIR}/app-prev.jar"
    fi
    sudo mv ~/app.jar "${APP_DIR}/app.jar"
    sudo chown ${APP_USER}:${APP_USER} "${APP_DIR}/app.jar"
    sudo systemctl restart ${SERVICE_NAME}
EOF
ok "재시작 요청 완료"

# STEP 4. 헬스 체크 (5초 간격, 최대 60초)
info "STEP 4 — 기동 대기 및 헬스 체크 (최대 60초)"
for i in {1..12}; do
    sleep 5
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${SERVER}/" || echo "000")
    if [[ "$HTTP_STATUS" == "200" ]]; then
        ok "배포 성공! (HTTP ${HTTP_STATUS})"
        exit 0
    fi
    echo "  기동 대기 중... (${i}/12, 현재 HTTP ${HTTP_STATUS})"
done
error "60초 내 기동 실패 — 로그 확인: sudo journalctl -u ${SERVICE_NAME} -n 50"

4 스크립트가 하는 일 한눈에 보기

변수를 설정하고 나면 스크립트가 아래 4단계를 순서대로 자동 실행합니다.

단계 하는 일 왜 필요한가
STEP 1 빌드 파일 확인 빌드를 깜빡하고 스크립트를 실행하는 실수를 방지합니다.
JAR 파일이 없으면 즉시 중단해 엉뚱한 배포를 막습니다.
STEP 2 서버로 JAR 전송 pem 키 인증으로 로컬의 빌드 결과물을 서버 홈 디렉토리로 안전하게 복사합니다.
앱 디렉토리에 직접 쓸 수 없어 홈을 경유합니다.
STEP 3 파일 교체 · 권한 설정 · 서비스 재시작 기존 JAR를 app-prev.jar로 백업한 뒤 새 파일로 교체합니다.
소유자를 앱 전용 계정으로 맞추고 systemd 서비스를 재시작합니다.
STEP 4 헬스 체크 Spring Boot 기동에 시간이 걸리므로 5초 간격으로 최대 60초간 HTTP 200 응답을 확인합니다. 실패 시 로그 확인 명령을 안내합니다.

5 빌드와 배포, 왜 분리했을까?

자동화 스크립트를 만들 때 "빌드(소스코드 → JAR 파일 변환)도 스크립트 안에서 처리할까?"라고 생각할 수 있습니다.

하지만 빌드는 IntelliJ에서, 배포는 WSL 스크립트로 분리하는 것이 좋습니다.

ℹ️
WSL이란?
WSL(Windows Subsystem for Linux)은 Windows 안에서 Linux 명령어를 사용할 수 있게 해주는 도구입니다. 별도의 가상 머신 없이도 scp, ssh 같은 Linux 명령어를 Windows에서 실행할 수 있습니다.
구분 WSL에서 빌드하면? IntelliJ에서 빌드하면?
Java 설치 별도 설치 필요 JDK 내장
오류 확인 터미널 텍스트만 IDE 강조 표시
빌드 속도 환경 설정 필요 증분 빌드 지원

6 각 단계 자세히 살펴보기

사전 준비 — JAR 출력 경로 고정하기

스크립트의 JAR_PATH에는 빌드 결과물인 JAR 파일의 경로를 지정해야 합니다.

기본 설정 그대로 빌드하면 경로가 아래처럼 길고, 버전 번호까지 포함되어 있습니다.

 
Bash — 기본 빌드 결과물 경로 (변경 전)
JAR_PATH="/mnt/c/Users/YourName/your-project/build/libs/your-app-0.0.1-SNAPSHOT.jar"
# 문제 1: 경로가 너무 길다
# 문제 2: 버전이 바뀌면 (0.0.1 → 0.0.2) 스크립트도 매번 수정해야 한다

프로젝트 build.gradlebootJar 블록에 출력 경로와 파일명을 고정하면 이 문제를 한 번에 해결할 수 있습니다.

 
Groovy — build.gradle
bootJar {
    destinationDirectory = file('C:/deploy')   // JAR가 저장될 폴더
    archiveFileName      = 'app.jar'           // 버전 번호 없이 고정 파일명
}
// bootJar: ./gradlew clean bootJar 실행 시 동작하는 태스크 설정 블록
// destinationDirectory: JAR 파일이 저장될 경로 (C:/deploy 폴더가 자동 생성됨)
// archiveFileName: 생성되는 파일 이름 고정 (기본값은 앱이름-버전.jar)

이제 JAR_PATH를 아래처럼 짧게 쓸 수 있습니다.

 
Bash — 변경 후 JAR_PATH
JAR_PATH="/mnt/c/deploy/app.jar"
# C:/deploy 폴더를 WSL 경로로 표현한 것 (/mnt/c/deploy)
# 버전이 바뀌어도 이 경로는 수정할 필요가 없습니다
💡
C:/deploy 폴더는 자동으로 만들어집니다
destinationDirectory에 지정한 폴더가 없어도 ./gradlew clean bootJar 실행 시 Gradle이 자동으로 생성합니다. 별도로 폴더를 만들 필요가 없습니다.

설정 변수 블록 — 여기만 수정하면 됩니다

스크립트 맨 위의 변수 블록에 값을 한 번만 채워두면, 이후에 서버 IP나 경로가 바뀌어도 이 블록만 수정하면 됩니다.

 
Bash — 설정 변수 블록 설명
SERVER="YOUR_SERVER_IP"     # EC2 퍼블릭 IP (또는 Elastic IP)
SSH_USER="ubuntu"          # SSH 접속 계정 (AWS EC2 기본값: ubuntu)
PEM="$HOME/.ssh/your-key.pem"
                           # pem: AWS에서 발급한 SSH 인증 키 파일
APP_DIR="/opt/your-app"    # 서버에서 앱이 저장된 디렉토리
SERVICE_NAME="your-app"    # systemd에 등록한 서비스 이름
APP_USER="appuser"         # 서버에서 앱을 실행하는 전용 계정
JAR_PATH="/mnt/c/deploy/app.jar"
                           # build.gradle bootJar 블록에서 출력 경로를 고정
                           # 버전이 바뀌어도 파일명이 app.jar로 유지됩니다
💡
Windows 경로를 WSL에서 쓰는 법
Windows의 C:\deploy\는 WSL에서 /mnt/c/deploy/로 씁니다. C:\/mnt/c/로 바뀌고, 역슬래시(\)가 슬래시(/)로 바뀝니다.

STEP 1 — JAR 파일 존재 확인

빌드를 깜빡하고 스크립트를 실행하는 실수를 막기 위해, 배포 전 JAR 파일이 실제로 있는지 먼저 확인합니다.

파일이 없으면 안내 메시지와 함께 즉시 중단합니다.

 
Bash — STEP 1. 파일 존재 확인
if [[ ! -f "$JAR_PATH" ]]; then
    error "JAR 파일이 없습니다. IntelliJ에서 './gradlew clean bootJar' 먼저 실행하세요."
fi
# [[ ! -f "$JAR_PATH" ]]: 해당 경로에 파일이 없으면(-f의 반대, !) 조건 성립
# error 함수: 메시지 출력 후 exit 1 (스크립트 강제 종료)

STEP 2 — 서버로 파일 전송 (scp)

로컬의 JAR 파일을 서버로 복사합니다. scp는 SSH 방식으로 파일을 안전하게 전송하는 명령어입니다.

앱 디렉토리(/opt/your-app)는 전용 계정만 쓸 수 있어서, 일단 ubuntu 홈 디렉터리(~/)로 보낸 뒤 다음 단계에서 sudo로 이동합니다.

 
Bash — STEP 2. scp 파일 전송
scp -i "$PEM" "$JAR_PATH" "${SSH_USER}@${SERVER}:~/app.jar"
# scp: SSH 기반 파일 복사 명령어 (Secure Copy Protocol)
# -i "$PEM": 비밀번호 대신 pem 키 파일로 인증
# ~/app.jar: 서버의 ubuntu 홈 디렉토리에 app.jar 이름으로 저장

STEP 3 — 교체 · 권한 설정 · 서비스 재시작

SSH로 서버에 접속해 명령을 실행합니다.

<< EOF ... EOF 형태(heredoc)로 여러 명령을 묶으면 SSH 접속을 한 번만 열어도 됩니다.

 
Bash — STEP 3. SSH 원격 명령 실행
ssh -i "$PEM" "${SSH_USER}@${SERVER}" bash << EOF
    # ① 기존 app.jar를 app-prev.jar로 백업 (롤백 대비)
    if [[ -f "${APP_DIR}/app.jar" ]]; then
        sudo mv "${APP_DIR}/app.jar" "${APP_DIR}/app-prev.jar"
    fi
    # ② 홈에 올려둔 새 JAR를 앱 디렉토리로 이동
    sudo mv ~/app.jar "${APP_DIR}/app.jar"
    # ③ 소유자를 앱 실행 계정으로 변경 (chown = change owner)
    sudo chown ${APP_USER}:${APP_USER} "${APP_DIR}/app.jar"
    # ④ systemd 서비스 재시작 (systemd = 리눅스 서비스 관리 도구)
    sudo systemctl restart ${SERVICE_NAME}
EOF
ℹ️
heredoc이란? (<< EOF ... EOF)
<< EOF부터 EOF까지의 내용을 하나의 텍스트 덩어리로 넘기는 셸 문법입니다. 여기서는 SSH에 연결된 서버가 이 구간 안의 명령들을 순서대로 실행합니다. SSH 접속을 여러 번 열지 않아도 되어 훨씬 효율적입니다.

STEP 4 — 헬스 체크

Spring Boot 앱은 시작하는 데 수 초가 걸립니다. 재시작 직후 바로 확인하면 아직 뜨지 않은 상태라 실패로 오해할 수 있습니다.

그래서 5초 간격으로 최대 12번(60초)까지 HTTP 응답을 기다립니다.

 
Bash — STEP 4. 헬스 체크
for i in {1..12}; do
    sleep 5
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://${SERVER}/")
    # curl: URL로 HTTP 요청을 보내는 명령어
    # -s: 진행 상황 숨김  -o /dev/null: 응답 본문 버림  -w "%{http_code}": 상태코드만 출력
    if [[ "$HTTP_STATUS" == "200" ]]; then
        ok "배포 성공! (HTTP ${HTTP_STATUS})"
        exit 0
    fi
    echo "  기동 대기 중... (${i}/12, 현재 HTTP ${HTTP_STATUS})"
done
error "60초 내 기동 실패 — 로그 확인: sudo journalctl -u ${SERVICE_NAME} -n 50"
💡
왜 80번 포트(Nginx)로 확인하나요?
Spring Boot는 기본적으로 8080 포트에서 실행됩니다. 하지만 실제 사용자는 Nginx(80 포트)를 통해 접속합니다. 헬스 체크도 80 포트(http://SERVER/)로 확인해야 실제 사용자 접속 경로와 동일하게 검증할 수 있습니다.

7 자동 백업과 롤백 방법

스크립트는 배포할 때마다 기존 app.jarapp-prev.jar로 이름을 바꿔 보관합니다.

새 버전에 문제가 생겼을 때 이 파일로 즉시 되돌릴 수 있습니다.

 
Bash — 롤백 명령어 (문제 발생 시)
ssh -i "~/.ssh/your-key.pem" ubuntu@YOUR_SERVER_IP \
  'sudo mv /opt/your-app/app-prev.jar /opt/your-app/app.jar \
   && sudo chown appuser:appuser /opt/your-app/app.jar \
   && sudo systemctl restart your-app'
🚨
롤백은 직전 버전 1개만 가능합니다
배포할 때마다 app-prev.jar가 덮어써집니다. 즉, 바로 직전 버전으로만 롤백할 수 있습니다. 불안정한 코드를 연속으로 배포하지 않도록 주의하세요.

8 처음 설정하고 사용하는 방법

스크립트 저장 위치

스크립트는 두 곳에 보관합니다. 프로젝트 폴더 안에는 Git으로 관리되는 원본을, WSL 홈에는 실행용 사본을 둡니다.

 
Bash — 최초 설정 순서
# 1. 프로젝트 안에 스크립트 보관 (Git 관리)
your-project/
└── deploy/
    └── deploy.sh   # 원본 (팀원과 공유, Git에 커밋)

# 2. WSL 홈으로 복사 (실행용)
cp "/mnt/c/Users/YourName/your-project/deploy/deploy.sh" ~/deploy.sh
⚠️
한글 경로 오류가 발생한다면
WSL에서 한글이 포함된 경로를 인식하지 못하는 경우가 있습니다. cp 실행 시 No such file or directory 오류가 발생한다면, deploy.sh를 먼저 C:\deploy\ 폴더에 스크립트를 복사해둔 뒤 아래 명령어를 사용하세요.

이후 배포 루틴 — 딱 2단계

 
Bash — 매번 배포할 때 하는 것
# STEP 1. IntelliJ 터미널에서 빌드
./gradlew clean bootJar

# STEP 2. WSL에서 배포 스크립트 실행 — 이게 전부입니다
bash ~/deploy.sh

9 정리 — 자동화로 얻은 것

항목 자동화 전 자동화 후
배포 단계 5단계 (직접 입력) 2단계 (빌드 + 스크립트)
실수 가능성 높음 (경로·계정·권한) 낮음 (변수 블록만 수정)
백업 없음 자동 (app-prev.jar)
배포 확인 수동 (브라우저 새로고침) 자동 (헬스 체크)
💡
다음 단계로 발전시키려면
이 스크립트는 기본형입니다. 익숙해지면 GitHub ActionsJenkins 같은 CI/CD 도구를 연동해서 "코드 push → 자동 빌드 → 자동 배포"까지 완전 자동화할 수 있습니다. 하지만 지금처럼 직접 스크립트를 짜보는 경험이 CI/CD의 원리를 이해하는 데 훨씬 도움이 됩니다.

잘못된 내용이 있다면 지적부탁드립니다. 방문해주셔서 감사합니다.

 

 

반응형