[CL] 3단계: CD(지속적 배포) 파이프라인 구축 - 100-hours-a-week/6-nemo-wiki GitHub Wiki

1. 개요

해당 문서는 수작업 기반 Big Bang 배포 경험을 바탕으로 CD(지속적 배포) 자동화 파이프라인을 구축하고 설계하는 것을 목표로 한다.

1.1. 실습 및 평가자 관점

  • 본 단계에서는 CI를 넘어, 운영 환경에 자동으로 릴리즈되는 CD 구조의 안정성과 설계 의도를 평가한다.
  • 배포 자동화 과정에서 발생할 수 있는 실패, 오류 전파, 무중단 전환 등의 리스크를 어떻게 제어하고 있는지에 중점을 둔다.
  • 특히, 배포 실패 시의 대응 전략(자동 롤백, 수동 복구) 및 배포 전후 모니터링/검증 절차의 유무가 핵심 평가요소이다.

1.2 실무자 관점

  • CI 이후의 코드를 실제 사용자에게 빠르게 전달할 수 있는 구조는 제품 출시 주기를 획기적으로 단축시킨다.
  • 팀은 “왜 지금 CD가 필요한가?”라는 질문에 대한 실무적 관점을 제시해야 하고, 서비스 특성에 맞는 배포 전략과 그 전략을 선택한 배경을 기술해야 한다.
  • 또한, 배포 오류 발생 시 빠르게 대응 가능한 롤백 구조나 검증 자동화 전략이 함께 제시되어야 한다.

1.3 과제 목표 및 구성

이번 과제는 다음 세 가지 핵심 작업으로 구성된다:

  1. CD 적용 범위 및 방식 결정
    • 어떤 환경(스테이징, 운영)에 CD를 적용할지와 그 방식(자동/수동, 승인 기반 등)을 명시한다.
    • 또한 선택한 배포 전략과 그 배경을 서비스 구조와 함께 설명한다.
  2. CD 파이프라인 설계
    • CI 산출물(build 결과물)을 기준으로 자동 배포 → 검증 → 전환이 이뤄지는 전체 흐름을 설계한다.
    • 실패 시 롤백 절차, 상태 체크, 알림, 모니터링 로직을 포함한다.
  3. 파이프라인 구성 명세
    • GitHub Actions 워크플로우 구조, start.sh, validate.sh, 비밀값 관리(Secrets), 서버 내 실행 로직을 명세화한다.
    • 실무에서 적용 가능한 수준의 상세한 구성 설계로 문서화한다.

1.4 제풀 항목

  • CD 파이프라인 흐름도
    • Push → 배포 실행 → 성태 검증 → 전환/롤백 흐름을 시각화한 다이어그램
  • CD 설계 설명서
    • 도입 이유, 전략 선택 배경, 구성 요소 간 흐름 및 기대 효과를 포함한 서술형 문서
  • 설정 및 스크립트 명세
    • cd.yml, 배포 스크립트, 검증 스크립트 등 실행 흐름을 담은 주요 설정 파일 내용
  • 추가 자료
    • 로그, 배포 전후 성능 변화, 시나리오 기반 검증 결과

2. 도입 배경 및 현행 문제 정의

2.1. 현재 배포 구조 개요

1단계 실습에서는 GitHub 없이 .jar, .py, .env 등의 파일을 로컬에서 준비한 후,

서버로 직접 전송(scp)하고 SSH로 접속하여 수동으로 서비스를 실행하는 Big Bang 배포 방식을 사용했다.

모든 서비스(Next.js, Spring Boot, FastAPI, MySQL, Redis)가 하나의 VM(GCP Compute Engine)에

통합되어 있으며, 포트 기반으로 각각 구동되며, 배포자는 다음과 같은 절차를 거쳐 수동 배포를 수행해야 했다:

  1. 로컬에서 빌드 및 테스트 수행
  2. scp로 서버에 코드 복사
  3. SSH 접속 후 실행 명령어 수동 입력 (java -jar, npm start, uvicorn)
  4. 수동 로그 확인 (tail, curl 등)

2.2. 문제 인식: 수작업 배포 구조의 한계

Big Bang 배포 방식의 경우 학습적으로는 유의미했지만, 다음과 같은 실질적 한계 직면했다:

한계점 설명
반복 작업 증가 매번 scp → ssh → 명령 실행 반복 → 생산성 저하
휴먼 에러 가능성 커맨드 누락, 경로 오류, 파일 덮어쓰기 등 실수 위험
Git과 단절 GitHub에 의존하지 않기 때문에 히스토리 관리 및 롤백 불가
운영자 의존도 특정 인원만 배포 가능, 협업 및 유지보수 어려움
확장 불가 배포가 수동이기 때문에 서비스가 성장할수록 병목 가능성 발생

2.3. CD 자동화 도입의 필요성

위와 같은 문제들을 해결하기 위해 다음과 같은 관점에서 지속적 배포(CD) 구조의 필요성이 도출되었다:

  • GitHub 중심 개발 → 자동 배포로 연결되는 통합된 흐름 필요
  • Push 기반으로 서버 내부에서 자동으로 실행되는 구조 확보
  • 수동 배포에서 발생하는 실수 제거 및 일관성 유지
  • 배포 이력 추적, 실패 시 롤백 대응, 상태 검증 자동화 등의 확장 기반 마련

이를 바탕으로,

“이제는 코드가 GitHub에 올라가는 순간, 자동으로 서버에서 안전하게 반영되는 구조가 필요하다.”

라는 명확한 기술적 요구가 발생했다.

3. 배포 자동화 도구 선택 및 비교

3.1. 도구 선택 배경

2단계부터 GitHub을 본격적으로 도입함에 따라,

코드 기반의 자동화된 배포 구조로 전환할 필요성이 명확해졌다.

이전까지는 배포를 위해 다음과 같은 과정을 수동으로 반복해야 했다:

  • 로컬에서 빌드
  • scp로 서버 전송
  • ssh로 접속
  • 명령어 수동 실행

이러한 구조는 반복성과 휴먼 에러 발생 가능성이 높기 때문에,

GitHub Push 이벤트 기반으로 서버에 자동 접속 후 배포가 이뤄지는 구조가 필요해졌다.

3.2. 핵심 후보 도구 비교

가장 현실적인 후보는 GitHub Actions 기반의 두 가지 방식이었다:

  • GitHub Actions + SSH: GitHub 클라우드 러너가 서버에 SSH로 접속해 명령어 실행
  • GitHub Actions + Self-hosted Runner: 서버 내부에 러너를 설치해, Push 이벤트 발생 시 직접 배포
항목 GitHub Actions + SSH GitHub Actions + Self-hosted Runner
실행 위치 GitHub 클라우드 → 서버에 SSH 접속 서버 내부 러너에서 직접 실행
파일 전송 scp 직접 수행 필요 checkout으로 코드 복제 가능
보안 SSH 키/포트 관리 필요 외부 접속 없이 실행 가능 (내부 보안 강화)
설정 난이도 비교적 간단, 익숙한 구조 러너 설치, 등록, 서비스 실행 필요
확장성 명령어 조작 자유로움 템플릿화 용이, 러너 라벨링으로 컨트롤 가능
디버깅/운영 GitHub와 서버 로그 분리 서버 내 로그로 추적 쉬움
운영 환경 통제 SSH 명령어로 제어 (start.sh 등) 서버 내부 제어가 직관적임

3.3. 기타 CD 도구 간단 비교

도구 장점 단점 서비스 적합도
Jenkins 자유도, 확장성, 플러그인 풍부 별도 서버 설치/관리 필요, 복잡 단일 서버에는 과도함
AWS CodeDeploy EC2/ASG 통합, 롤백 기능 우수 AWS 전용, GCP와 비호환 현재 GCP 사용 중
GitLab CI/CD GitLab과 통합 우수 GitHub 사용자에 부적합 도구 변경 부담
ArgoCD GitOps + Kubernetes 완벽 연동 K8s 필요, 러닝커브 있음 Kubernetes 미사용 중

3.4. 결론

결론적으로 현재 단계에서는 다음과 같은 이유로 GitHub Actions + SSH 방식을 채택했다.

  • GitHub에 Push만 하면 자동으로 원격 서버에 접속해 배포 수행 가능
  • 기존 수작업 배포 방식에서 큰 구조 변경 없이 자동화 도입 가능
  • 명령어 기반으로 서비스별 배포 흐름 제어가 유연함
  • SSH key 기반 배포로 이후 Rollback, 헬스체크 등 확장 설계도 용이함
  • MVP 단계로, self-hosted runner를 운영할 만큼의 복잡한 보안/비용/네트워크 이슈가 없으며, GitHub 클라우드 러너만으로도 충분히 안정적인 운영이 가능함

GitHub Actions + SSH 방식은

현재 우리 구조(GCP 단일 서버, GitHub 중심 개발, 기존 수동 배포 경험)를 기반으로,

가장 현실적이고 적용이 용이한 CD 도구였다.

4. CD 파이프라인 구성 및 실행 흐름 설계

4.1. CD 적용 범위 및 방식 결정

지속적 배포(Continuous Delivery / Deployment)를 설계하기 위해 가장 먼저 고려해야 할 점은

어떤 브랜치에서, 어떤 방식으로 배포가 이루어질 것인가다.

현재 프로젝트는 경량화 된 Git Flow 전략을 도입하여 다음과 같은 브랜치 체계를 운용 중이다:

main / develop / hotfix/* / feature/* / bugfix/*

각 브랜치는 목적과 배포 대상이 다르며, 실제 배포가 필요한 maindevelop 브랜치에

서로 다른 CD 적용 범위 및 자동화 수준을 분리하여 설계하였다.

브랜치 목적 CD 방식 배포 서버
develop 기능 통합 및 QA 테스트 자동 배포 (Continuous Deployment) 테스트 서버
main 운영 배포 수동 승인 기반 배포 (Continuous Delivery) 운영 서버
  1. develop 브랜치 (Continuous Deployment)
    • develop 브랜치는 기능 통합과 QA 테스트를 위한 공간으로 이곳에서 기능 검증을 수행한다.
    • 병합된 코드는 자동으로 테스트 서버에 배포되기 때문에 빠른 개발 사이클을 위해 자동화가 필수이다.
    • 따라서 별도의 승인 없이 푸시만으로 배포가 이루어지는 Continuous Deployment 전략을 적용했다.
  2. main 브랜치 (Continuous Delivery)
    • 반면 main 브랜치는 실제 사용자에게 서비스되는 운영 환경이므로 배포 전 인적 검토와 승인 절차가 필요하다.
    • 이러한 특성상 안정성이 최우선이기에, 수동 검토 및 승인을 거쳐 배포하도록 Continuous Delivery 전략을 적용했다.

따라서 본 프로젝트에서 develop 에는 빠른 피드백을 위한 자동 배포(Continuous Deployment)를, main 에는 운영 안정성을 고려한 수동 승인 배포(Continuous Delivery)를 적용함으로써, 속도와 안정성의 균형을 맞춘 실용적인 CD 전략을 구성했다.

4.2. CD 파이프라인 설계

본 프로젝트는 Github Actions를 활용하여 CD 파이프라인을 구성하였다.

브랜치별로 배포 방식이 달라지므로, GitHub Actions 워크플로우 안에서

브랜치를 기준으로 분기하여 서버에 다른 배포 스크립트를 실행하는 구조로 설계하였다.

Final_Archieticture-CD_파이프라인 drawio_(3)

  1. 개발 완료 후 PR 생성

    개발자는 feature/* 브랜치에서 작업 후, develop 브랜치에 PR을 생성한다.

    이벤트 워크플로우 설명
    pull_request CI - PR Lint & Test 코드 스타일 및 테스트 자동 검사 수행
  2. develop 브랜치에 merge 또는 push 발생

    develop 브랜치에 PR이 머지되거나 직접 push되면, 아래 순서로 워크플로우가 실행된다.

    이벤트 워크플로우 설명
    push to develop CI - Backend Build & Upload to GCS bootJar 실행 후 app.jar 생성 → GCS 업로드
    workflow-run (CI 워크플로우 종료 후 실행) CD - Deploy to Staging (Develop) CI 성공 시 자동 트리거되어 Staging 서버에 배포 (app.jar, .sh 다운로드 및 실행)

    Staging 환경에 자동 배포되는 구조 (Continuous Deployment)

    workflow_run을 사용하는 이유:

    → CI가 완료되기 전에 CD가 먼저 실행되면, GCS에 app.jar가 아직 업로드되지 않아 배포가 실패할 수 있기 때문

    → 따라서 CI - Build & Upload to GCS가 완료된 이후에만 CD - Deploy to Staging이 실행되도록 workflow_run 트리거를 사용함

    → 이로써 빌드 산출물이 누락되지 않고, 안정적으로 Staging 서버에 자동 배포되도록 설계하였다.

  3. 운영 배포 준비: PR → main

    develop 환경에서 통합 및 기능 테스트를 완료한 후, main 브랜치로 PR을 생성하고 머지한다.

    이벤트 워크플로우 설명
    push to main CI - Backend Build & Upload to GCS main 기준 app.jar, .sh 파일 GCS 업로드
  4. 운영 서버 배포

    이벤트 워크플로우 설명
    push to main CD - Deploy to Production main에 push되면 운영 서버에 자동 배포. GCS에서 app.jar 및 스크립트 다운로드 후 실행됨

    Production 환경에 수동 승인 배포되는 구조 (Continuous Delivery) on: push + environment.approval을 사용하는 이유:

    → 운영 배포는 CI 성공 후 자동 배포되기보단, 사람이 승인한 후 실행되어야 하기 때문

    → GitHub Actions의 environment.production 설정을 통해 push to main 발생 시에도 운영자 승인 없이는 실제 배포가 이뤄지지 않도록 설계함

    → 이로써 실수로 인한 자동 배포를 방지하고, 운영 안정성을 높였다.

4.3. 배포 신뢰성 확보 전략

지속적 배포(Continuous Delivery/Deployment)가 안정적으로 운영되기 위해서는

배포 이후 서비스 상태를 검증하고, 문제 발생 시 빠르게 복구할 수 있는 구조가 필요하다.

본 프로젝트는 이를 위해 다음과 같은 헬스 체크 절차롤백 전략을 함께 설계하였다.

  1. 헬스 체크 (Health Check)

    배포가 완료된 후, validate.sh 스크립트를 통해 애플리케이션의 상태를 자동 점검한다.

    해당 스크립트는 /health 엔드포인트를 호출하여 다음 세 가지 구성 요소를 확인한다:

    • Spring Boot 애플리케이션 기동 여부 (응답이 왔다는 것 자체가 사실상 배포 성공을 의미)
    • MySQL 연결 상태
    • Redis 연결 상태

    장상적인 응답이 다음과 같은 형식으로 반환되며, "status": "UP" 일 때만 배포가 성공한 것으로 간주

    {
      "spring": "OK",
      "mysql": "OK",
      "redis": "OK",
      "status": "UP"
    }
    
  2. 롤백 전략 (Rollback Strategy)

    본 프로젝트는 개발 초기에 배포 실패 시 빠르게 이전 안정 버전으로 복원할 수 있도록

    GCS 기반 수동 롤백 전략을 설계하였다.

    특히 직전 배포 버전(app-previous.jar)을 자동 보관하여 빠르고 명확한 복구가 가능하도록 구성하였다.

    GCS 업로드 구조는 다음과 같다:

    파일명 용도
    app.jar 현재 배포에 사용되는 최신 빌드 파일
    app-previous.jar 직전 배포 버전 (롤백 대상)
    20250421-1652-abcdef.jar 고유 커밋 기반 히스토리 보관용

    배포 실패 시 롤백 수행 절차는 다음과 같다:

    1. GCS에서 app-previous.jar 다운로드
    2. 이를 서버에 업로드하여 기존 app.jar를 덮어씀
    3. 기존 배포 스크립트 (start.sh)로 서비스 재기동

    해당 전략은 CI/CD 흐름을 크게 변경하지 않으면서도, 배포 실패 시 안정적으로 복구할 수 있는 최소한의 안전장치를 제공하며, 향후 자동화 및 모니터링 시스템과도 자연스럽게 연동될 수 있다.

5. 파이프라인 구성 명세

5.1. CD 파이프라인 구성 요약

이벤트 워크플로우 설명
CI 성공 시 CD - Deploy to Staging (develop) dev 서버에 자동 배포
환경 승인 (main) CD - Deploy to Production (main) 운영 서버에 수동 승인 배포 (stop.sh, start.sh, validate.sh 실행)

5.2 Github Actions 워크플로우 구성

  1. cd-staging.yml
    • 트리거: workflow_run (CI 성공 시)
    • 작업
      • GCS에서 파일 다운로드
      • dev 서버에 ssh 접속 → stop.sh, start.sh, validate.sh 실행
  2. cd-production.yml
    • 트리거: push to main + environment.approval (GitHub 환경 승인)
    • 작업
      • CS에서 파일 다운로드
      • dev 서버에 ssh 접속 → stop.sh, start.sh, validate.sh 실행

5.3. 배포 스크립트 구성

스크립트 설명
start.sh 포트 점유 확인 → Spring Boot 실행 (nohup java -jar app.jar
stop.sh 기존 8080 포트 프로세스 종료
validate.sh /healthz API 호출 → status: UP 여부 확인
rollback.sh app-previous.jarapp.jar로 복사 후 재시작

5.4. GCS 디렉토리 구조

  • 브랜치에 따라 TARGET_ENV=dev 또는 prod로 분기 업로드

  • GCS 구조는 배포 환경별로 명확한 격리 및 관리 가능

    gs://sample-app-artifact/
    ├── dev/
    │   └── backend/
    │       ├── app.jar
    │       ├── start.sh
    │       ├── stop.sh
    │       └── validate.sh
    ├── prod/
        └── backend/
            ├── app.jar
            ├── app-previous.jar
            ├── start.sh
            ├── stop.sh
            ├── validate.sh
            └── rollback.sh
    

5.5. 환경 변수 및 보안 설정 (GitHub Secrets)

환경변수 이름 용도 설명 적용 대상 브랜치 비고
GCP_KEY_JSON GCP 서비스 계정 인증을 위한 키 JSON 공통 google-github-actions/auth@v1에 사용
GCP_PROJECT_ID GCP 프로젝트 ID 공통 setup-gcloud에 사용
GCS_BUCKET GCS 아티팩트 업로드 대상 버킷 이름 공통 jar, sh 업로드 경로 지정에 사용
DEV_SSH_USER 개발 서버 SSH 접속용 유저명 develop 스테이징 배포용
DEV_SSH_HOST 개발 서버 IP or 도메인 develop 스테이징 배포용
SSH_USER 운영 서버 SSH 접속용 유저명 main 운영 배포용
SSH_HOST 운영 서버 IP or 도메인 main 운영 배포용
SSH_PRIVATE_KEY SSH 접속을 위한 개인 키 (PEM 형식) 공통 scp, ssh 전송에 사용

5.6. 스크립트

  1. GitHub Actions 워크플로우 파일 (.github/workflows/)

    1. cd-staging

      name: CD - Deploy to Staging (Develop)
      
      on:
        workflow_run:
          workflows: ["CI - Backend Build & Upload to GCS"]
          types:
            - completed
      
      jobs:
        deploy:
          if: ${{ github.event.workflow_run.conclusion == 'success' }}
          runs-on: ubuntu-22.04
          environment:
            name: staging
      
          steps:
            - name: Authenticate to GCP
              uses: google-github-actions/auth@v1
              with:
                credentials_json: ${{ secrets.GCP_KEY_JSON }}
      
            - name: Set up gcloud CLI
              uses: google-github-actions/setup-gcloud@v1
              with:
                project_id: ${{ secrets.GCP_PROJECT_ID }}
      
            - name: Set env
              run: |
                echo "TARGET_ENV=dev" >> $GITHUB_ENV
                echo "SSH_USER=${{ secrets.DEV_SSH_USER }}" >> $GITHUB_ENV
                echo "SSH_HOST=${{ secrets.DEV_SSH_HOST }}" >> $GITHUB_ENV
      
            - name: Download from GCS
              run: |
                mkdir -p scripts
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/$TARGET_ENV/backend/app.jar ./scripts/app.jar
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/$TARGET_ENV/backend/*.sh ./scripts/
      
            - name: Create SSH private key
              run: |
                echo "$SSH_PRIVATE_KEY" > private_key.pem
                chmod 600 private_key.pem
              env:
                SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      
            - name: Deploy to staging server
              run: |
                ssh -o StrictHostKeyChecking=no -i private_key.pem $SSH_USER@$SSH_HOST "mkdir -p ~/deploy/backend"
                scp -o StrictHostKeyChecking=no -i private_key.pem ./scripts/* $SSH_USER@$SSH_HOST:~/deploy/backend/
                ssh -o StrictHostKeyChecking=no -i private_key.pem $SSH_USER@$SSH_HOST "
                  cd ~/deploy/backend &&
                  bash stop.sh &&
                  bash start.sh &&
                  bash validate.sh
                "
      
    2. cd-prod

      name: CD - Deploy to Production (Manual)
      
      on:
        push:
          branches: [main]
      
      jobs:
        deploy:
          runs-on: ubuntu-22.04
          environment:
            name: production
      
          steps:
            - name: Authenticate to GCP
              uses: google-github-actions/auth@v1
              with:
                credentials_json: ${{ secrets.GCP_KEY_JSON }}
      
            - name: Set up gcloud CLI
              uses: google-github-actions/setup-gcloud@v1
              with:
                project_id: ${{ secrets.GCP_PROJECT_ID }}
      
            - name: Set env for main
              run: |
                echo "TARGET_ENV=prod" >> $GITHUB_ENV
                echo "SSH_USER=${{ secrets.PROD_SSH_USER }}" >> $GITHUB_ENV
                echo "SSH_HOST=${{ secrets.PROD_SSH_HOST }}" >> $GITHUB_ENV
      
            - name: Download files from GCS
              run: |
                mkdir -p scripts
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/$TARGET_ENV/backend/app.jar ./scripts/app.jar
                gsutil cp gs://${{ secrets.GCS_BUCKET }}/$TARGET_ENV/backend/*.sh ./scripts/
      
            - name: Create SSH private key
              run: |
                echo "$SSH_PRIVATE_KEY" > private_key.pem
                chmod 600 private_key.pem
              env:
                SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      
            - name: Deploy to production server
              run: |
                ssh -o StrictHostKeyChecking=no -i private_key.pem $SSH_USER@$SSH_HOST "mkdir -p ~/deploy/backend"
                scp -o StrictHostKeyChecking=no -i private_key.pem ./scripts/* $SSH_USER@$SSH_HOST:~/deploy/backend/
                ssh -o StrictHostKeyChecking=no -i private_key.pem $SSH_USER@$SSH_HOST "
                  set -e
                  cd ~/deploy/backend
                  bash stop.sh
                  bash start.sh
                  bash validate.sh
                "
      
  2. 배포 스크립트 파일

    1. stop.sh

      #!/bin/bash
      echo "[CD] Stopping Spring Boot..."
      
      PID=$(lsof -t -i:8080)
      
      if [ -n "$PID" ]; then
        kill -9 $PID
        echo "[CD] Process $PID stopped."
      else
        echo "[CD] No process running on port 8080."
      fi
      
    2. start.sh

      #!/bin/bash
      echo "[CD] Start Spring Boot Server..."
      
      # 실행 중인 프로세스가 있다면 종료 (예: port 8080)
      PID=$(lsof -t -i:8080)
      if [ -n "$PID" ]; then
        echo "[CD] Stopping existing process on port 8080..."
        kill -9 $PID
      fi
      
      # JAR 실행 (복사된 ~/deploy 경로 기준)
      JAR_PATH=$(ls -t ~/deploy/backend/*.jar | head -n 1)
      
      if [ -z "$JAR_PATH" ]; then
        echo "[CD] ❌ No JAR file found to execute."
        exit 1
      fi
      
      echo "[CD] Running $JAR_PATH"
      nohup java -jar "$JAR_PATH" > ~/deploy/spring.log 2>&1 &
      
      echo "[CD] Spring Boot started."
      
    3. validate.sh

      #!/bin/bash
      echo "[CD] Validating app..."
      URL="http://localhost:8080/healthz"
      
      for i in {1..10}; do
        RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" $URL)
        BODY=$(echo $RESPONSE | sed -e 's/HTTPSTATUS\:.*//g')
        STATUS=$(echo $RESPONSE | sed -e 's/.*HTTPSTATUS://')
        APP_STATUS=$(echo "$BODY" | grep -o '"status":"[^"]*"' | cut -d':' -f2 | tr -d '"')
      
        if [ "$STATUS" = "200" ] && [ "$APP_STATUS" = "UP" ]; then
          echo "[CD] ✅ Health check passed"
          exit 0
        fi
        sleep 3
      done
      
      echo "[CD] ❌ Health check failed: $BODY"
      exit 1
      
      
    4. rollback.sh

      #!/bin/bash
      echo "[CD] ⚠️ Rolling back to previous version..."
      
      APP_DIR=~/deploy/backend
      CURRENT_JAR="$APP_DIR/app.jar"
      BACKUP_JAR="$APP_DIR/app-previous.jar"
      
      # 1. 현재 실행 중인 앱 종료
      PID=$(lsof -t -i:8080)
      if [ -n "$PID" ]; then
        echo "[CD] Stopping running app on port 8080..."
        kill -9 $PID
      fi
      
      # 2. 백업된 JAR이 있는지 확인
      if [ ! -f "$BACKUP_JAR" ]; then
        echo "[CD] ❌ No backup file (app-previous.jar) found. Rollback failed."
        exit 1
      fi
      
      # 3. 현재 JAR 백업 (선택적 - 원래 실패한 상태라 무의미할 수도 있음)
      cp "$CURRENT_JAR" "$APP_DIR/app-failed.jar"
      
      # 4. 이전 JAR로 복원
      cp "$BACKUP_JAR" "$CURRENT_JAR"
      echo "[CD] ✅ Restored app.jar from backup."
      
      # 5. 재실행
      nohup java -jar "$CURRENT_JAR" > "$APP_DIR/spring.log" 2>&1 &
      
      echo "[CD] 🟢 Rollback complete. Server restarted with previous version."
      

6. 결론 및 고도화 방향 제안

본 문서에서는 기존 수작업 기반의 Big Bang 배포 구조를 개선하고, GitHub Actions와 GCS 기반의 CI/CD 파이프라인을 도입하여, 단일 서버 환경에서도 자동화된 배포 구조를 구축하였다.

이번 3단계 설계를 통해 다음과 같은 주요 성과를 얻었다:

  • GitHub와 배포 자동화를 연동하여 push → 서버 적용까지의 배포 흐름을 통합함
  • **환경별 분기 (develop/main)**에 따라 자동 배포 / 승인 기반 배포를 구분 적용함으로써 속도와 안정성의 균형을 확보함
  • GCS에 버전별 .jar 파일을 백업하고, 실패 시 빠르게 복원 가능한 롤백 구조를 마련
  • 배포 후 헬스 체크(Health Check) 를 통해 서비스 상태를 자동 검증하고, 실패 시 즉시 대응할 수 있도록 구성함

운영 배포에서는 environment.approval 기반 수동 승인을 도입하여, 실수로 인한 자동 배포를 방지하고 안정성을 확보하였다. 또한, 이전 버전 자동 보관 및 롤백 전략을 통해 운영 중 장애에도 빠르게 복구할 수 있는 구조를 마련하였다.

이제 다음 단계에서는 다음과 같은 고도화를 고려할 수 있다:

  • Docker 기반의 배포 구조로 전환하여, 환경별 실행 환경 불일치 해소 및 서비스 확장 기반 마련
  • GitHub Actions와 Discord Webhook 연동, 배포 로그 모니터링, 알림 도입 등 운영 자동화 체계 확대
  • Multi-tier 구조로 확장 시, Load Balancer 기반 블루그린 및 카나리 배포 전략 도입 가능성 고려

이번 단계에서는 단일 VM 기반의 배포 자동화를 성공적으로 설계·구현함으로써, MVP 서비스 수준에서 안정성과 신속성을 동시에 확보할 수 있었다.

향후 단계에서는 Docker 기반 구성, 컨테이너 오케스트레이션(Kubernetes), 고급 배포 전략(블루그린, 카나리), 모니터링 및 알림 연동 등을 통해 보다 확장성 있는 클라우드 네이티브 아키텍처로 발전시키고자 한다.