CI/CD 파이프라인 구조
CI/CD 파이프라인은 크게 두 단계로 구성했습니다
- Build Job: 코드 빌드 및 검증
- Deploy Job: Docker 이미지 빌드, ECR 푸시, EC2 배포
1. GitHub Actions 워크플로우 트리거 설정
워크플로우는 main 브랜치에 대한 push 또는 pull request가 열릴 때 실행됩니다
name: CI/CD Pipeline
on:
push:
branches: ['main']
pull_request:
branches: ['main']
types: [opened, synchronize, reopened]
2. Build Job - 코드 빌드 및 검증
첫 번째 단계에서는 소스 코드를 체크아웃하고 Java 21 환경을 설정한 후 Gradle을 사용하여 애플리케이션을 빌드합니다.
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew build -x test
-x test옵션으로 테스트를 제외하고 빌드 속도를 최적화 (필요시 테스트 포함 가능)temurin배포판 사용
3. Deploy Job - Docker 이미지 빌드 및 배포
Build Job이 성공적으로 완료된 후, main 브랜치에 직접 push된 경우에만 Deploy Job 이 실행됩니다
3.1 AWS 인증 및 ECR 로그인
deploy:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
보안 설정
- 모든 민감한 정보(AWS 자격증명, DB 정보 등)는 GitHub Secrets에 저장
- 절대 코드에 직접 하드코딩하면 안됨!!!!
3.2 Docker 이미지 빌드 및 ECR 푸시
Git commit SHA를 이미지 태그로 사용하여 각 배포를 고유하게 식별합니다
- name: Build, tag, and push image to Amazon ECR
run: |
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
ECR_REPOSITORY="travodo"
IMAGE_TAG="${{ github.sha }}"
docker build -t "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
docker push "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
3.3 Dockerfile 구조 (Multi-stage Build)
효율으로 이미지를 생성하기 위해 Multi-stage build를 활용합니다
# 1단계: 빌드
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew build -x test
# 2단계: 런타임
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# 타임존/UTF-8 세팅
ENV TZ=Asia/Seoul
ENV LANG=C.UTF-8
# 빌드된 JAR 파일만 런타임 이미지로 복사합니다.
COPY --from=builder /app/build/libs/*.jar app.jar
# HTTP 포트 노출
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
왜 Multi-stage Build를 사용했는가?
- 최종 이미지 크기 감소 (JDK 대신 JRE만 포함)
- 빌드 도구가 런타임 이미지에 포함되지 않아 보안 강화
- 빌드 캐시 최적화로 빌드 시간 단축
3.4 EC2 인스턴스에 배포
SSH를 통해 EC2 인스턴스에 접속하여 새 이미지를 배포합니다
- name: Deploy to EC2 instance
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
port: 22
timeout: 60s
command_timeout: 10m
debug: true
script: |
# AWS CLI 설정
aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws configure set region ${{ secrets.AWS_REGION }}
ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
ECR_REPOSITORY="travodo"
IMAGE_TAG="${{ github.sha }}"
# ECR 로그인
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin "$ECR_REGISTRY"
# 최신 이미지 Pull
docker pull "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
# 기존 컨테이너 중지 및 삭제
docker stop travodo-app || true
docker rm travodo-app || true
# 새 컨테이너 실행
docker run -d --name travodo-app --restart unless-stopped \
-p 8080:8080 \
-e TZ=Asia/Seoul \
-e SPRING_PROFILES_ACTIVE=${{ secrets.SPRING_PROFILES_ACTIVE }} \
-e DDL_AUTO=update \
-e DB_HOST=${{ secrets.DB_HOST }} \
-e DB_PORT=3306 \
-e DB_NAME=${{ secrets.DB_NAME }} \
-e DB_USERNAME=${{ secrets.DB_USERNAME }} \
-e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
-e JWT_SECRET=${{ secrets.JWT_SECRET }} \
-e MAIL_USERNAME=${{ secrets.MAIL_USERNAME }} \
-e MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }} \
-e FRONTEND_URL=${{ secrets.FRONTEND_URL }} \
-e KAKAO_REST_API_KEY=${{ secrets.KAKAO_REST_API_KEY }} \
-e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
-e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
-e AWS_S3_BASE_URL=${{ secrets.AWS_S3_BASE_URL }} \
-e S3_REGION=${{ secrets.S3_REGION }} \
-e S3_BUCKETNAME=${{ secrets.S3_BUCKETNAME }} \
"$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
배포 프로세스
- ECR에서 최신 이미지 Pull
- 기존 컨테이너 안전하게 중지 및 제거 (
|| true로 실패 시에도 계속 진행) - 환경 변수를 통해 애플리케이션 설정 주입
--restart unless-stopped옵션으로 자동 재시작 설정
3.5 Health Check 및 모니터링
배포 후 애플리케이션이 정상적으로 시작되었는지 확인합니다
# 컨테이너 시작 대기 (애플리케이션 시작 확인)
echo "컨테이너 시작 대기 중..."
sleep 15
# 컨테이너 상태 확인
docker ps | grep travodo-app || echo "컨테이너가 실행 중이 아닙니다"
# 컨테이너 로그 확인 (디버깅용)
echo "=== 컨테이너 로그 (최근 50줄) ==="
docker logs --tail 50 travodo-app || echo "로그를 가져올 수 없습니다"
# Health Check 확인 (최대 2분 대기)
echo "=== Health Check 시작 ==="
for i in {1..60}; do
if curl -f http://localhost:8080/api/health > /dev/null 2>&1; then
echo "애플리케이션이 정상적으로 시작되었습니다"
break
fi
if [ $i -eq 60 ]; then
echo "애플리케이션 시작 실패 (2분 초과)"
echo "=== 최근 에러 로그 ==="
docker logs --tail 100 travodo-app | grep -i error || docker logs --tail 100 travodo-app
exit 1
fi
echo "애플리케이션 시작 대기 중... ($i/60)"
sleep 2
done
Health Check를 하는 이유:
- 애플리케이션 시작 실패 시 확인
- 배포 실패 시 자동으로 워크플로우 실패 처리
- 에러 로그 자동 출력으로 디버깅이 편함
3.6 리소스 정리
배포 완료 후 불필요한 Docker 이미지를 정리하여 디스크 공간을 확보합니다
# 사용하지 않는 이미지 정리
docker image prune -af
# Nginx 설정 테스트 및 리로드 (선택사항)
if command -v nginx &> /dev/null; then
sudo nginx -t && sudo systemctl reload nginx || echo "Nginx 리로드 실패"
fi
GitHub Secrets 설정
다음과 같은 Secrets을 GitHub 저장소에 설정해야 합니다
AWS 관련
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_REGION
EC2 관련
EC2_HOSTEC2_USERNAMEEC2_SSH_KEY
애플리케이션 설정
SPRING_PROFILES_ACTIVEDB_HOST,DB_PORT,DB_NAME,DB_USERNAME,DB_PASSWORDJWT_SECRETMAIL_USERNAME,MAIL_PASSWORDFRONTEND_URLKAKAO_REST_API_KEYAWS_S3_BASE_URL,S3_REGION,S3_BUCKETNAME
Secrets 설정 방법
- GitHub 저장소 -> Settings -> Secrets and variables -> Actions
- "New repository secret" 클릭
- Name과 Value 입력 후 저장
도메인 설정 및 HTTPS 구성 (DuckDNS + Nginx)
프로덕션 환경에서는 IP 주소 대신 도메인을 사용하고 HTTPS를 적용해야 합니다. 무료 도메인 서비스인 DuckDNS와 Let's Encrypt를 사용한 SSL 인증서 발급, 그리고 Nginx 리버스 프록시 설정을 진행했습니다.
1. DuckDNS 도메인 발급 및 설정
1.1 DuckDNS 계정 생성 및 도메인 발급
- DuckDNS 웹사이트에 접속
- OAuth를 통한 로그인 (Google, GitHub 등)
- 원하는 서브도메인 입력 (예:
travodo) - 도메인 자동 생성:
travodo.duckdns.org
2. Nginx 설치 및 설정
2.1 Nginx 설치
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
2.2 Nginx 리버스 프록시 설정
Spring Boot 애플리케이션(8080 포트)을 443 포트(HTTPS)로 프록시하기 위한 설정입니다
# /etc/nginx/sites-available/travodo
server {
listen 80;
server_name travodo.duckdns.org;
# HTTP에서 HTTPS로 리다이렉트
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name travodo.duckdns.org;
# SSL 인증서 경로 (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/travodo.duckdns.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/travodo.duckdns.org/privkey.pem;
# SSL 보안 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 보안 헤더
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 클라이언트 최대 요청 크기 (이미지 업로드 고려)
client_max_body_size 500M;
# 프록시 설정
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
# 타임아웃 설정
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket 지원 (필요한 경우)
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
}
}
설정 활성화
sudo ln -s /etc/nginx/sites-available/travodo /etc/nginx/sites-enabled/
sudo nginx -t # 설정 검증
sudo systemctl reload nginx
3. Let's Encrypt SSL 인증서 발급
3.1 Certbot 설치
sudo apt install certbot python3-certbot-nginx -y
3.2 SSL 인증서 발급
sudo certbot --nginx -d travodo.duckdns.org
인증서 발급 시
- 이메일 주소 입력 (만료 알림용)
- 약관 동의
- HTTP -> HTTPS 리다이렉트 선택 (Y)
Certbot이 자동으로
- 인증서 발급
- Nginx 설정 업데이트
- 자동 갱신 설정
3.3 인증서 자동 갱신 확인
Let's Encrypt 인증서는 90일마다 갱신이 필요합니다. Certbot이 설치되면 자동 갱신 cron job이 설정됩니다
# 갱신 테스트
sudo certbot renew --dry-run
# 갱신 상태 확인
sudo systemctl status certbot.timer
4. 방화벽 설정
# HTTP, HTTPS 포트 허용
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 22/tcp # SSH
sudo ufw enable
5. CI/CD 파이프라인에 Nginx 설정 반영
배포 후 Nginx 설정 테스트 및 리로드가 포함되어 있습니다
# Nginx 설정 테스트 및 리로드
if command -v nginx &> /dev/null; then
sudo nginx -t && sudo systemctl reload nginx || echo "Nginx 리로드 실패"
fi
6. Spring Boot 애플리케이션 설정
Swagger 설정에서 운영 서버 URL을 HTTPS 도메인으로 설정
Server prodServer = new Server()
.url("https://travodo.duckdns.org")
.description("운영 서버");'Spring' 카테고리의 다른 글
| Rest API & Spring JPA (0) | 2025.11.03 |
|---|