배포의 모든 것 - 3. Github Action을 통한 CI/CD

2025. 3. 18. 01:40·프로젝트

현재 AWS 서버는 EC2와 RDS가 준비되어 있습니다. 

이번 시간에는 깃허브 액션을 통해서 CI/CD 파이프 라인 구축을 진행해 보도록 하겠습니다..!

✅  깃 플로우(Git-Flow)

우선 깃 플로우라는 것을 알아야 합니다.

💠 master(main) : 실제 배포가 되서 운영되는 브랜치
💠 hotfix : 버그가 생긴 경우
💠 release : develop에서 충분히 검증이 된 코드 , 배포를 앞두고 있는 코드.
💠 develop: 개발된 기능이 모이는 브랜치.
💠 feature: 기능 하나와 관련된 브랜치.

 

보통 포로젝트를 하는 경우 완전히 똑같지는 않더라도 위와 같은 브랜치 전략에 따라서 개발을 하게 됩니다!

아마 프로젝트를 진행하시는 경우, develop 브랜치에 코드를 모아놓고 테스트를 하게 될 수도 있습니다. 

 

🤔 위와 같은 상황이라면 어떤 경우에 자동으로 배포가 되어야 할까요?

 

다음과 같은 상황을 생각할 수 있습니다.

 

☑️ Develop에 merge되는 경우

- develop 브랜치에서 많은 테스트를 하고 또 이 API가 서버에서 작동을 잘하는지 확인하기 위해서 서버에 배포가 필요할 수 있습니다.

 

☑️ Main에 merge 되는 경우

- 모든 테스트를 마치고 머지를 시키는 경우 서버에 또 배포가 필요할 수도 있습니다.

 

네, 필요에 따라서 2개 이상의 CI/CD 파이프 라인이 필요할 수도 있습니다.

이번 포스트에서는 현재 로컬에서 작업하고 있는 Spring Boot 어플리케이션이 있고,  브랜치가 분리된 깃허브 레포지토리가 생성되어 있는 상황을 가정하겠습니다.

✅ CI/CD란?

💠 CI (Continuous Integration) - 지속적 통합

말 그대로 지속적으로 통합되는 것을 뜻합니다. 
각각 다른 개발자들이 개별적으로 Issue를 생성해서 각자의 브랜치에서 예를 들어 main 브랜치로 모으는 과정도 CI에 해당됩니다.

이때 두 가지 Point가 있습니다.

 

💡 코드의 변경 사항을 주기적으로 빈번하게 Merge 할 것.

  • 너무 오랫동안 작업하고 나서 코드를 병합하면 시간이 오래 걸리게 됩니다.
  • 작은 단위로 개발하여 주기적으로 Merge 해야 합니다.

💡통합을 위한 단계 자동화

  • Merge를 할 때마다 사람이 Build후에 테스트까지 진행하게 된다면 너무 번거로울 것입니다..! 
  • 이런 과정은 자동화하면 좋습니다.

그래서 보통 이런 일련의 과정들을 자동화하는 것을 CI 파이프라인을 만든다고 표현합니다!

Git 또는 Github와 혼동하시면 안 됩니다. Github를 이용해서 브랜치를 관리하고 머지할 수 있겠지만 보통 CI 파이프라인을 만든다고 하면 Build와 Test의 과정까지 자동화하는 것을 뜻합니다.

🤔 이제 "지속적인 통합"이라는 말이 이해가 가시나요?

💠 CD(Continous Delivery 또는 Continous Deployment)

🧐CD는 마지막을 완전 자동화 하느냐 아니면 검증팀이 수동으로 배포하느냐에 따라서 갈리게 됩니다 

💡 Continous Delivery

  • 배포(Release) 준비까지만 하고 검증팀이 수동으로 배포를 진행합니다.

💡 Continous Deloyment

  • 자동으로 배포까지 되는 것을 의미합니다.
🙂 그렇다면 CI/CD 파이프 라인을 만든다는 것은 어떤 것을 의미할까요?

위에서 말한 CI와 CD를 자동으로 해주는..! 지속적으로 Build와 Test를 진행해서 통합해 주고, 지속적으로 배포 또는 배포 직전 준비까지 해주는 세팅을 해준다는 뜻입니다.

CI/CD를 도와주는 다양한 도구들이 있습니다. 이번 실습에서는 Github Action을 사용하도록 하겠습니다.
깃허브 액션과 관련된 포스트를 읽고 오시면 이해 하시는 것에 도움이 됩니다. 🔗

✅  Github Action을 통한 CI/CD 

먼저, 저는 간단히 start.spring.io를 통해서 프로젝트를 생성했고 다음과 같은 의존성을 추가했습니다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

그 후, 레포지토리와 연결을 하고 main branch 이외에 develop 브랜치를 만들었습니다.

🤔 깃허브 기본 세팅에 대한 부분은 나중에 다시 정리하겠습니다. 배포 게시물에 추가하기에는 양이 많고 다른 내용이라고 생각되어서 일단 넘어가겠습니다.

 

  • 이슈를 하나 파주겠습니다.

  • Development에서 브랜치를 만들어줍시다.

  • 개발은 develop에서 뻗어나가 이루어지기 때문에 develop에서 브랜치가 뻗어나가도록 브랜치를 만들어줍니다.

  • 인틸리제로 돌아와서 git fetch를 해줍니다.
  • 버전과 os에 따라서 위의 메뉴바가 없을 수 있습니다. 

  • 없다면 왼쪽 아래에서 터미널을 열고 git 명령어로 가져와줍니다.

  • 아까 만들어준 브랜치로 checkout 해줍니다.

  • 다음과 같이 root 경로에 반드시 디렉토리 이름을 .github/workflows로 만들어야 합니다. 그 안에 yml을 하나 만들어주세요 이름은 자유입니다. (깃허브 마크가 보여야 제대로 만들어진 것 입니다!!)

  • root 경로 라고 하면 이곳에서 보여야 한다는 것을 의미합니다!
name: CI/CD Pipeline

on:
  push:
    branches: [ develop ]
  pull_request:
    branches: [ develop ]  # develop 브랜치로의 PR도 트리거됨

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

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

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Make application-secret.yml
        run: |
          touch ./src/main/resources/application-secret.yml
          echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.yml
        shell: bash

      - name: Build with Gradle Wrapper (Skip Tests)
        run: |
          ./gradlew --stop && ./gradlew clean --refresh-dependencies
          ./gradlew clean bootJar -x test  
          ls -l build/libs 

      - name: Deploy JAR to EC2
        env:
          PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
          HOST: ${{ secrets.EC2_HOST }}
          USER: ubuntu
          TARGET_DIR: /home/ubuntu/cicd/
        run: |
          # SSH 키 저장 (줄바꿈 유지)
          printf "%s\n" "${{ secrets.EC2_SSH_KEY }}" > private_key.pem
          chmod 600 private_key.pem
          
          # 가장 최근 빌드된 실행 가능한 JAR 찾기
          LATEST_JAR=$(ls -t build/libs/*.jar | grep -v 'plain' | head -n 1)

          echo "Deploying $LATEST_JAR to EC2..."

          # 빌드된 실행 가능 JAR만 EC2로 전송
          scp -v -i private_key.pem -o StrictHostKeyChecking=no "$LATEST_JAR" $USER@$HOST:$TARGET_DIR

      - name: Run Application on EC2
        env:
          PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
          HOST: ${{ secrets.EC2_HOST }}
          USER: ubuntu
          TARGET_DIR: /home/ubuntu/cicd/

        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem

          # EC2에서 기존 애플리케이션 종료 후 새 JAR 실행
          ssh -i private_key.pem -o StrictHostKeyChecking=no $USER@$HOST << EOF
            pkill -f 'java -jar' || echo "No existing app running"
            nohup java -jar $TARGET_DIR/*.jar > $TARGET_DIR/app.log 2>&1 &
          EOF
  • yml의 내용은 다음과 같이 입력하고 commit 후 push 해줍시다.

  • 그다음에는 현재 브랜치에서 develop으로 머지하는 PR을 만들어줍니다.

  • 깃허브 메뉴에서 Action을 눌러주세요 그럼 CI/CD가 돌아갔고 실패한 것을 확인할 수 있습니다.
  • 돌아간 기록이 있다면 성공입니다! 실패하는 것이 당연해요. 이제 CI/CD를 돌리기 위한 설정을 해보겠습니다.

  • 만들어 놓았던 EC2 서버에 접속하면 /home/ubuntu 경로로 접속되게 됩니다. 이에 따라서 ls를 입력하면 아무것도 나오지 않습니다.
mkdir -p /home/ubuntu/cicd
  • 위의 코드를 입력해서 cicd 경로를 만들어주고 다시 IntelliJ로 돌아와 줍니다.

### env ###
src/main/resources/application-secret.yml
  • gitignore에 위의 코드를 추가해주세요.

  • 그리고 Application-secret.yml을 만들어줍시다.
  • application.yml은 git으로 추적되고 application-secret.yml은 추적되지 않고 있다면 성공입니다(주황색으로 보이면..!)
spring:
  config:
    import: application-secret.yml
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        jdbc:
          time_zone: Asia/Seoul
        show_sql: true

        highlight_sql : true

logging:
  level:
    org.springframework.web: DEBUG
    org.springframework.web.client.DefaultRestClient: OFF
  • application.yml에는 다음과 같이 application-secret.yml을 import 합니다.
  • 이렇게 하면 깃허브 commit에 민감한 정보에 대한 기록을 숨길 수 있습니다. 
spring:
  profiles:
    active: local
---
spring:
  config:
    activate:
      on-profile: local
  datasource:
    username: (DB사용자 이름)
    password: (DB 비밀번호 )
    url: dbc:mysql://(DB 경로):3306/(DB 이름)
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        show_sql: true
        format_sql: true
        use_sql_comments: true
        default_batch_fetch_size: 1000
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
    org.springframework.jdbc.core: DEBUG
  • 위와 같이 application-secret.yml을 완성해주세요!
  • RDS는 Private VPC에 있기 때문에 위의 yml을 바탕으로 로컬에서 실행하면 당연히 DB 연결이 되지 않습니다...!! 
    하지만, EC2에서 실행되면 작동하겠죠..! 프로필 분리는 뒤에서 다시하겠습니다. (RDS를 Public으로 했다면 문제없이 되겠지만요...)
  • 일단 EC2는 위의 yml구조를 가지고 배포를 해야 정상적으로 동작이 되며, 앞에서 소개한 session manager로 포트 포워딩을 하고 localhost의 (포워딩한 포트)로 설정하면 private vpc에 있는 RDS도 local에서 테스트 가능하긴 합니다.

  • 그다음 만들어 놓았던 Repository의 Setting > Secrets and Variables > Actions로 들어와 주세요.

  • deploy.yml에 입력을 했었던 변수들을 그대로 만들어주면 됩니다.
  • APPLICATION_SECRET : application-secret.yml을 그대로 복사해주세요
  • EC2_HOST : EC2 Public IP 주소
  • EC2_SSH_KEY : pem 키 내부의 내용을 전부 복사해주시면 됩니다 (pem키 잘 보관하셨죠..?)

  • 실패했던 ci/cd를 다시 돌려줍니다..!

  • 조금 기다려 주시면 빌드가 완료된 것을 확인할 수 있습니다.
  • 이제 EC2 콘솔로 가서 아래 코드를 써주세요.
ps -ef | grep java

  • 다음과 같이 실행 중이라고 뜨면 성공입니다.

자 지금까지 실행된 프로세스를 설명해드리겠습니다. 

name: CI/CD Pipeline

on:
  push:
    branches: [ develop ]
  pull_request:
    branches: [ develop ]  # develop 브랜치로의 PR도 트리거됨

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

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

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew

      - name: Make application-secret.yml
        run: |
          touch ./src/main/resources/application-secret.yml
          echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.yml
        shell: bash

      - name: Build with Gradle Wrapper (Skip Tests)
        run: |
          ./gradlew --stop && ./gradlew clean --refresh-dependencies
          ./gradlew clean bootJar -x test  
          ls -l build/libs 

      - name: Deploy JAR to EC2
        env:
          PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
          HOST: ${{ secrets.EC2_HOST }}
          USER: ubuntu
          TARGET_DIR: /home/ubuntu/cicd/
        run: |
          # SSH 키 저장 (줄바꿈 유지)
          printf "%s\n" "${{ secrets.EC2_SSH_KEY }}" > private_key.pem
          chmod 600 private_key.pem
          
          # 가장 최근 빌드된 실행 가능한 JAR 찾기
          LATEST_JAR=$(ls -t build/libs/*.jar | grep -v 'plain' | head -n 1)

          echo "Deploying $LATEST_JAR to EC2..."

          # 빌드된 실행 가능 JAR만 EC2로 전송
          scp -v -i private_key.pem -o StrictHostKeyChecking=no "$LATEST_JAR" $USER@$HOST:$TARGET_DIR

      - name: Run Application on EC2
        env:
          PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
          HOST: ${{ secrets.EC2_HOST }}
          USER: ubuntu
          TARGET_DIR: /home/ubuntu/cicd/

        run: |
          echo "$PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem

          # EC2에서 기존 애플리케이션 종료 후 새 JAR 실행
          ssh -i private_key.pem -o StrictHostKeyChecking=no $USER@$HOST << EOF
            pkill -f 'java -jar' || echo "No existing app running"
            nohup java -jar $TARGET_DIR/*.jar > $TARGET_DIR/app.log 2>&1 &
          EOF

위의 코드에 따라서 ci/cd가 실행이 됩니다. 이 부분에 대해서 간단히만 설명드리고 디테일적인 부분은 깃허브 액션 포스트에서 확인해 주세요.

 

✅ On의 조건에 따라서 develop 브랜치에 push를 하거나 pull request를 하는 경우 위의 코드가 돌아가게 된다는 조건입니다.

  •  이 조건에 충족하게 되면 아래의 job가 실행됩니다. 보통 push 조건만 써도 되긴 하지만, ci/cd를 구현하는 과정에서는 pull request도 허용하면 테스트를 하는 것이 용이해서 추가해 놨습니다.

✅ 기본세팅

  • 권한을 설정하고 기본 세팅을 하는 부분이라고 간단히 이해해주세요.

✅ application-secret.yml을 빌드에 포함시키기

⭐ 아주 아주 중요한 부분입니다!! 현재 저희는 DB 비밀번호등 민감한 정보를 application-secret으로 만들어 놓고 application.yml이 이 것을 참조하도록 하고 있습니다.

그런데 application-secret.yml은 깃허브로 추적하고 있지 않습니다. 이에 따라서 ci/cd 환경에서 application-secret.yml을 만들어 주는 겁니다. 이렇게 되면 변수들이 외부로 공개되지 않도록 관리할 수 있습니다.

이때 ${{ secrets.APPLICATION_SECRET }} 이런 식으로 변수로 관리되도록 하고 이 변수를 github secret로 관리하는 것입니다.
인텔리제의 환경변수와 비슷한 과정입니다.

즉, application-secret.yml이 바뀔 때마다 로컬 resource에서만 바꾸는 것이 아니라 github secret의 변수도 수정해주어야 하며 팀원들에게도 수정된 내용을 공유해야 합니다. 

 

✅ Jar 파일로 빌드하는 과정입니다.

현재 테스트를 제외한 상태입니다!

그 이유는 현재 cicd 환경에서는 테스트가 실패하기 때문입니다? 왜냐하면 현재 yml은 로컬과 외부의 프로필이 분리가 되어 있지 않습니다. 프로필을 분리한다는 뜻은 EC2환경과 로컬 환경에서 다른 yml조건을 사용한다는 뜻입니다.

예를 들어서, EC2에서는 private subnet에 존재하는 rds에 접근이 가능하기 때문에 rds의 DB에 연결이 가능하지만, 로컬에서는 접속이 불가능하기 때문에 로컬 DB에 연결하는 yml 프로필을 분리해서 사용할 수 있다는 뜻입니다.

현재는 프로필이 분리되어 있지 않기 때문에 cicd 환경에서 RDS의 DB에 연결을 시도할 경우 실패하게 되겠죠?? 그래서 테스트를 우선 제외했습니다. 이 부분은 Docker를 추가하는 과정에서 프로필을 분리하며 제거할 예정입니다.

✅ 이제 만든 Jar 파일을 EC2로 복사하는 과정입니다.

EC2의 주소, Private SSH Key, 그리고 유저 이름을 통해서 EC2에 접속하여 Jar파일을 EC2로 전송합니다.

저는 그대로 작성했지만, 유저 이름도 github secrets로 관리해도 좋습니다! 

✅ EC2에서 스프링 어플리케이션 실행

  • 말 그대로 어플리케이션을 실행해주는 코드입니다.
크게 어려운 부분이 없습니다. 
Spring Application에 secret 변수 넣어주기 ->  Jar 파일로 빌드 -> Jar 파일을 EC2로 복사 -> 복사된 파일 실행

 

다음 시간에는 Nginx를 이용해서 EC2에 연결할 수 있도록 배포를 완성하고 추가적으로 도메인을 구매해서 도메인명으로 접속하는 것과 HTTPS를 구현하는 과정을 추가하겠습니다.

 

Reference:

유튜브 - 드림코딩(CI/CD 5분 개념 정리) 🔗

'프로젝트' 카테고리의 다른 글

Docker에 대해서 알아보자 - 1.Docker의 개념과 생태계  (0) 2025.04.18
배포의 모든 것 - 4. 도메인 연결과 HTTPS 적용하기  (0) 2025.04.03
배포의 모든 것 - 2. RDS와 Session Manager  (0) 2025.02.25
배포의 모든 것 - 1. AWS 시작하기 및 EC2 띄우기  (5) 2025.01.25
Restful API Endpoint를 어떻게 설계해야 할까?  (1) 2025.01.14
'프로젝트' 카테고리의 다른 글
  • Docker에 대해서 알아보자 - 1.Docker의 개념과 생태계
  • 배포의 모든 것 - 4. 도메인 연결과 HTTPS 적용하기
  • 배포의 모든 것 - 2. RDS와 Session Manager
  • 배포의 모든 것 - 1. AWS 시작하기 및 EC2 띄우기
potato-farm
potato-farm
개발 혼자 공부하기
  • potato-farm
    감자밭
    potato-farm
  • 전체
    오늘
    어제
    • 분류 전체보기 (27)
      • ETC (2)
      • 알고리즘 (0)
      • Java (0)
      • DB (2)
      • Spring (0)
      • 프로젝트 (15)
      • Server (3)
      • CS (0)
        • 운영체제 (0)
      • Infra (4)
        • IAC (1)
        • AWS (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • hELLO· Designed By정상우.v4.10.3
potato-farm
배포의 모든 것 - 3. Github Action을 통한 CI/CD
상단으로

티스토리툴바