처음부터 끝까지, 실전 DevOps 파이프라인 구축기
왜 DevOps 파이프라인을 직접 구축했을까
도구를 따로따로 써본 적은 많습니다. Jenkins로 빌드 돌리고, Docker로 이미지 말고, 쿠버네티스에 배포하고. 그런데 어느 순간 의문이 들더군요. 이 도구들이 각각 무엇을 하는지는 알겠는데, 서로 어떻게 연결돼서 하나의 흐름이 되는지는 머릿속에서 잘 그려지지 않았습니다.
명령어를 외우는 것과 파이프라인을 이해하는 건 다른 문제입니다. 코드가 푸시되는 순간부터 프로덕션에 떨어지기까지, 그 사이에 몇 개의 관문이 있고 각 관문이 무엇을 막아주는지. 그걸 표면이 아니라 구조로 보고 싶었습니다.
그래서 GitHub부터 Grafana까지, 현업에서 흔히 쓰는 10가지 도구를 한 줄로 엮어봤습니다. 이론으로만 알던 DevSecOps가 실제로 어떻게 맞물려 돌아가는지, 손으로 직접 만져보면서 확인한 기록입니다.
전체 파이프라인 아키텍처
음식 배달 앱에 비유하면 흐름이 한눈에 들어옵니다. 주문(코드 푸시)이 들어오면 주방(CI)에서 요리를 만들고, 위생 검사를 거쳐, 포장(컨테이너화)한 다음, 배달원(CD)이 고객(프로덕션)에게 전달하죠. 중간에 검사 하나라도 실패하면 음식은 손님에게 나가지 않습니다.
파이프라인 흐름:
개발자 → GitHub → Jenkins CI → OWASP 스캔 → SonarQube 분석 → Docker 빌드 → Trivy 보안 스캔 → DockerHub 푸시 → Jenkins CD → GitHub 매니페스트 업데이트 → ArgoCD → Kubernetes 배포 → Prometheus 모니터링 → Grafana 대시보드
각 단계마다 명확한 목적이 있습니다. 그리고 한 단계에서 문제가 생기면 뒤로 넘어가지 않고 전체 파이프라인이 멈추도록 설계했습니다. 이게 **품질 게이트(Quality Gate)**의 핵심입니다. 막아야 할 곳에서 확실히 막는 것. 통과시키면 안 되는 걸 통과시키지 않는 것.
환경 구성과 사전 준비
시스템 요구사항:
| 항목 | 권장 사양 |
|---|---|
| RAM | 16GB 이상 |
| CPU | 4코어 이상 |
| OS | Ubuntu 22.04 LTS |
| 디스크 | 50GB 이상 |
| 네트워크 | 인터넷 연결 필수 |
실제로는 AWS EC2 t3.xlarge 인스턴스에서 구축했습니다. 도구를 하나둘 띄울 때는 여유로워 보이지만, 전부 동시에 돌리면 메모리가 금방 빠듯해집니다. 특히 SonarQube와 Jenkins가 리소스를 꽤 가져가더군요. 학습용이라 해도 RAM은 넉넉하게 잡는 게 정신 건강에 좋습니다.
사전 준비사항:
- Docker 설치 및 권한 설정
sudo apt update && sudo apt install docker.io -y
sudo systemctl start docker && sudo systemctl enable docker
sudo usermod -aG docker $USER
# 로그아웃 후 재로그인 필요
- Kubernetes 환경 구성
sudo snap install kubectl --classic
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube start --driver=docker
- 보안 스캔 도구 설치
# Trivy 설치
wget https://github.com/aquasecurity/trivy/releases/latest/download/trivy_0.50.1_Linux-64bit.deb
sudo dpkg -i trivy_0.50.1_Linux-64bit.deb
CI 파이프라인 구축하기
Jenkins와 SonarQube 연동
Jenkins와 SonarQube는 Docker 컨테이너로 띄웠습니다. 프로덕션이라면 당연히 별도 서버에 올리겠지만, 학습 목적에서는 컨테이너가 훨씬 빠릅니다. 망가지면 지우고 다시 띄우면 그만이니까요.
# Jenkins 실행
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts
# SonarQube 실행
docker run -d \
--name sonarqube \
-p 9000:9000 \
sonarqube:lts-community
핵심 플러그인 설치:
- Docker Pipeline
- SonarQube Scanner
- OWASP Dependency Check
- Kubernetes CLI
- Email Extension
Jenkinsfile 작성의 핵심
실제로 사용한 Jenkinsfile의 핵심 부분입니다:
pipeline {
agent any
environment {
IMAGE_NAME = "${DOCKERHUB_USERNAME}/flask-devops"
SCANNER_HOME = tool 'SonarQube-Scanner'
}
stages {
stage('Code Quality & Security') {
parallel {
stage('OWASP Dependency Check') {
steps {
dependencyCheck additionalArguments: '--scan ./'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'dependency-check-report',
reportFiles: 'dependency-check-report.html'
])
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('SonarQube') {
sh '''$SCANNER_HOME/bin/sonar-scanner \
-Dsonar.projectKey=flask-devops \
-Dsonar.projectName=flask-devops \
-Dsonar.sources=. \
-Dsonar.exclusions=**/node_modules/**'''
}
}
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}
여기서 핵심은 parallel 블록입니다. OWASP 스캔과 SonarQube 분석을 동시에 돌리죠. 시간도 줄지만, 그보다 중요한 건 두 검사가 서로 독립적으로 작동한다는 점입니다. 하나가 느리다고 다른 하나가 발목 잡히지 않습니다.
그리고 waitForQualityGate abortPipeline: true. 이 한 줄이 사실상 파이프라인의 성격을 결정합니다. 품질 기준을 통과하지 못하면 그 자리에서 멈춥니다. 빌드는 됐는데 품질은 미달인 코드를 다음 단계로 흘려보내지 않겠다는 선언이죠.
CD 파이프라인과 GitOps
ArgoCD로 구현하는 GitOps
CD 쪽에서는 GitOps 패턴을 적용했습니다. Jenkins가 쿠버네티스에 직접 kubectl apply를 날리는 대신, Git 저장소의 매니페스트만 업데이트합니다. 그러면 ArgoCD가 변경을 감지해서 클러스터 상태를 Git에 맞춰 동기화하죠.
이 구조가 왜 중요하냐면, 배포의 기준점이 'Jenkins가 무엇을 했는가'에서 'Git에 무엇이 적혀 있는가'로 옮겨가기 때문입니다. 클러스터의 현재 상태가 Git과 다르면 ArgoCD가 그 차이를 메웁니다. 운영 중에 누가 손으로 바꿔놓은 것도 결국 Git 기준으로 되돌아오고요. 장애 상황에서 '지금 떠 있는 게 대체 무슨 버전이냐'를 추적할 때, 이 단일 기준점이 생각보다 큰 차이를 만듭니다.
# ArgoCD 설치
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 초기 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
Kubernetes 매니페스트 예시:
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
spec:
replicas: 2
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
spec:
containers:
- name: flask-app
image: yourusername/flask-devops:latest
ports:
- containerPort: 5000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
자동 이미지 태그 업데이트
Jenkins CD 파이프라인은 새 이미지가 빌드될 때마다 Git 저장소의 매니페스트를 자동으로 고쳐 씁니다:
stage('Update Manifest') {
steps {
script {
sh '''
git config user.email "jenkins@example.com"
git config user.name "Jenkins"
sed -i "s|image: .*|image: ${IMAGE_NAME}:${BUILD_NUMBER}|g" k8s/deployment.yaml
git add k8s/deployment.yaml
git commit -m "Update image to ${BUILD_NUMBER}"
git push origin main
'''
}
}
}
한 가지 짚고 넘어가면, 여기서 latest 태그 대신 ${BUILD_NUMBER}로 고정 태그를 박는 게 포인트입니다. latest는 편하지만 '지금 떠 있는 게 어느 빌드냐'를 흐려버립니다. GitOps의 추적 가능성을 살리려면 이미지 태그도 불변이어야 하죠.

모니터링과 관찰 가능성
Prometheus와 Grafana 스택
모니터링 스택은 Helm으로 설치했습니다. kube-prometheus-stack은 Prometheus, Grafana, AlertManager, 그리고 각종 exporter를 한 번에 깔아주는 차트라 초기 구성 부담이 확 줄어듭니다.
# Helm 설치
sudo snap install helm --classic
# 차트 저장소 추가
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# 모니터링 네임스페이스 생성
kubectl create namespace monitoring
# Prometheus 스택 설치
helm install prometheus prometheus-community/kube-prometheus-stack -n monitoring
실용적인 알람 설정
대시보드는 예쁘게 떠 있어도 아무도 안 보고 있으면 의미가 없습니다. 결국 중요한 건 '문제가 생겼을 때 사람을 깨우는' 알람이죠. 그래서 Grafana에 다음 규칙들을 걸어뒀습니다:
주요 알람 규칙:
- Pod 재시작 횟수 임계치 초과
- 메모리 사용률 80% 이상
- HTTP 응답 시간 5초 초과
- 애플리케이션 가용성 95% 미만
Slack과 이메일 양쪽으로 알림을 보냈습니다. 다만 운영을 해본 사람이라면 알겠지만, 알람은 '많이 보내는 것'보다 '꼭 필요할 때만 보내는 것'이 어렵습니다. 임계치를 너무 낮게 잡으면 알람 피로(alert fatigue)가 쌓이고, 결국 진짜 장애 알람도 무시하게 되니까요. 이번엔 학습용이라 임계치를 단순하게 잡았지만, 실서비스라면 이 부분을 가장 오래 다듬게 됩니다.
보안과 품질 관리
다층 보안 스캔
이번 파이프라인에서 가장 공을 들인 부분이 보안입니다. shift-left, 즉 보안 검증을 가능한 한 앞단으로 당겨서 개발 초기부터 막도록 구성했습니다. 문제를 프로덕션에서 발견하는 것과 PR 단계에서 발견하는 것은 비용이 자릿수로 다릅니다.
- OWASP Dependency Check: 라이브러리 취약점 스캔
- SonarQube: 코드 품질 및 보안 이슈 분석
- Trivy: 컨테이너 이미지 취약점 스캔
- Kubernetes 보안 정책: Pod Security Standards 적용
서로 다른 레이어를 다른 도구가 본다는 게 핵심입니다. 소스 코드, 의존성, 컨테이너 이미지, 런타임 정책. 어느 한 층만 검사해서는 빈틈이 생깁니다. 특히 Trivy가 인상적이었는데, 컨테이너 이미지의 OS 패키지부터 애플리케이션 의존성까지 전 레이어를 훑어서 CVE 정보와 함께 보고서를 뽑아주더군요. 내 코드는 깨끗해도 베이스 이미지가 취약하면 결국 뚫린다는 걸 시각적으로 보여줍니다.
SonarQube Quality Gate
SonarQube에는 다음 조건으로 품질 게이트를 걸었습니다:
| 지표 | 임계값 |
|---|---|
| 코드 커버리지 | 80% 이상 |
| 중복 코드 | 3% 이하 |
| 보안 취약점 | 0개 |
| 코드 스멜 | 10개 이하 |
| 기술 부채 비율 | 5% 이하 |
숫자 자체보다, 이 기준을 파이프라인이 강제한다는 점이 중요합니다. 사람의 의지에 맡기면 바쁠 때 가장 먼저 무너지는 게 품질 기준이거든요. 게이트로 박아두면 적어도 협상의 여지가 없어집니다.

배운 점과 개선 방향
구축 과정에서 가장 까다로웠던 건 의외로 도구 자체가 아니라 도구들 사이의 권한 관리와 네트워크 설정이었습니다. Jenkins가 Kubernetes API에 접근하거나, ArgoCD가 private Git 저장소를 붙잡을 때, 인증 설정에서 시간을 가장 많이 까먹었습니다. 도구를 띄우는 건 명령어 몇 줄이지만, '누가 무엇에 접근할 수 있는가'를 제대로 잠그는 건 늘 그보다 어렵습니다.
실제 프로덕션이라면 이런 점들을 더 손보고 싶습니다:
- Infrastructure as Code: Terraform으로 전체 인프라 관리
- Secret 관리: HashiCorp Vault 또는 AWS Secrets Manager 도입
- Multi-cluster 환경: 개발/스테이징/프로덕션 환경 분리
- 백업 전략: ETCD 백업 및 재해복구 계획
- 성능 최적화: 빌드 캐시, 이미지 레이어 최적화
특히 보안 쪽에서는 **RBAC(Role-Based Access Control)**을 더 촘촘하게 나누고, 네트워크 정책으로 Pod 간 통신을 최소 권한으로 제한하는 작업이 남았다고 봅니다. 학습 환경에서는 '일단 통신 열어두고 보자'가 가능하지만, 실서비스에서는 그게 곧 공격 표면이 되니까요.
직접 손으로 끝까지 엮어보고 나서 든 생각은, DevOps는 결국 도구의 조합이 아니라는 겁니다. 같은 10개 도구를 똑같이 깔아도, 팀이 어떻게 협업하고 장애에 어떻게 대응하느냐에 따라 완전히 다른 결과가 나옵니다. 파이프라인은 그 문화를 강제하는 장치에 가깝죠. 품질 게이트도, GitOps도, 결국은 '사람의 의지에 기대지 않고도 기준이 지켜지게 만드는' 구조적 장치입니다.
자동화된 파이프라인이 있어도 그 위에서 일하는 사람들의 합의가 없으면, 게이트는 언젠가 abortPipeline: false로 바뀝니다. 도구가 아니라 그 합의를 지키는 게 진짜 DevOps에 가깝다고 생각합니다.