Develop

초보 백엔드가 무중단 배포와 CI/CD를 직접 구축하면서 만난 에러들 본문

백엔드/게시판 만들기

초보 백엔드가 무중단 배포와 CI/CD를 직접 구축하면서 만난 에러들

230801 2025. 12. 4. 20:15

안녕하세요  .ᐟ 

팀 프로젝트에서 무중단 배포 도입하다가 health check 부분에서 에러가 난다고해서

확인해볼 겸 ..공부해볼 겸.. 개인 프로젝트에 도입해보기로 했습니다.

 

이 글은...

Spring Boot 로 CRUD API 까지 만들줄 알지만,

CI / CD, 무중단 배포, Dockerfile, docker-compose, EC2 가 아직 감이 안오는 분이 읽으시면 좋을 것 같습니다.


CI / CD

CI/CD는 “코드 → 빌드 → 테스트 → 이미지 → 서버 배포” 과정의 전체 자동화 파이프라인 입니다.

  • CI(Continuous Integration)
    • 코드가 변경될 때마다 자동으로 빌드, 테스트, 정적 분석 등을 돌려서 “메인 브랜치에 합쳐도 되는 상태인지”를 계속 확인하는 것
    • GitHub Actions에서 코드 체크아웃 → 도커 이미지 빌드 → GHCR에 push 하는 부분까지가 CI에 해당
  • CD(Continuous Delivery/Deployment)
    • CI를 통과한 결과물을 스테이징/운영 환경에 자동으로 배포하도록 이어 붙인 것
    • SSH로 EC2 접속 → docker-compose down && docker-compose up -d → /actuator/health 체크까지가 CD

 

왜 CI / CD 가 필요한가요?

  • 수동으로 배포할때는 로컬에서 ./gradlew bootJar -> EC2 접속해서 JAR 복사 -> java -jar 로 직접 실행해야 되는데,
  • CI / CD 를 구축해놓으면 git 에서 push 한번만 하면 빌드 + 테스트 + 도커 이미지 생성 + 서버 배포 까지 자동으로 됩니다.
  • 개발자는 PR 코드리뷰랑 배포결과 확인만 하면 됩니다.

 


 

무중단 배포

무중단 배포는 서비스의 중단 없이 새로운 버전의 소프트웨어를 배포하는 것 입니다.

 

이번 프로젝트에서는 단일 EC2 + Docker Compose 기반으로 다운타임을 줄이는 연습을 했고, Nginx + Blue/Green 은 향후 진행해보려고 합니다.

 

  • Blue‑Green 배포
    • Blue(현재 운영)와 Green(새 버전) 두 개의 환경을 동시에 띄워 두고​ 새 버전(Green)이 정상임이 확인되면 트래픽만 Green으로 스위칭하는 방식으로, 문제가 생기면 다시 Blue로 빠르게 롤백할 수 있어 안정성이 높습니다.

 

  • 무중단 배포의 대표적인 3가지 방식 (Rolling, Blue/Green, Canary)
더보기
  • Rolling
    • 여러서버가 있을 때 새 버전을 점진적으로 교체하는 방식
      • 요청을 중단, 새로운 버전을 배포, 중단된 요청을 재개하면서 다시 요청을 처리
      • 이때 v2와 v1 이 공존하는 시기가 발생하게되고, 호환성 문제가 야기될 수 있음
    • 가용 자원이 제한적일 경우 혹은 큰 변화가 없거나 이미 충분히 테스트했을 경우 사용함
  • Blue/Green
    • 신규 서버를 추가로 배포하는 방식
      • 새로운 Green 환경으로 App을 배포하고, 기존의 Blue 환경으로 보내던 트래픽을 Green 환경으로 전환하는 방식
    • 시스템 자원이 충분한 경우 혹은 빠르고 안전한 롤백이 필요한 경우 사용함
  • Canary
    • 새 버전에 점진적으로 트래픽을 전환하는 방식
      • 일부의 사용자만 새로운 버전의 서버로 트래픽을 전환하고, 사용자들에게 피드백을 받아 서버가 안정적인지 판단을하고 점진적으로 트래픽을 증가시켜 전환함
    • 금융권처럼 안정성이 중요한 시스템인 경우 혹은 새로운 기능이나 실험적 기능을 도입하는 경우 사용함

 

일반 배포랑 무중단 배포랑 뭐가 다른가요?

  • 일반 배포
    • 서버 프로세스를 끄고 새 버전을 올리는 방식
    • 잠깐이라도 502나 Connection refused 가 뜰 수 있음
  • 무중단 배포
    • 새 버전을 옆에 먼저 띄워두고, 준비가 끝났을 때 트래픽만 갈아타는 방식
    • 사용자는 배포가 진행된지도 모름

 

상태(state)와 무중단 배포의 관계

  • 무중단 배포를 설계할때는 어디에 상태(state)가 저장되는지 중요합니다.
  • 이 프로젝트에서는 앱 컨테이너는 무상태(stateless) 이고, 상태는 DB 와 파일 스토리지(volume)에만 남도록 구성해서 컨테이너 교체 시 영향을 최소화 하려고 했습니다.

 


 

2.  작업 흐름

한 대의 EC2 안에서 도커 컨테이너를 이용해 배포 다운타임을 최대한 줄이는 연습을 했습니다.

 

  • 기존 상태
    • EC2 위에 Spring Boot + MySQL, GitHub Actions에서 docker run 으로 직접 컨테이너 띄우는 구조
    • develop 푸시 → 이미지 빌드/푸시 → EC2에 SSH → 기존 컨테이너 stop/remove → 새 컨테이너 run
  • 문제 상황
    • dial tcp ...:22: i/o timeout
      • EC2 보안 그룹에서 22 포트가 특정 IP만 허용되어 GitHub Actions IP가 차단된 문제
    • CJCommunicationsException: Communications link failure
      • 컨테이너에서 호스트 MySQL에 접속이 안 되고, JDBC URL 과 Spring 환경변수(JDBC_DATABASE_* vs SPRING_DATASOURCE_*)가 어긋나 있던 문제
    • 이후 DB 권한(root 비번 초기화, board_user@'%' 생성), bind-address=0.0.0.0 수정까지 연쇄 트러블슈팅..
  • 구조 개선 결정
    • “호스트 MySQL + 개별 docker run” 구조는 디버깅 포인트가 너무 많아서, 앱과 DB를 모두 Docker Compose로 관리하는 스테이징 환경으로 재구성하기로 결정

 


 

3. Dockerfile vs Docker Compose 개념과 역할

 

Dockerfile

Dockerfile은 이미지를 빌드하기 위한 스크립트로, 기반 이미지, JDK, 애플리케이션 JAR, 환경변수, ENTRYPOINT 등을 정의합니다.​

 

  • 프로젝트 적용
    • gradle 이미지로 빌드 스테이지에서 bootJar 실행
    • runtime 스테이지에서 JRE 기반 경량 이미지 사용
    • ENTRYPOINT ["java", "-jar", "app.jar"] 형태로 Spring Boot 실행
  • 효과
    • 어떤 환경에서든 동일한 이미지 생성
    • GitHub Actions에서 빌드/푸시만 하면 EC2에서 docker pull 로 바로 가져다 쓸 수 있음

 

Dockerfile 이 없으면

  • JDK 버전, OS 환경, 설치된 패키지 등등 서버마다 환경이 달라져서 "로컬에선 됐는데.. 서버에선 안되네" 상황이 벌어짐
  • 서버를 새로 만들때마다 같은 환경을 다시 세팅하는데 시간이 많이 걸림

Dockerfile 이 있으면

  • 해당 서비스가 돌아가기 위해 필요한 JDK, OS, 빌드방법, 실행방법 등이 스크립트 형태로 고정됨
  • 새로운 서버를 만들어도 docker pull + docker run 만 하면 동일환경을 세팅할 수 있음

 


 

docker-compose.yml

Docker Compose 파일은 여러 컨테이너(서비스)의 실행 구성을 정의하는 YAML 입니다.

 

  • 주요 요소
    • services: app, mysql 같은 서비스 정의
    • ports: 호스트–컨테이너 포트 매핑
    • environment: DB URL, 계정, Spring 프로필 같은 환경변수
    • volumes: 데이터나 업로드 파일을 호스트에 영속화
    • networks: 서비스 간 통신 네트워크 정의
  • 효과
    • docker-compose up -d 한 번으로 DB + 앱 + 네트워크 + 볼륨까지 한 번에 구성
    • 앱은 mysql:3306 이라는 서비스 이름으로 DB에 붙으니, IP 변화에 영향을 받지 않습니다.

 

docker-compose 가 없으면

  • app 컨테이너, DB 컨테이너를 각각 docker run 으로 실행해야 함
  • 매번 포트, 환경변수, 볼륨 옵션을 직접 써야하기 때문에 오타나 누락 문제가 발생함

docker-compose 가 있으면

  • 프로젝트의 컨테이너 구성을 한 yaml 파일에 선언할 수 있음
  • docker-compose up -d 명령어로 동시에 같은 네트워크에 올릴 수 있음

 


 

4. EC2의 역할

  • EC2 인스턴스는 컨테이너를 실행하는 서버 + Nginx/로깅 같은 인프라 컴포넌트가 올라가는 호스트 로 사용하고, 애플리케이션과 DB 는 컨테이너 안에서 돌게 책임을 분리했습니다.
    • 이번 구성은 학습 + 스테이징 용이라 DB도 도커 컨테이너로 올렸고, 운영환경이라면 DB는 RDS 같은 매니지드 서비스를 두고 애플리케이션간 컨테이너만 교체하는 구조를 고려해야 할 것 같습니다.
  • 나중에 서버를 변경해도(ex.. 새로운 EC2) Docker/Compose 설치 + docker-compose.yml 을 가져오기만 하면 복구가 가능합니다. 그리고 EKS, ECS 같은 오케스트레이션으로 옮길 때도 컨테이너 단위 사고방식을 유지할 수 있다고 합니다.
  • 프로젝트 적용
    • Docker / Docker Compose 설치 및 유지보수
    • GitHub Actions 에서 SSH 접속을 허용하는 bastion 역할 (22 포트, 보안 그룹 설정)
    • /home/ubuntu/uploads, mysql-data 같은 볼륨의 실제 스토리지 위치 제공
  • Blue‑Green 전략으로 확장 시 (추후 계획..)
    • app-blue, app-green 두 컨테이너를 다른 포트로 올리고, Nginx 로드밸런싱 설정으로 트래픽을 전환

 


 

 

5. GitHub Actions + Docker Compose 기반 배포 파이프라인

GitHub Actions

  • GitHub 안에 들어있는 자동화 서버
  • 코드가 푸시되면 정해둔 YAML(workflow) 파일 대로 빌드, 테스트, 배포 명령을 자동으로 실행해줍니다.
  • 사용 방법
    • 프로젝트 내에. github/workflows 디렉터리를 만들고, deploy-staging.yml, deploy-production.yml 같은 파일을 만들어서 설정해놓으면, 해당 워크플로우대로 CI / CD 를 진행하게 됩니다.
  • 역할
    • 언제 동작할지: on: push, on: pull_request 등 트리거 정의
    • 어떤 환경에서 돌릴지: runs-on: ubuntu-latest 같은 러너 설정
    • 무엇을 할지: steps: 안에 uses:(액션 재사용) + run:(직접 쉘 명령)으로 단계 정의
  • Secrets (민감 정보)
    • DB 비밀번호, SSH 키, 토큰 같은 것은 코드에 직접 쓰지 않고 Secret 으로 관리할 수 있습니다.
    • Secrets 는 Actions 로그에 값이 그대로 찍히지 않도록 자동 마스킹 처리 됩니다.
    •  방법
      • GitHub 레포지토리 -> Settings -> Security -> Secrets and variables -> Actions
      • New repository secret 클릭
      • name / secret 형태로 입력
        • ex) STAGING_HOST : EC2 퍼블릭 IP주소

Secret 등록

 

 

 

  • GitHub에서 셋팅한 Secret 을 YAML 에서 쓰는 방법
    • secrets.* 로 접근해서 변수 사용
with:
  host: ${{ secrets.STAGING_HOST }}
  username: ${{ secrets.STAGING_USER }}
  key: ${{ secrets.STAGING_SSH_KEY }}

 


배포 파이프라인

  • 기존 방식
    • develop 브랜치에 push → 이미지 빌드 & GHCR 푸시 → SSH로 EC2 접속 → docker run 으로 컨테이너 직접 띄우고 health 체크
  • 개선 후 방식
    • 동일하게 이미지까지 빌드/푸시한 뒤, 마지막 단계만 이렇게 변경
    • 의미
      • 워크플로우는 “이미지 버전 관리”에 집중
      • 실제 배포/롤백/컨테이너 라이프사이클 관리는 전부 docker-compose 가 담당​
      • curl /actuator/health 로 최종 건강 상태 검증
        • health check는 Spring Actuator /actuator/health 기준으로만 보고 있고, DB, 디스크, 파일 업로드 경로까지 통과해야 status: "UP" 이라서, 배포 성공 여부를 비교적 명확하게 판단할 수 있었습니다.
    • 현재 스테이징 배포는 board-app 이미지의 latest 태그를 기준으로 하고 있고, 필요하다면 sha 태그를 조합해서 특정 커밋 버전으로 롤백할 수 있도록 메타데이터를 남겨두고 있습니다.
script: |
      cd ~
      echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      docker pull ghcr.io/0802222/board_r/board-staging:latest

      docker-compose down
      docker-compose up -d

      sleep 30
      curl -f http://localhost:8080/actuator/health || exit 1

      docker image prune -af

 

 


 

6. 트러블슈팅

 

  • SSH 타임아웃
    • 증상 : dial tcp xxx:22: i/o timeout
    • 원인 : EC2 보안 그룹에서 22 포트가 내 IP만 허용, GitHub Actions IP 차단
    • 해결 : 보안 그룹 인바운드 규칙에 22 포트를 0.0.0.0/0 으로 허용
    • 느낀점 : CI / CD 에서 쓰는 서버는 GitHub Actions 같은 외부 IP 도 들어와야 한다는걸 알게 됨
  • DB 통신 실패
    • 증상 : CJCommunicationsException: Communications link failure
    • 원인 : 컨테이너에서 호스트 MySQL 로 가는 네트워크/권한 문제
      Spring 설정이 JDBC_DATABASE_URL을 보는데 환경변수는 SPRING_DATASOURCE_URL 로만 세팅되어 있던 문제
    • 해결 :MySQL bind-address 를 0.0.0.0 으로 변경, board_user@'%' 계정 생성
      Spring application.properties 와 GitHub Actions/compose 환경변수 이름 통일(JDBC_DATABASE_*)
    • 느낀점 : 내 application.yml 과 deploy-staging.yml과 docker-compose.yml 의 환경변수가 동일해야 한다는점을 배웠음
  • FileStorage 경로 문제
    • 증상 : FileStorageException: 파일 저장 디렉토리를 생성할 수 없습니다
    • 원인 : /app/uploads 볼륨 마운트와 호스트 디렉터리 권한 불일치
    • 해결 : ./uploads:/app/uploads 볼륨 설정 + /home/ubuntu/uploads 디렉터리 생성/권한 777 부여
    • 느낀점 : 컨테이너안 경로만 신경쓰면 될 줄 알았는데, 호스트 볼륨 경로와 권한까지 줘야된다는걸 알게됨..
  • docker-compose 포트 충돌
    • 증상 : failed to bind host port 0.0.0.0:3306: address already in use
    • 원인 : 호스트에 기존 MySQL 데몬가 3306 사용 중
    • 해결 : 도커 mysql 포트를 3307:3306 으로 변경
    • 느낀점 : 로컬 MySQL, 도커 MySQL, RDS 등 여러 DB가 한 서버에 있을 수 있기때문에, 뭐가 어떤 포트를 쓰는지 알아야됨

 


 

7. 개인 프로젝트 배포 전략

실무에서 많이 쓰는 브랜치 전략인 main-develop-feature 패턴을 적용해봤습니다.

  • 브랜치 전략
    • main: 운영(prod), 언제든 배포 가능한 상태
    • develop: 통합/스테이징, develop 푸시 시 EC2 스테이징 자동 배포
    • feature/*: 실제 개발 브랜치
  • 배포 플로우
    • feature/* → develop PR 머지 → 스테이징 자동 배포
    • 스테이징에서 검증 후 → develop → main PR 머지 → 프로덕션 배포 (deploy-production.yml 활용)
  • 무중단/Blue‑Green 확장 아이디어 (추후)
    • 단일 app 컨테이너 대신 app-blue, app-green 두 서비스를 docker-compose 로 정의하고 포트만 다르게 할당
    • Nginx upstream 을 8080↔8081 사이에서 스위칭하면 완전한 Blue‑Green 무중단 배포로 발전 가능