Spring

GitHub Actions로 스프링 CI/CD 구축하기

개발자임정혁 2025. 12. 28. 03:50

CI/CD 파이프라인 구조

CI/CD 파이프라인은 크게 두 단계로 구성했습니다

  1. Build Job: 코드 빌드 및 검증
  2. 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"

배포 프로세스

  1. ECR에서 최신 이미지 Pull
  2. 기존 컨테이너 안전하게 중지 및 제거 (|| true 로 실패 시에도 계속 진행)
  3. 환경 변수를 통해 애플리케이션 설정 주입
  4. --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_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_REGION

EC2 관련

  • EC2_HOST
  • EC2_USERNAME
  • EC2_SSH_KEY

애플리케이션 설정

  • SPRING_PROFILES_ACTIVE
  • DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD
  • JWT_SECRET
  • MAIL_USERNAME, MAIL_PASSWORD
  • FRONTEND_URL
  • KAKAO_REST_API_KEY
  • AWS_S3_BASE_URL, S3_REGION, S3_BUCKETNAME

Secrets 설정 방법

  1. GitHub 저장소 -> Settings -> Secrets and variables -> Actions
  2. "New repository secret" 클릭
  3. Name과 Value 입력 후 저장

도메인 설정 및 HTTPS 구성 (DuckDNS + Nginx)

프로덕션 환경에서는 IP 주소 대신 도메인을 사용하고 HTTPS를 적용해야 합니다. 무료 도메인 서비스인 DuckDNS와 Let's Encrypt를 사용한 SSL 인증서 발급, 그리고 Nginx 리버스 프록시 설정을 진행했습니다.

1. DuckDNS 도메인 발급 및 설정

1.1 DuckDNS 계정 생성 및 도메인 발급

  1. DuckDNS 웹사이트에 접속
  2. OAuth를 통한 로그인 (Google, GitHub 등)
  3. 원하는 서브도메인 입력 (예: travodo)
  4. 도메인 자동 생성: 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