본문 바로가기
카테고리 없음

[SpringBoot] CI/CD 구축(1)

by KongJiHoon 2026. 1. 2.
 

✅ CI/CD 란?

CI(지속적인 통합)과 CD(지속적인 배포)는 애플리케이션 개발 팀이 코드 변경을 보다 자주, 안정적으로 제공하기 위한 방식 또는 운영원칙이다. CI/CD는 통합 및 제공을 자동화함으로써 소프트웨어 개발팀이 코드 품질과 보안을 보장하며, 비즈니스 요구사항을 충족하는 데에 집중하도록 한다.

 

1️⃣ CI(Continuous Integration)

👉 CI는 지속적인 통합이라는 뜻으로, 새로운 코드 변경이 정기적으로 빌드 및 테스트 되어 공유 레포지토리에 통합하는 것을 의미한다.

 

다수의 개발자가 형상관리 툴을 공유하는 사용하는 환경

  • 다수의 개발자가 한 팀으로 작업할 경우, 공유 레포지토리에 수많은 commit이 쌓이게 된다. 이런 상황에서 자동화된 빌드 및 테스트는 소스코드의 충돌을 방지하는 이점이 있다.

CI의 목표

  • 버그를 신속하게 찾아 해결하고, 소프트웨어의 품질을 개선하고, 새로운 업데이트의 검증 및 릴리즈의 시간을 단축시키는 것에 있다.

 

2️⃣ CD(Continuous Deployment)

👉 CD는 지속적인 배포라는 뜻으로, CI가 새로운 소스코드의 빌드, 테스트, 병합까지 의미한다면, CD는 개발자의 변경 사항이 레포지토리를 넘어 고객의 프로덕션 환경까지 릴리즈 되는 것을 의미한다. 소프트웨어가 언제든지 신뢰 가능한 수준의 버전을 유지할 수 있도록 support하는 것이 CD라고 할 수 있다.

 


 

✅ CI/CD를 도입한 이유

프로젝트는 기능이 늘어나면서 배포 과정도 자연스럽게 복잡해진다. 코드 변경이 잦아지고 운영 환경 설정 값(DB, Redis, JWT, OAuth)이 많아지면 수동 배포 방식은 실수 가능성이 커지고, 배포 속도도 느려지는 한계가 있다.

 

🔥 목표

main 브랜치에 push만 하면, GitHub Action에서 자동으로 빌드/이미지 푸시를 수행하고, EC2에서 docker compose로 최신 이미지를 pull하여 재기동까지 완료되도록 한다.

 

1️⃣ CI/CD 목표와 범위

main 브랜치에 push가 발생하면, 운영 서버(EC2)에 다오으로 최신 버전으로 배포되도록 한다.

 

구체적인 흐름

 

1. 빌드(Build)

  • GitHub Actions Runner에서 Spring Boot 프로젝트를 빌드하여 배포 가능한 결과물을 만든다.

2. 이미지 생성 및 푸시(Build & Push Image)

  • Dockerfile을 기반으로 Docker 이미지를 생성하고,
  • Docker Hub에 최신 이미지를 push한다.

3. 운영 서버 배포(Deploy)

  • GitHub Actions가 EC2에 SSH로 접속한다.
  • 서버의 docker compose가 최신 이미지를 pull하고 컨테이너를 재기동한다.

 

범위(Scope)

이번에 구축한 파이프라인은 “운영 배포 자동화”의 최소 단위를 우선 완성하는 데에 집중했다.
따라서 아래 항목까지를 이번 범위로 포함했다.

 

✅ 포함한 범위

  • GitHub Actions를 이용한 자동 실행(트리거: main push)
  • Gradle 빌드 실행(JDK 17 환경)
  • Docker 이미지 빌드 및 Docker Hub push
  • EC2 SSH 접속 후 docker compose 기반 배포 수행
    • docker compose pull
    • docker compose down
    • docker compose up -d
    • docker ps로 상태 확인

 

❓Docker란

  • 애플리케이션과 그 실행에 필요한 모든 환경(코드, 라이브러리, 시스템 도구 등)을 컨테이너라는 격리된 단위로 패키징하여, 어떤 환경에서든 동일하게 빠르고 안정적으로 실행되도록 돕는 오픈소스 소프트웨어 플랫폼
  • 컨테이너
    • 가상화 기술 중 하나로 대표적으로 LXC(Linux Container)가 있습니다. 기존 OS를 가상화 시키던 것과 달리 컨테이너는 OS레벨의 가상화로 프로세스를 격리시켜 동작하는 방식
  • 이미지
    • 컨테이너를 생성하는 데 필요한 읽기 전용 탬플릿. 모든 파일과 설정이 포함된다.

 

 

시스템 아키텍처

 

 

✅ GitHub Action Workflow

name: CI-CD

on:
  push:
    branches: [ "main" ]

jobs:

  build-and-push:
    name: Build & Push Docker Image
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Grant permission for gradlew
        run: chmod +x gradlew

      - name: Build JAR
        run: ./gradlew clean build -x test

      # Docker Buildx
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build & Push Docker Image
        uses: docker/build-push-action@v4
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/basketball-backend:latest


  deploy:
    name: Deploy to EC2
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Install SSH key
        run: |
          mkdir -p ~/.ssh
          printf "%s" "${{ secrets.EC2_SSH_KEY }}" | tr -d '\r' > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keygen -y -f ~/.ssh/id_rsa > /dev/null

      - name: Deploy on EC2
        run: |
          ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
            cd /srv/basketball

            # 최신 이미지 Pull
            docker compose pull

            # 기존 컨테이너 종료
            docker compose down

            # 새로운 컨테이너 실행
            docker compose up -d

            # 상태 확인
            docker ps
          EOF

 

 

1️⃣ 트리거(Workflow 실행 조건)

on:
  push:
    branches: [ "main" ]
  • main 브랜치에 push될 때마다 Workflow 실행
  • 따라서 운영 배포가 main 반영과 연결된다
    • 장점 : 배포 흐름이 단순하고 빠름
    • 주의점 : main으로 들어가는 코드는 "배포 가능한 상태"여야 함. (보통 PR에서 테스트 통과 -> main merge 구조로 안정화)

 

2️⃣ Job 구성

  • build-and-push(CI)
    • 빌드/이미지 생성/이미지 push까지 담당
  • deploy(CD)
    • 서버(EC2)에 반영하는 배포 담당
    • needs: build-and-push로 CI가 성공해야만 실행

 

🔸 build-and-push(CI)

build-and-push:
  name: Build & Push Docker Image
  runs-on: ubuntu-latest

 

  • GitHub가 제공하는 Ubuntu 러너에서 실행된다.
  • 이 러너는 매번 새로 만들어지는 “깨끗한 환경”이라서, 빌드 재현성이 좋다.

 

Checkout Repository

- name: Checkout Repository
  uses: actions/checkout@v4

 

하는 일

  • 러너(ubuntu)에 GitHub 레포지토리 코드를 내려받는다.

왜 필요?

  • 다음 단계(Gradle build, Docker build)가 레포지토리 파일을 기반으로 동작하므로 필수 단계다.

 

Set up JDK 17

- name: Set up JDK 17
  uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: 17

하는 일

  • CI 러너에 **Java 17(Temurin 배포판)**을 설치한다.

왜 필요?

  • Spring Boot 프로젝트가 Java 17 기반이기 때문.
  • CI 서버에서도 동일한 Java 버전으로 빌드해야 “로컬에서는 되는데 CI에서는 깨짐” 문제를 줄일 수 있다.

 

Grant permission for gradlew

- name: Grant permission for gradlew
  run: chmod +x gradlew

하는 일

  • gradlew 실행 권한 부여.

왜 필요?

  • 리눅스 환경에서 실행권한이 없으면 Permission denied로 Gradle Wrapper 실행이 실패할 수 있음.
  • 특히 Windows에서 커밋된 파일이 CI 리눅스에서 실행될 때 자주 터지는 문제 중 하나.

 

Build JAR

- name: Build JAR
  run: ./gradlew clean build -x test

하는 일

  • Gradle로 빌드해서 JAR 파일을 생성한다.
  • clean → 이전 빌드 산출물 제거 후 새로 빌드
  • -x test → 테스트 task는 제외

왜 필요?

  • “컴파일/패키징이 되는 상태인지”를 확인하고, 배포 가능한 산출물(JAR)을 만들기 위함.

주의(현 상태의 특성)

  • -x test 때문에 테스트 실패를 CI에서 잡지 못함
  • 즉, 컴파일은 되지만 런타임 오류가 있는 코드도 배포될 가능성이 있음
    ❗️ 이건 추후 개선 포인트!

 

Set up Docker Buildx

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2

 

하는 일

  • Docker Buildx를 세팅한다.

왜 필요?

  • buildx는 더 유연한 빌드(캐시, 멀티플랫폼 등)를 지원한다.
  • 지금 당장 멀티플랫폼을 안 쓰더라도, docker/build-push-action과 궁합이 좋고 표준처럼 쓰임.

 

Login to Docker Hub

- name: Login to Docker Hub
  uses: docker/login-action@v2
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

하는 일

  • Docker Hub에 로그인(인증)한다.
  • 인증 정보는 GitHub Secrets에서 가져온다.

왜 필요?

  • 인증이 없으면 CI 환경에서 Docker Hub에 이미지를 push할 권한이 없다.

보안 포인트

  • 계정/토큰을 코드에 하드코딩하지 않고 Secrets로 관리하는 방식은 실무에서도 기본이다.

 

Build & Push Docker Image

- name: Build & Push Docker Image
  uses: docker/build-push-action@v4
  with:
    context: .
    file: ./Dockerfile
    push: true
    tags: ${{ secrets.DOCKERHUB_USERNAME }}/basketball-backend:latest

하는 일

  • 레포지토리 루트(context: .)를 기준으로 Dockerfile을 이용해 이미지를 빌드한다.
  • push: true이기 때문에 빌드 후 Docker Hub에 바로 업로드(push) 한다.
  • 이미지 태그는 .../basketball-backend:latest

핵심 의미

  • 이 단계가 끝나면 Docker Hub에는 “최신 버전의 이미지”가 올라가 있고,
  • 다음 deploy job은 서버에서 이 이미지를 pull해서 실행하게 된다.

추적성 관점의 한계(현 상태)

  • 태그가 latest 하나라서 “어떤 커밋이 배포됐는지” 추적이 약함
    → 개선 포인트: latest + ${{ github.sha }} 같이 태그 2개 push

 

🔸 CD Job: deploy 상세

deploy:
  name: Deploy to EC2
  needs: build-and-push
  runs-on: ubuntu-latest

핵심

  • needs: build-and-push
    → CI가 성공해야만 배포가 실행된다.
    → “빌드/푸시 실패했는데 배포가 진행되는 사고”를 방지하는 최소 안전장치다.

 

install SSH key

- name: Install SSH key
  run: |
    mkdir -p ~/.ssh
    printf "%s" "${{ secrets.EC2_SSH_KEY }}" | tr -d '\r' > ~/.ssh/id_rsa
    chmod 600 ~/.ssh/id_rsa
    ssh-keygen -y -f ~/.ssh/id_rsa > /dev/null

하는 일(줄별로)

  • ~/.ssh 디렉토리 생성
  • GitHub Secrets의 EC2_SSH_KEY를 러너의 ~/.ssh/id_rsa로 저장
  • tr -d '\r'는 Windows 줄바꿈(CRLF) 섞였을 때 SSH 키 깨지는 걸 방지
  • 권한을 600으로 설정(SSH는 키 권한이 느슨하면 거부함)
  • ssh-keygen -y로 키가 정상인지 검증(간접 체크)

왜 필요?

  • 다음 단계에서 ssh user@host로 EC2에 접속해야 하기 때문.

 

Deploy on EC2

- name: Deploy on EC2
  run: |
    ssh -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} << 'EOF'
      cd /srv/basketball

      docker compose pull
      docker compose down
      docker compose up -d

      docker ps
    EOF

(1) SSH 접속

  • ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}로 EC2에 접속
  • StrictHostKeyChecking=no는 최초 접속 시 known_hosts 확인을 생략해서 실행을 끊김 없이 진행하도록 함
    (편하지만 보안적으로는 약점 포인트 → 개선 가능)

(2) 배포 디렉토리 이동

  • cd /srv/basketball
  • 이 폴더에 docker-compose.yml 및 .env 등이 존재한다는 전제

(3) 배포 명령 3종 세트

  1. docker compose pull
    • Docker Hub에서 최신 이미지를 가져옴
  2. docker compose down
    • 기존 컨테이너 종료 및 네트워크 정리
  3. docker compose up -d
    • 최신 이미지로 컨테이너를 백그라운드 실행

(4) 상태 확인

  • docker ps로 현재 실행 중인 컨테이너를 출력

이 방식의 특징

  • 구현이 단순하고 안정적으로 “최신 버전 반영”이 가능
  • 대신 down → up이라 짧은 다운타임이 발생할 수 있음→ 무중단 배포는 후속 개선 포인트로 쓰기 좋다.

 

📌 정리

 

✅ main push
CI: Gradle build → Docker 이미지 빌드/푸시(Docker Hub)
CD: EC2 SSH 접속 → docker compose pull/down/up → 최신 버전 실행