3058 단어
15 분
프로젝트에 ci cd 간단하게 적용해 보기
2025-05-07

도입 배경#

프로젝트를 진행하며 새로운 기능이 추가될 때마다 ec2에 직접 들어가서 다음과 같은 과정을 반복했다. (처음에 배포가 익숙하지 않아 아예 재배포 스크립트를 만들어놓다..)

1. cd be
2. ps aux | grep java
3. kill -9 첫번째 프로세스
4. rm nohup.out
5. git pull origin dev
6. ./gradlew build -x test
7. export $(cat .env | xargs) -> 환경변수 로드하기
8. nohup java -jar build/libs/xxx-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev &

시작부터 지금까지 재배포를 위해 다음과 같은 스크립트를 100번 이상 반복적으로 작성하였고, 이 불편함을 덜기 위해 ci cd를 학습하고 도입하였다.

ci / cd란?#

docappstore

ci란?#

개발자들은 코드 변경 사항을 주기적으로 빈번하게 머지한다. 특히 협업을 할 때는 충돌을 최소화 하기 위해 작은 단위로 나눠서 개발한다. 이 때 코드가 push되면 자동으로 테스트, 빌드가 수행되어 안정적인 배포 파일을 만든다. 만약 테스트, 빌드가 실패하면 push된 코드에 오류가 있다고 개발자에게 피드백을 주어 오류에 발빠르게 대처할 수 있다.

cd란?#

CD는 Continuous Delivery(지속적 제공)와 Continuous Deployment(지속적 배포)를 모두 의미하고 간단히 말하면 배포 과정의 자동화이다. 처음에는 EC2에 들어가서 수동으로 배포하느라 항상 같은 명령어를 반복 입력했지만, CD를 도입하면 코드를 푸시할 때 자동으로 테스트, 빌드, 배포까지 이어진다.

ci / cd의 장점#

  • 개발 속도를 높일 수 있다
  • 안정성을 확보할 수 있다

ci / cd 툴#

ci / cd 툴을 찾아보니 많이 쓰이는 2가지는 Jenkinsgithub action 이 있었고 두 가지 중에 어떤 툴을 사용할 지 고민했다. 젠킨스는 github action보다 먼저 사용되어 참고할 자료가 많았다. 젠킨스는 직접 서버를 구축하고 설정해야 하지만, GitHub Actions는 별도의 빌드 서버를 직접 관리할 필요 없이 GitHub에서 제공하는 가상 환경(runners)에서 빌드 작업을 수행할 수 있다. 따라서 초기 설정이 간편한 GitHub Actions를 선택했다.

Github Actions의 구성 요소#

먼저 Github Actions에서 사용되는 용어를 알아보자.

  1. Workflow

레포에 추가할 수 있는 일련의 자동화된 커맨드 집합. 하나 이상의 Job으로 구성되어 있고 빌드, 테스트, 배포 등 각각의 역할에 맞는 workflow를 추가할 수 있으며 .github/workflows 디렉토리에 YAML 형식으로 저장한다.

  1. Event

Workflow를 실행시키는 Push, Pull Request, Commit 등의 특정 행위를 의미한다.

  1. Job

같은 Runner에서 실행되는 여러 step들의 집합을 의미한다. 기본적으로 하나의 Workflow 내의 여러 job들은 독립적으로 실행되지만 필요에 따라 의존 관계를 설정하여 순서를 지정해줄 수 있다.

  1. Step

커맨드를 실행할 수 있는 각각의 Task. Shell 커맨드가 될 수도 있고, 하나의 Action이 될 수 있다. 하나의 Job 내에서 각각의 Step은 다양한 Task로 인해 생성된 데이터를 공유할 수 있다.

  1. Action

Job을 만들기 위해 Step을 결합한 독립적인 커맨드. 재사용이 가능한 Workflow의 가장 작은 단위 블럭.

  1. Runner

Github Actions Workflow 내에 있는 Job을 실행시키기 위한 애플리케이션. Github에서 호스팅하는 가상 환경에서 실행할 수 있다.

github action 적용하기#

docappstore

먼저 github repository에 가서 Actions로 이동한다. gradle을 검색하고 java with gradle이라는 workflow를 찾는다.

# Actions 탭에 나타날 workflow의 이름
name: Java CI with Gradle

on:
    # workflow가 실행되는 조건(main 브랜치에 push, pr이 일어난 경우)
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:  # 해당 workflow의 job 목록 
  build: # job의 이름(build라는 이름으로 job이 표시)

    runs-on: ubuntu-latest # Runner가 실행되는 환경 정의
    permissions:
      contents: read

    steps: # build job 내의 step 목록
    - uses: actions/checkout@v4 ## 내 github 레포지토리 코드를 러너에 다운
    - name: Set up JDK 17  # 자바 17 설치
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    - name: Setup Gradle # gradle 설정
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0

    - name: Build with Gradle Wrapper
      run: ./gradlew build # 프로젝트를 빌드(테스트 + 컴파일)

다음과 같이 기본 스크립트가 있고 해당 작업 내용을 보면 주석에 정리한 내용과 같다. github action의 동작 원리와 명령어만 익히고 예전에 수동 배포 하던 방식을 그대로 github action으로 작성해보았다.

첫 CI/CD 적용: EC2 내부에서 빌드 및 배포#

name: Java CI/CD with EC2 Git Pull Deploy

on:
  push:
    branches:
      - main

jobs:
  git-pull-and-deploy:
    runs-on: ubuntu-latest

    steps:
    - name: ✅ SSH into EC2, pull latest code, build, and restart app
      uses: appleboy/ssh-action@v0.1.10
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ec2-user
        key: ${{ secrets.EC2_KEY }}
        script: |
          echo "👉 [1] Move to backend project folder"
          cd ~/backend

          echo "🔄 [2] Git pull latest code"
          git reset --hard HEAD
          git pull origin main

          echo "🧹 [3] Kill existing app (port 8080)"
          pid=$(lsof -t -i:8080)
          if [ -n "$pid" ]; then
            kill -9 $pid
          else
            echo "No app running on port 8080."
          fi

          echo "⚙️ [4] Grant execute permission to gradlew"
          chmod +x ./gradlew

          echo "🛠️ [5] Build project (including tests)"
          ./gradlew clean build

          echo "🚀 [6] Start new Spring Boot app"
          export $(cat .env | xargs)
          nohup java -jar build/libs/innervoice-0.0.1-SNAPSHOT.jar \
            --spring.profiles.active=dev > nohup.out 2>&1 &

          echo "📄 [7] Check nohup output (last 30 lines)"
          tail -n 30 nohup.out

먼저 GitHub Actions를 이용한 가장 간단한 CI/CD 방식을 적용하였다. 이 접근법은 GitHub에서 EC2로 SSH 접속하여 서버 내부에서 직접 코드를 pull 받고 빌드하는 방식이다.

실행 흐름

  1. 코드 merge 후 main branch에 push
  2. EC2 서버에 SSH 접속
  3. EC2 안에서 git pull
  4. EC2 안에서 ./gradlew clean build -x test (CI)
  5. EC2 안에서 기존 앱 죽이기 (port 8080 kill)
  6. EC2 안에서 새 jar 실행 (CD)
문제 상황
  • 빌드나 테스트 실패해도 EC2 서버 안에서는 다 돌아가버린다.
  • CI 서버(GitHub Actions)에서 빌드/테스트를 먼저 검증하고 “통과한 코드만” 배포하는 안전장치가 없다.
  • build를 ec2 안에서 하는데 그러면 기존에 수행하던 서비스 + build까지 하니까 부하 발생

즉, CI 서버에서 미리 코드 검증을 하지 않고, 운영 서버(EC2) 안에서 직접 검증하려고 하는 구조

보통 작은 프로젝트나 급하게 빨리 배포해야 할 때 사용하는 방법

개선된 CI/CD 파이프라인: GitHub Actions에서 빌드 후 배포#

# 워크플로 이름 (GitHub UI에서 보여짐)
name: Java CI/CD with GitHub Build and EC2 Deploy

# 워크플로를 실행할 조건
on:
  push:  # main 브랜치에 push될 때 실행
    branches:
      - main
  pull_request:  # main 브랜치로 PR 생성/업데이트 시에도 실행
    branches:
      - main

jobs:
  build-and-deploy:  # 하나의 Job: 빌드 및 배포를 담당
    runs-on: ubuntu-latest  # GitHub에서 제공하는 Ubuntu 최신 서버에서 실행됨

    steps:
      # Step 1: GitHub 레포지토리의 코드를 체크아웃 (가져오기)
      - name: ✅ Checkout code
        uses: actions/checkout@v3

      # Step 2: Java 21을 설치 (Temurin은 OpenJDK 배포판 중 하나)
      - name: ☕ Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '21'

      # Step 3: gradlew 파일에 실행 권한 부여 (권한 없으면 빌드 실패)
      - name: 🔐 Grant execute permission to gradlew
        run: chmod +x ./gradlew

      # Step 4: Gradle로 빌드 및 테스트 수행
      - name: 🛠️ Build with Gradle (with tests)
        run: ./gradlew clean build

      # Step 5: push 이벤트일 때만 EC2로 JAR 파일 복사
      - name: 📦 Copy JAR to EC2
        if: github.event_name == 'push'  # PR일 경우 생략됨
        uses: appleboy/scp-action@v0.1.4  # EC2로 파일 전송
        with:
          overwrite: true  # 기존 파일 덮어쓰기 허용
          host: ${{ secrets.EC2_HOST }}  # EC2의 IP 주소 또는 도메인 (비밀 변수로 관리)
          username: ec2-user  # EC2 접속 계정 (Amazon Linux 기본은 ec2-user)
          key: ${{ secrets.EC2_KEY }}  # 개인 키를 GitHub Secrets로 저장해 사용
          source: "build/libs/innervoice-0.0.1-SNAPSHOT.jar"  # 전송할 jar 파일 경로
          target: "/home/ec2-user/backend/"  # EC2에서 저장할 위치

      # Step 6: EC2에 SSH 접속 후 기존 앱 종료 및 새 JAR 실행
      - name: 🚀 SSH into EC2 and restart app
        if: github.event_name == 'push'  # push일 때만 실행
        uses: appleboy/ssh-action@v0.1.10  # SSH 원격 명령 실행
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.EC2_KEY }}
          script: |
            echo "🧹 Kill existing app (port 8080)"
            pid=$(lsof -t -i:8080)  # 8080 포트 사용 중인 프로세스 ID 검색
            if [ -n "$pid" ]; then
              kill -9 $pid  # 프로세스 강제 종료
            else
              echo "No app running on port 8080."  # 실행 중인 앱이 없을 경우 메시지 출력
            fi

            echo "🚀 Start new Spring Boot app"
            export $(cat ~/backend/.env | xargs)  # .env 파일의 환경변수를 export
            nohup java -jar ~/backend/build/libs/innervoice-0.0.1-SNAPSHOT.jar \
              --spring.profiles.active=dev > ~/backend/nohup.out 2>&1 &  # 백그라운드 실행 및 로그 저장

            echo "📄 Show last 30 lines of nohup"
            tail -n 30 ~/backend/nohup.out  # nohup 로그 파일의 마지막 30줄 출력

실행 흐름

  1. main 브랜치 push (Merge된 코드)
  2. GitHub Actions
    • 코드 checkout ✅ (통합 시작)
    • 전체 프로젝트 build ✅ (통합 검증)
    • 전체 테스트 실행 ✅ (통합 검증 완료)
  3. 빌드/테스트 통과
  4. 빌드된 jar 파일을 EC2로 전송 (배포)
  5. EC2에서 실행

위 방식의 장점

  1. 기존 방식은 git pull 이후 빌드 실패 가능성이 있지만 github action에서 빌드 완료된 검증된 파일만 배포
  2. git pull을 하여 코드 전체를 pull하지만, 코드 전체를 가져오는 것이 아니라 러너에서 빌드된 .jar 파일만 ec2 서버로 복사하고 실행하므로 가벼움
  3. 다중 서버라면 여러 서버마다 git pull, build 해야 하지만, jar 파일만 여러 서버에 전송하면 되므로 확장성 좋음

보안 설정 : Github Secrets 사용#

docappstore

Action Runner가 프로젝트를 배포할 서버에 접속하려면 key 파일(.pem 등)과 사용자의 이름, 서버의 주소가 필요하다. 이런 정보를 공개적인 곳에 노출시킬 수 없으므로, Github에서 제공되는 Actions secrets and variables 저장소를 활용한다.

Settings>Security>Secrets and variables>Actions로 이동하면 다음과 같이 Actions에서 사용할 수 있는 레포지토리 시크릿을 등록할 수 있는 페이지가 나온다.

docappstore

New repository secret으로 들어가 name(변수명), secret(값)을 작성해준 뒤 Add secret을 눌러준다.

스크립트 작성 위치#

docappstore

작성한 github action 스크립트는 /.github/workflows/ 하위에 gradle.yml 파일명으로 작성한다.

workflow 실행#

main branch에 push 하는 이벤트가 발생했을 때 작성한 workflow가 실행되도록 작성하였다.

docappstore

Actions 창으로 이동하면 다음과 같이 등록된 Workflow가 실행되는 것을 확인할 수 있다.

docappstore

처음에 스크립트 작성이 쉽게 해결되지 않아 하나씩 고쳐보고 몸소 배운 흔적들..

성과#

GitHub Actions를 도입하고 나서, 빌드와 테스트를 GitHub에서 미리 진행하고 EC2 서버에는 최종적으로 검증된 jar 파일만 배포하게 되었다. 덕분에 배포 과정이 훨씬 더 간단해졌으며, 서버에 직접 접속해서 복잡한 명령어를 입력할 필요가 없어 부담이 배포에 대한 많이 줄었다..! 또한 백엔드 팀원끼리 고민했던 문제가 build를 ec2 내에서 진행하기 때문에 실패하면 서비스가 중단되어 게시글을 통해 이용자들에게 사과를 하였는데 build와 test가 완료된 코드들이 실행되므로 이에 대한 문제를 해결했다.(지금은 잠시 서버가 중단되지만 추후에 무중단 배포도 적용하면 좋을 것 같다) 전체적으로 자동화를 통해 불필요한 시간 낭비가 줄어들었다.

참고 자료#

프로젝트에 ci cd 간단하게 적용해 보기
저자
Joonyoung Hwang
게시일
2025-05-07