Docker CI/CD의 필요성
기존 프로젝트에서는 각 마이크로서비스를 도커 이미지로 관리하고 있었다
하지만, 각 서비스를 이미지화할 때 이 작업을 수작업으로 해야하는 문제점이 있었고 이는 번거로움으로 이어졌다.
따라서 이번 프로젝트에서는 master 브랜치에 작업 내용을 push했을 때, 이미지가 자동으로 Docker hub로 업로드되도록 CI/CD를 구축해볼 예정이다.
CI/CD의 기초 개념
그렇다면 CI/CD가 도대체 무엇일까?
CI/CD를 구축하기 위해서 이 부분에 대해서 정확하게 알고 넘어갈 필요가 있다.
CI 기본 개념
CI는 지속적 통합의 약자로, 코드 변경 사항을 공유 분기(브랜치)로 빈번하게 병합하는 것을 용이하게 하는 프로세스이다.
CI는 코드 변경 사항을 테스트, 빌드 등을 자동으로 하는 것을 의미한다고 보면 쉽다.
만약 코드를 병합하는 날을 정해두고 모든 분기를 병합했을 때, conflict가 발생한 상황에 대해서 생각해보자.
엄청나게 많은 충돌을 해결하는데 오랜 시간이 걸릴 것이고, 이 문제를 코드 병합하는 날에만 확인할 수 있으니 전체적인 생산성이 줄어든다.
CI는 변경사항을 병합한 후, 변경사항이 애플리케이션을 손상시키지 않도록 자동으로 애플리케이션을 빌드 및 테스트할 수 있다.
따라서 충돌이 발생해도 해당 버그를 빠르게, 그리고 자주 수정하기 용이하다.
CD 기본 개념
CD는 지속적 전달, 혹은 지속적 배포를 의미한다.
CD를 통해서 새로운 기능과 버그 수정 사항을 빠르게 사용자에게 제공할 수 있다.
지속적 전달은 github 또는 컨테이너 레지스트리로 자동 업로드하는 등 코드가 프로덕트 환경에서 배포될 준비를 하는 것이다.
지속적 배포는 이러한 코드를 실제 프로덕션 환경에 배포하는 것을 의미한다.
따라서 Docker hub로의 업로드는 지속적 전달에 속한다는 것을 알 수 있다.
CD는 CI와 밀접한 관계를 갖는다.
CI에서는 코드를 테스트 및 빌드하고, CD를 통해 이 결과물을 컨테이너 레지스트리에 업로드 및 실제 배포하는 과정을 거친다.
CI/CD를 위한 도구 비교
우리는 여러 도구들을 활용하여 CI/CD를 구축할 수 있다.
이번에는 각 도구들에 대해서 비교해보고, 최종적으로 도구를 결정해본다.
비교 대상은 git action과 jenkins이다. 둘다 강력한 도구이지만, 아래와 같이 비교했다.
| git action (무료) | jenkins | |
| 타입 | github SaaS 기반 도구 | 사용자가 직접 서버를 생성함 |
| 병렬 처리 | 병렬 처리 가능 (1~2개 노드) | 서버 성능에 따라 무제한 병렬 처리 가능 |
| 빌드 속도 | github runner 기반의 빠른 빌드 속도 | jenkins 서버 성능에 따라 다름 |
| 자원 사용량 | github SaaS 기반 관리 | 사용자 서버에서 직접 관리 |
| 플러그인 관리 | github 제공 플러그인 기반 간단한 관리 | 사용자가 플러그인 버전을 직접 관리 |
| 플러그인 설치 | 설치 속도는 빠르지만, 재사용은 느림 | 설치 속도는 느리지만, 재사용은 빠름 |
결론
jenkins는 강력한 도구임은 확실하다.
서버의 성능만 충분하다면, 병렬 처리를 통해 높은 성능의 CI/CD를 구축할 수 있다.
하지만, jenkins 서버 관리를 사용자가 직접 해야하지만, git action은 버그 및 서버 관리를 github가 담당한다.
또한, 현재 jenkins에 충분한 서버 자원을 할당할 수 없기에 기대했던 성능에 도달하기 어려울 것이라고 판단했다.
따라서 jenkins가 아닌 git action을 사용하기로 결정했다.
git action 기반의 Docker CI/CD 구축
간단한 CI/CD 파일 생성
github는 .github/workflows에 있는 yml 파일을 자동으로 인식하여 git action을 실행한다
따라서 아래와 같은 파일을 해당 디렉토리에 추가했다.
name: Build and Push Services
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
service: [auth-service, user-service, verification-service, gateway-service, music-service, discovery-service]
steps:
# 현재 리포지토리 코드를 git action 실행 환경으로 받아옴
- uses: actions/checkout@v3
# Java 17 설치
- name: Set Up Java 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
# Gradle 설정
- name: Gradle Setup
uses: gradle/gradle-build-action@v2
# 서브모듈 포함 체크아웃
- name: Checkout with Submodules
uses: actions/checkout@v3
with:
token: ${{ secrets.ACCESS_TOKEN }}
submodules: true
# 프로젝트 빌드
- name: Build ${{ matrix.service }}
run: |
chmod +x ./gradlew
./gradlew :${{ matrix.service }}:build
# Docker 로그인
- name: Docker Hub Login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Docker 이미지 빌드 및 푸시
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.service }}/Dockerfile
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:latest
이번에는 문법을 분석해보자.
- on : git action이 실행되는 조건 정의
- jobs : 조건이 충족했을 때, 실행할 작업들 정의
- Build : 작업의 이름. 다른 이름을 사용해도 좋다,
- runs-on : 작업이 실행되는 환경을 정의
- strategy : 반복 실행 전략 설정
- matrix.service의 값들을 차례대로 반복하며 작업을 실행함
- 이를 통해서 각 마이크로서비스들을 순회하면서 프로젝트 빌드 및 업로드가 가능하다
- steps : 작업 내부에서 실행할 각 단계를 정의
- name : 단계의 이름을 정의
- uses : 사용할 플러그인을 정의 (github, docker 등에서 플러그인을 제시함)
- with : 전달할 인저 설정
- run : 실행할 셸 명령어 정의
- Build : 작업의 이름. 다른 이름을 사용해도 좋다,
지금은 이 정도만 알아도 충분하다.
다만 현재는 전체 단계를 실행하는데 소요되는 시간이 상당하다.
이 부분을 해결하기 위해서 빌드 캐싱을 진행할 예정이다.
gradle 캐시를 활용하여 CI/CD 최적화
첫 번째로 수행한 캐싱 방법이다.
이 방법은 gradle 빌드를 진행한 후, jar 파일을 Dockerfile 내부로 복사하는 방식으로 진행된다.
이를 위해서 중간에 아래 단계를 추가했다.
# Gradle 캐시 설정
- name: Gradle Build Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
이 방법을 사용했을 때, 첫 실행에서 약 5분이 소요되었다. 나쁘지 않은 속도이다.
하지만, 캐싱이 제대로 되지 않았는지 기대하던 성능이 나오지는 않았다.
Docker 캐시 기반의 CI/CD 최적화
다음으로 시도한 방법은 Docker 캐시를 활용한 방법이다.
Docker 내부에서 gradle 빌드를 진행하고, 빌드 결과인 jar 파일을 복사해서 사용한다.
아래와 같이 Docker 캐싱이 가능하도록 설정을 진행했다.
on:
push:
branches:
- master
jobs:
builds:
runs-on: ubuntu-latest
strategy:
matrix:
service: [auth-service, user-service, verification-service, gateway-service, music-service, discovery-service, audio-service]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: checkout repo with submodule
uses: actions/checkout@v3
with:
token: ${{ secrets.ACCESS_TOKEN }}
submodules: true
- name: Docker Hub Login
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.service }}/Dockerfile
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/${{ matrix.service }}:test
cache-from: type=gha
cache-to: type=gha,mode=max
도커 레이어 기반의 캐싱 방식을 채택했을 때, 시간이 약 3.5분으로 단축된 것으로 확인했다.
결과
gradle 캐싱 방식을 사용했을 때는 약 5분이 소요되었지만, Docker 캐싱 방법을 사용했을 때는 약 3.5분이 소요되었다.
전체적으로 약 30%의 성능 향상을 확인할 수 있었다.
이는 Docker의 캐싱 방식이 gradle의 캐싱 방식과 다르기 때문인 것으로 보인다.
마무리
이번에는 MSA 환경에서 Docker hub CI/CD 시스템을 구축해보았다.
더 나아가 Docker를 활용한 캐싱을 통해 CI/CD 성능을 최적화할 수 있었다.
다만 현재 git submodule을 활용하여 설정파일을 관리하고 있기에 서브 모듈에 변경사항이 생겼을 때, 도커 이미지를 갱신할 수 있는 기능이 추가로 필요할 것으로 보인다.
'Spring > 기초 개념' 카테고리의 다른 글
| [ Spring Boot ] Spring MockMvc 테스트 유지보수 후기 (0) | 2026.02.04 |
|---|---|
| [Spring Boot] Spring MVC + WebClient 환경에서 JPA 사용하기 (0) | 2025.04.09 |
| [Spring Boot] Spring AOP Self Invocation과 @Transactional (0) | 2025.04.07 |
| Spring Data JPA의 영속성 컨텍스트 (0) | 2025.04.02 |
| [Spring Boot] 실시간 비동기 작업 처리기 만들기 - 실습 (0) | 2025.03.26 |