[Spring Boot] Nginx + GitHub Actions + CodeDeploy + SSL 자동 배포 구축기

이재윤·2025년 7월 27일

SpringBoot와 Devops

목록 보기
6/6

Nginx + GitHub Actions + CodeDeploy + SSL 자동 배포 구축기

🔎 왜 Nginx를 사용했을까?

지난 포스터에서는 Spring Boot에서 자체적으로 SSL을 적용하고 있었습니다. 하지만 다음과 같은 이유로 Nginx를 리버스 프록시로 두고 SSL을 위임하는 구조로 전환하게 되었습니다.

  • 간편한 SSL 인증서 갱신: Let’s Encrypt + Certbot 조합은 Nginx와 궁합이 좋아 자동 갱신이 매우 쉬움
  • 정적 자원 캐싱 및 압축 가능: JS, CSS, 이미지 등 정적 리소스는 Nginx가 더 빠르게 서빙
  • 보안 및 무중단 설정 유연함: Spring Boot는 8080 포트에서 내부 실행 → Nginx가 외부 443 포트에서 HTTPS로 중계
  • 서비스 구조 분리: WAS(Spring Boot)와 Web Server(Nginx) 역할 분리가 가능하여 운영과 디버깅이 쉬움

🧩 전체 아키텍처 구성

[GitHub] → [GitHub Actions] → [S3] → [CodeDeploy] → [EC2] + [Nginx] → [HTTPS 응답]


📁 전체 프로젝트 구조

project/
├──.github/workflows/deploy.yml
├── build.gradle
├── appspec.yml
├── scripts/
│   ├── cleanup.sh
│   ├── stop.sh
│   └── start.sh
├── generate-properties.sh
├── src/main/resources/application-prod.properties

⚙️ 1. Nginx + SSL 인증서 발급

# Certbot + Nginx 설치
sudo apt install certbot python3-certbot-nginx

# 인증서 발급
sudo certbot --nginx -d yourdomain.com

⚙️ 2. Systemd 서비스 등록

/etc/systemd/system/springboot-app.service

[Unit]
Description=Spring Boot App
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/server
ExecStart=/usr/bin/java -Duser.timezone=Asia/Seoul -jar /home/ubuntu/server/spring-webapp.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
KillMode=control-group
TimeoutStopSec=20

StandardOutput=append:/home/ubuntu/server/logs/app.log
StandardError=append:/home/ubuntu/server/logs/error.log

[Install]
WantedBy=multi-user.target
# Systemd 서비스 등록
sudo nano /etc/systemd/system/springboot-app.service

# 재시작
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable springboot-app

🛠 3. GitHub Actions - deploy.yml

- name: Prepare deployment bundle
  run: |
    mkdir -p deploy/scripts
    cp build/libs/*.jar deploy/
    cp appspec.yml deploy/
    cp scripts/*.sh deploy/scripts/
    chmod +x deploy/scripts/*.sh

- name: Upload to AWS S3
  run: |
    aws deploy push \
      --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
      --ignore-hidden-files \
      --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
      --source ./deploy

- name: Deploy to AWS EC2 from S3
  run: |
    aws deploy create-deployment \
      --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
      --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
      --s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip

📦 4. appspec.yml

version: 0.0
os: linux

#배포 파일 설정
files:
  - source: /
    destination: /home/ubuntu/server
    overwrite: yes

#이미 있을 경우 덮어쓰기
file_exists_behavior: OVERWRITE

#files 섹션에서 복사한 파일에 대한 권한 설정
permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

#배포 이후에 실행할 일련의 라이프사이클
hooks:
  BeforeInstall:
    - location: scripts/cleanup.sh
      timeout: 60
      runas: ubuntu
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ubuntu
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas: ubuntu

🧹 5. scripts

cleanup.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ubuntu/server"
OLD_DIR="$PROJECT_ROOT/old"
LOG_DIR="$PROJECT_ROOT/logs"
DEPLOY_LOG="$LOG_DIR/deploy.log"
TIME_NOW=$(date +%c)

# old 디렉토리 없으면 생성
mkdir -p "$OLD_DIR/logs"

# 기존 JAR 백업
if [ -f "$PROJECT_ROOT/spring-webapp.jar" ]; then
  echo "$TIME_NOW > 기존 JAR 백업 시작" >> $DEPLOY_LOG
  mv "$PROJECT_ROOT/spring-webapp.jar" "$OLD_DIR/spring-webapp-$(date +%Y%m%d%H%M%S).jar"
  echo "$TIME_NOW > spring-webapp.jar 백업 완료" >> $DEPLOY_LOG
else
  echo "$TIME_NOW > 백업할 기존 JAR 없음" >> $DEPLOY_LOG
fi

# 로그 백업
for log_file in "$LOG_DIR/app.log" "$LOG_DIR/error.log"; do
  if [ -f "$log_file" ]; then
    mv "$log_file" "$OLD_DIR/logs/$(basename "$log_file" .log)-$(date +%Y%m%d%H%M%S).log"
    echo "$TIME_NOW > $(basename "$log_file") 백업 완료" >> $DEPLOY_LOG
  fi
done

# 오래된 로그 정리 (선택)
find "$OLD_DIR/logs" -name "*.log" -type f -mtime +14 -exec rm -f {} \;
echo "$TIME_NOW > 14일 초과 백업 로그 파일 정리 완료" >> $DEPLOY_LOG

start.sh

#!/usr/bin/env bash

DEPLOY_LOG="/home/ubuntu/server/deploy.log"
TIME_NOW=$(date +%c)
JAR_SOURCE=$(ls /home/ubuntu/server/*.jar | tail -n 1)
JAR_TARGET="/home/ubuntu/server/spring-webapp.jar"

echo "$TIME_NOW > 현재 Nginx 상태:" >> $DEPLOY_LOG
sudo systemctl status nginx >> $DEPLOY_LOG

echo "$TIME_NOW > 새 애플리케이션 복사 시작" >> $DEPLOY_LOG

if [ ! -f "$JAR_SOURCE" ]; then
  echo "$TIME_NOW > JAR 파일이 존재하지 않아 복사 실패!" >> $DEPLOY_LOG
  exit 1
fi

cp "$JAR_SOURCE" "$JAR_TARGET"
chmod +x "$JAR_TARGET"
echo "$TIME_NOW > JAR 복사 및 권한 부여 완료" >> $DEPLOY_LOG

echo "$TIME_NOW > springboot-app 서비스 재시작" >> $DEPLOY_LOG
sudo systemctl restart springboot-app

stop.sh

#!/usr/bin/env bash

DEPLOY_LOG="/home/ubuntu/server/deploy.log"
TIME_NOW=$(date +%c)

echo "$TIME_NOW > springboot-app 서비스 종료 시도" >> $DEPLOY_LOG

sudo systemctl stop springboot-app

if systemctl is-active --quiet springboot-app; then
  echo "$TIME_NOW > 종료 실패, 강제 종료 시도 필요" >> $DEPLOY_LOG
else
  echo "$TIME_NOW > springboot-app 서비스 정상 종료됨" >> $DEPLOY_LOG
fi

# 혹시 남아있는 java 프로세스 있는지 확인
PID=$(lsof -t -i:8080)

if [ -n "$PID" ]; then
  echo "$TIME_NOW > 포트 8080 열려 있음, PID $PID 강제 종료 시도" >> $DEPLOY_LOG
  kill -9 "$PID"
  echo "$TIME_NOW > PID $PID 강제 종료 완료" >> $DEPLOY_LOG
fi

application.properties

spring.profiles.active=prod

application-prod.properties

...(중략)

server.forward-headers-strategy=framework

🌐 Nginx 리버스 프록시 설정

server {
    listen 80;
    server_name yourdomain.com;

    # 인증서 발급용 (Let's Encrypt)
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    # 나머지는 HTTPS로 리다이렉트
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}
# Nginx 리버스 프록시 설정
sudo nano /etc/nginx/sites-enabled/default

🔁 인증서 자동 갱신

# 갱신 시뮬레이션
sudo certbot renew --dry-run

# systemd 타이머로 주기적 자동 실행 확인
sudo systemctl list-timers | grep certbot
결과해석
Tue 2025-07-08 17:32:47 UTC🔔 다음 실행 예정 시간
16h⏱ 현재로부터 16시간 후
certbot.timer🕒 systemd 타이머 이름
certbot.service🔁 실행될 서비스 이름
  • Certbot은 매일 2번, 일반적으로 12시간 간격으로 갱신 시도합니다.
  • 이때 실제로 인증서가 갱신될 필요가 있다면 → 자동 갱신 수행
  • 아직 유효 기간이 충분하다면 → 아무 일도 하지 않음

✅ 정리하며

이제 git push만 하면 끝입니다.

  • ✅ 자동 빌드
  • ✅ 자동 S3 업로드
  • ✅ 자동 배포
  • ✅ Nginx + HTTPS로 안전한 서비스 제공

🔧 Spring Boot + Nginx 배포 트러블슈팅 정리

Spring Boot 프로젝트를 AWS EC2에 배포하면서 겪었던 여러 이슈들과 그에 대한 해결 과정을 정리합니다. 특히 systemd 서비스 종료 실패, Swagger CORS 문제 등을 중심으로 정리했습니다.


✅ 문제 1: systemctl stop springboot-app 했는데도 서버가 꺼지지 않음

🔍 증상

  • systemctl stop 명령어를 실행 했지만, 서버는 여전히 구동 중
  • curl localhost:8080 결과가 나옴
  • ps -ef | grep spring-webapp.jar 하면 java 프로세스가 살아있음

🔎 원인

  • springboot-app.serviceKillMode, Restart의 옵션이 누락되어 Spring Boot 프로세스를 제대로 종료하지 못함

✅ 해결 방법

/etc/systemd/system/springboot-app.service 파일을 아래와 같이 수정

[Service]
...(중략)

SuccessExitStatus=143
Restart=on-failure
RestartSec=5
KillMode=control-group
TimeoutStopSec=20

stop.sh 파일에 프로세스 종료 코드 추가 (혹시 종료를 못할 경우 대비)

...(중략)

if [ -n "$PID" ]; then
  echo "$TIME_NOW > 포트 8080 열려 있음, PID $PID 강제 종료 시도" >> $DEPLOY_LOG
  kill -9 "$PID"
  echo "$TIME_NOW > PID $PID 강제 종료 완료" >> $DEPLOY_LOG
fi

✅ 문제 2: Swagger 접속 시 Failed to fetch / CORS, Network, URL scheme 오류

🔍 증상

  • Swagger UI에 접속은 되지만, API 요청 시 Failed to fetch 메시지 발생

🔎 원인

  • 서버를 Spring Boot와 Nginx로 분리했기 때문에 Spring Boot는 클라이언트에게 받은 요청이 http://localhost:8080에서 왔다고 착각하여 생긴 문제
  • 따라서, HTTPS 환경에서 Swagger가 자동으로 HTTP URL로 요청하는 문제

✅ 해결 방법

Nginx 러버스 프록시 설정 추가

server {
    ...(중략)

    location / {
        ...(중략)
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}
설정의미사용 목적
X-Forwarded-Proto: $scheme원래 요청 프로토콜 (http/https)Swagger 등에서 URL scheme 올바르게 구성
X-Forwarded-Host: $host원래 요청 호스트 (도메인)리다이렉션이나 Swagger base URL 등 도메인 기반 처리

application-prod.properties에 설정 추가

...(중략)

server.forward-headers-strategy=framework

✅ 기타 팁

  • Nginx 설정 수정 후에는 반드시 아래 명령어로 반영해야 함:
sudo nginx -t && sudo systemctl reload nginx
  • systemd 서비스 수정 후에는 반드시 아래 명령어 실행:
sudo systemctl daemon-reexec
sudo systemctl daemon-reload

✅ 마무리

이슈들이 한 번에 해결되지 않아 꽤 시간을 잡아먹었지만, 결국 하나씩 로그와 상태를 보며 차근차근 해결할 수 있었습니다. 비슷한 환경에서 Spring Boot + Nginx로 배포하는 분들에게 도움이 되었으면 합니다.




아직 학생이라 부족한 점이 있을 수 있습니다. 댓글과 피드백은 언제든지 환영입니다!

profile
부족한 점이 많습니다. 피드백은 환영입니다!

0개의 댓글