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

[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
# Certbot + Nginx 설치
sudo apt install certbot python3-certbot-nginx
# 인증서 발급
sudo certbot --nginx -d yourdomain.com
/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
- 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
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
#!/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
#!/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
#!/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
spring.profiles.active=prod
...(중략)
server.forward-headers-strategy=framework
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 | 🔁 실행될 서비스 이름 |
이제 git push만 하면 끝입니다.
Spring Boot 프로젝트를 AWS EC2에 배포하면서 겪었던 여러 이슈들과 그에 대한 해결 과정을 정리합니다. 특히 systemd 서비스 종료 실패, Swagger CORS 문제 등을 중심으로 정리했습니다.
systemctl stop springboot-app 했는데도 서버가 꺼지지 않음systemctl stop 명령어를 실행 했지만, 서버는 여전히 구동 중curl localhost:8080 결과가 나옴ps -ef | grep spring-webapp.jar 하면 java 프로세스가 살아있음springboot-app.service에 KillMode, 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
Failed to fetch / CORS, Network, URL scheme 오류
Failed to fetch 메시지 발생http://localhost:8080에서 왔다고 착각하여 생긴 문제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 등 도메인 기반 처리 |
...(중략)
server.forward-headers-strategy=framework
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
이슈들이 한 번에 해결되지 않아 꽤 시간을 잡아먹었지만, 결국 하나씩 로그와 상태를 보며 차근차근 해결할 수 있었습니다. 비슷한 환경에서 Spring Boot + Nginx로 배포하는 분들에게 도움이 되었으면 합니다.
아직 학생이라 부족한 점이 있을 수 있습니다. 댓글과 피드백은 언제든지 환영입니다!