| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- netty
- 백엔드개발
- helm
- NIO
- redis
- monitoring
- 성능최적화
- 백엔드
- JPA
- kafka
- grafana
- Kotlin
- mysql
- 데이터베이스
- 성능 최적화
- 트랜잭션
- docker
- spring boot
- webflux
- Java
- jvm
- DevOps
- GitOps
- RDBMS
- Kubernetes
- prometheus
- 동시성제어
- selector
- CloudNative
- SpringBoot
- Today
- Total
유성
정답 없는 CI/CD, 우리 환경에 맞는 최적의 파이프라인 설계 본문
이 글에서는 단순한 코드 전송을 넘어, 복잡한 환경에서 어떻게 안정적이고 효율적인 CI/CD 파이프라인을 구축했는지 그 설계 과정을 공유한다.
1. CI/CD란 무엇인가?: 규모에 따른 변칙적 대응
CI/CD는 배포 파이프라인으로, 프로젝트의 규모와 팀의 운영 성격에 따라 매우 유연한 구조를 가진다.
필자는 이를 크게 두 가지 배포전략으로 구분한다.
- 작고 빠른 배포: main 브랜치에 코드가 병합되는 즉시 빌드부터 실서버 배포까지 자동화되는 방식이다. 빠른 피드백이 중요한 초기 프로젝트에 적합하다.
- 큰고 신중한 배포: 병합 시 빌드와 검증은 자동으로 수행되며, 실제 배포는 관리자의 수동 트리거를 통해 결정하는 방식이다. 안정성이 최우선인 대규모 시스템에서 주로 채택한다.
이 두 방식 모두 자동화된 파이프라인을 통해 소프트웨어의 품질을 높이는 CI/CD의 본질을 담고 있으며, 밸런스를 어디에 두는지 각 서비스의 특성에 따라 다를것이다.
2. CI 단계: 품질을 지키는 '게이트키퍼'
CI(Continuous Integration)는 코드의 품질과 비즈니스 로직을 검증하는 빌드 및 테스트 단계이다.
테스트가 실패하면 배포 단계로 넘어가지 않도록 차단하는 '게이트키퍼' 역할을 수행한다.
GitHub Actions를 기준으로 이 흐름을 세분화하면 다음과 같다.
- 코드 병합: 개발자가 로컬에서 작업한 코드를 GitHub 저장소에 push 하거나 PR을 병합하는 단계이다.
- 빌드 트리거: GitHub 서버가 코드의 변화를 감지하고 이벤트를 발행한다. 조건이 맞는 경우 설정된 workflow 파일에 따라 파이프라인 가동이 시작된다.
- 빌드 및 테스트: 지정된 Runner Agent 환경에서 코드를 불러와 컴파일하고 테스트를 실행한다.
3. Runner Agent의 선택과 CI 스크립트
CI 단계를 수행할 때 고민해야 할 부분중 하나는 "어떤 컴퓨터에서 실행할 것인가"이다.
- GitHub-hosted Runner: GitHub에서 제공하는 클라우드 서버로, Docker가 기본으로 설치되어있어 RDBMS나 NoSQL 같은 인프라리를 즉시 실행하여 테스트하기 편리하다. (무료 버전은 좀 많이 느리다)
- Self-hosted Runner: 사용자가 직접 관리하는 서버를 러너로 사용한다. 내부 네트워크 자원에 접근해야 하거나, 고사양의 빌드 자원이 필요할 때 효과적이다.
이후 추가로 설명을 하겠지만, 빌드 시간을 단축하기 위해 맥북 self-hosted 방식으로 실행을 한다.
빌드 스크립트
name: Production Deployment
on:
push:
branches: [ "main" ]
concurrency:
group: production-deploy
cancel-in-progress: true
jobs:
build-and-test:
runs-on: self-hosted
steps:
- name: Checkout code
uses: actions/checkout@v4
# 1. Java 환경 설정
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
# 2. 프로젝트 빌드
- name: Build with Gradle
run: ./gradlew build
테스트는 MySQL TestContainer를 띄우는 과정이 있어, Runner OS(Mac)에는 docker를 설치했다.
Note: 테스트 시 인메모리 DB를 사용할 수 있지만, 실제 환경과의 격차를 줄이기 위해 TestContainers를 활용해 PROD 환경과 동일한 인프라 사용하는 방식을 개인적으로 매우매우 권장한다.

내 컴퓨터에 있는 Runner Agent도 정상 수행로그가 찍혀있다.
2026-01-23 06:52:10Z: Running job: build-and-test
2026-01-23 06:53:55Z: Job build-and-test completed with result: Succeeded
빌드된 파일은 Runner Agent를 실행하는 컴퓨터(서버)에 저장되고 정상 저장을 확인할 수 있다.

4. CD 단계: 검증된 코드를 공개하는 과정
CI 단계에서 코드의 무결성을 검증했다면, 이제는 그 결과물을 실제 운영 환경으로 전달해야 한다.
CD 단계는 크게 세 가지 과정으로 나뉜다.
- 빌드 파일 패키징: 전 단계에서 생성된 .jar 파일을 가벼운 Docker 이미지로 패키징한다.
- 이미지 업로드: 배포 서버가 언제든 접근할 수 있도록 Docker Hub와 같은 컨테이너 레지스트리에 저장한다.
- 배포: 이미지를 불러와 기존 서버를 새로운 버전으로 업데이트한다.
이 과정은 설정에 따라 코드 푸시 시 자동 실행될 수도 있고, 안정성을 위해 관리자가 버튼을 눌러 수동으로 실행하거나 특정 시간대에 맞춰 동작하도록 스케줄링할 수도 있다.
스케줄링을 했다면 해당 시간에 맞춰 작업자는 정상 배포되는지 모니터링만 진행한되면 된다.
5. 전략적 선택: 왜 Self-hosted Runner인가?
이번 프로젝트에서 필자는 GitHub-hosted Runner 대신 Self-hosted Runner를 선택했다.
이는 네트워크 보안과 인프라 구조 때문이다.
- 네트워크 제약: 배포 대상 서버들이 MacOS 내 가상 머신(VM)에 설치되어 있어 외부(GitHub 서버)에서 내부로 접속(Inbound)하기 위해선 복잡한 방화벽 설정이나 터널링이 필요로 한다.
- 보안과 주도권: Self-hosted Runner는 내 Mac에서 GitHub 서버로 접속하여 작업을 가져오는 Outbound 방식을 사용한다. 덕분에 외부로 포트를 개방할 필요 없이, 내부 k8s 클러스터에 대한 배포 주도권을 안전하게 유지할 수 있다.
배포 자동화 스크립트 (YAML)
CI 단계의 성공 여부를 확인한 뒤(needs: build-and-test), 본격적인 배포를 수행하는 코드이다.
~~~~~~~~~~~~~~~~~~~~
deploy: # CD 단계
needs: build-and-test # 빌드가 성공해야 실행됨
runs-on: self-hosted
steps:
# 1. Docker Hub 로그인
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 2. Docker 이미지 패키징 및 푸시
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/demo11:latest
${{ secrets.DOCKERHUB_USERNAME }}/demo11:${{ github.sha }}
# 3. kubectl 설치 및 k8s 컨텍스트 설정
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Set up Kubeconfig
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG }}
# 4. Kubernetes 실제 배포 수행
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/spring-boot-app spring-boot-container=${{ secrets.DOCKERHUB_USERNAME }}/demo11:${{ github.sha }}
kubectl rollout status deployment/spring-boot-app
최적화된 패키징용 Dockerfile
이미 CI 단계에서 빌드가 완료되었으므로, Dockerfile은 빌드 기능 없이 실행 환경만 갖춘 가벼운 구조를 가진다.
FROM amazoncorretto:17-alpine-jdk
WORKDIR /app
# CI 단계에서 생성된 jar 파일만 복사
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
이미지 태그에는 커밋 SHA를 사용하여, 어떤 소스 코드가 배포되었는지 이력을 1:1로 매칭할 수 있도록 설계했다.
6. 배포 결과 확인: 모니터링

파이프라인이 종료된 후, 실제 Kubernetes Master 노드에 접속해 배포 상태를 확인해보았다.
특히 커밋 해시(SHA)를 태그로 사용했기 때문에, 현재 서버에 떠 있는 코드가 어떤 소스 코드 지점인지 명확하게 추적할 수 있다.
이미지 태그 확인
ubuntu@k8s-master:~$ sudo kubectl describe deployment spring-boot-app | grep Image
Image: ******/demo11:895359e80fa55d091f43d9d40634ef5199cff200
Pod 배포 상태 확인
ubuntu@k8s-master:~$ sudo kubectl get pod -l app=spring-boot -o wide
NAME READY STATUS RESTARTS AGE IP NODE
spring-boot-app-7f84ffbf9c-4n7ft 1/1 Running 0 23m 10.42.3.44 k8s-worker2
spring-boot-app-7f84ffbf9c-tbqd8 1/1 Running 0 24m 10.42.2.48 k8s-worker1
배포가 정상적으로 수행되어 설정한 레플리카 개수만큼 각 워커 노드에 고르게 분배되었다.
이로써 코드 변경부터 실제 배포까지의 모든 과정이 사람의 개입 없이 자동화된것이다.
7. 마치며
CI/CD란 결국 '코드가 사용자에게 전달되는 파이프라인'을 의미한다. 이 파이프라인의 형태에는 정해진 정답이 없으며, 조직의 규모와 인프라 환경에 따라 무궁무진하게 변할 수 있다.
화려한 도구를 써야만 CI/CD로 생각할 수 있는데, 기술의 화려함보다 중요한 것은 '우리 환경에 맞는 자동화 수준'이다.
- 온프레미스 환경에서 미들웨어 팀이 Apache의 생명주기를 엄격히 관리해야 한다면, 빌드 후 특정 디렉토리에 .war파일을 안전하게 떨어뜨려 주는 것까지만 구현해도 훌륭한 CI/CD이다.
- 반면, 클라우드 네이티브 환경이라면 이번 글처럼 컨테이너라이징과 오케스트레이션 도구를 활용해 롤링 업데이트까지 자동화하는 것이 좀 더 좋은 방법일 것이다.
결국 CI/CD의 본질은 도구의 사용이 아니라, "반복되는 수동 작업을 제거하여 휴먼 에러를 줄이고, 개발자가 비즈니스 로직에만 집중할 수 있는 환경을 만드는 것"에 있다.
'DevOps' 카테고리의 다른 글
| Kubernetes 기반 PostgreSQL 고가용성(HA) 아키텍처 구축 및 카오스 검증 (0) | 2026.02.01 |
|---|---|
| GitHub Actions와 ArgoCD로 완성하는 선언적 쿠버네티스 배포 시스템 (0) | 2026.01.29 |
| Kubernetes에서 MySQL 마스터-슬레이브(Replication) 구축하기 (0) | 2026.01.21 |
| Linux Page Cache와 Linux OS의 메모리 사용 전략 (0) | 2025.12.09 |
| 랜섬웨어 공격 경험 기록: Docker 설정이 만든 보안 구멍 (1) | 2025.08.21 |