DevOpsDevOps 학습 로드맵 · 3/9

FastAPI 서비스 골격, RabbitMQ DLQ, Harbor+Trivy, Jenkins→ArgoCD 파이프라인 구축 절차

메시지 큐·CI/CD 개념부터, 헬스체크/메트릭을 표준화한 api-server·inference-worker 골격, RabbitMQ ack/nack+DLQ, Harbor 사설 레지스트리+Trivy 스캔, Jenkins 빌드와 ArgoCD GitOps 배포, 선택 확장인 CloudNativePG까지.

2026-07-3113 min read
#FastAPI#RabbitMQ#Jenkins#ArgoCD#GitOps#Harbor

목표

api-server(FastAPI)와 inference-worker(비동기 워커) 두 서비스를 최소 골격으로 만들고, 둘 사이를 RabbitMQ로 연결한다. 그 다음 Jenkins로 빌드하고, Harbor 사설 레지스트리에 Trivy로 스캔한 이미지를 올리고, ArgoCD로 배포하는 파이프라인까지 연결한다. (선택 확장으로 PostgreSQL을 CloudNativePG 오퍼레이터로 운영하는 것도 다룬다.)

실제 구축 절차

1. api-server 골격 - 헬스체크/메트릭 표준화

Kubernetes는 파드가 "살아있는지"와 "트래픽을 받을 준비가 됐는지"를 각각 다른 방식으로 확인한다 - 프로세스가 떠 있어도 DB 연결이 안 되어 있으면 트래픽을 받으면 안 되기 때문이다. 이 확인 방식을 서비스마다 다르게 만들면 게이트웨이 라우팅과 모니터링 양쪽에 태울 수 없으니, 엔드포인트 형식을 먼저 고정한다.

# api-server/app/main.py
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()
Instrumentator().instrument(app).expose(app, endpoint="/metrics")

@app.get("/healthz")
def healthz():
    return {"status": "ok"}

@app.post("/jobs")
def create_job(payload: JobCreate):
    job_id = save_job_as_pending(payload)
    publish_to_queue("jobs.upload", {"job_id": job_id, **payload.dict()})
    return {"job_id": job_id, "status": "PENDING"}

@app.get("/jobs/{job_id}")
def get_job(job_id: str):
    return get_job_status(job_id)

/healthz는 프로세스 생존만 확인하고, DB·큐 연결까지 확인하는 /readyz를 별도로 두는 편이 낫다 - /healthz가 무거우면 kubelet의 liveness probe가 불필요하게 서비스를 재시작시킬 수 있다.

2. RabbitMQ 배포 - StatefulSet + ack/nack + DLQ

메시지 큐란 무엇이고 왜 쓰는가: api-server가 이미지 업로드 요청을 받자마자 AI 추론까지 끝내고 응답하면 사용자는 몇 분을 기다려야 한다. 대신 "작업 접수"만 즉시 응답하고, 실제 처리는 별도 워커가 나중에 하도록 미루는 게 비동기 처리다. 이때 api-serverinference-worker가 서로를 직접 호출하면, 워커가 잠깐이라도 다운된 순간의 요청은 그냥 사라진다. 메시지 큐는 그 사이에서 "작업이 접수됐다"는 사실 자체를 저장해두는 중간 저장소다 - 워커가 나중에 다시 살아나도 큐에 쌓인 작업을 그대로 이어받을 수 있다.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq \
  --set auth.username=app \
  --set auth.password=<시크릿으로 관리> \
  --set replicaCount=1

ack/nack이란: 큐는 메시지를 워커에게 전달한 것만으로 "처리 완료"라고 여기지 않는다. 워커가 실제로 작업을 끝낸 뒤 명시적으로 "ack(확인)"을 보내야 큐에서 그 메시지가 지워진다. 워커가 처리 도중 죽어서 ack를 못 보내면, 큐는 그 메시지를 다른 워커에게 다시 전달한다 - 이게 "작업이 사라지지 않는다"는 보장의 실체다. nack은 "실패했다"는 명시적 신호이고, 반복 실패한 메시지를 **DLQ(Dead Letter Queue)**라는 별도 큐로 격리해 무한 재시도를 막는다.

# inference-worker/app/queue_setup.py
import pika

def declare_queues(channel):
    channel.exchange_declare("jobs.dlx", exchange_type="direct")
    channel.queue_declare("jobs.dlq", durable=True)
    channel.queue_bind("jobs.dlq", "jobs.dlx", routing_key="jobs.upload")

    channel.queue_declare(
        "jobs.upload",
        durable=True,
        arguments={
            "x-dead-letter-exchange": "jobs.dlx",
            "x-dead-letter-routing-key": "jobs.upload",
        },
    )
# inference-worker/app/consumer.py
def on_message(channel, method, properties, body):
    headers = properties.headers or {}
    death_count = _retry_count(headers)  # x-death 배열 길이로 계산
    try:
        run_inference(body)
        channel.basic_ack(delivery_tag=method.delivery_tag)
    except Exception:
        if death_count >= 3:
            channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)  # DLQ로
        else:
            channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

inference-worker는 처음엔 실제 ONNX 모델 대신 time.sleep(180)으로 "오래 걸리는 작업"만 흉내 낸다 - 파이프라인 자체(큐 소비, ack/nack, 상태 갱신)가 먼저 제대로 도는지 검증하는 게 우선이다.

3. 검증 실험 설계 - 워커 강제 종료

# 작업 하나를 등록한 뒤, 처리 중인 워커 파드를 강제 종료
kubectl delete pod <inference-worker-pod> --grace-period=0 --force

# 확인할 것
# 1) 메시지가 ack 안 된 상태로 큐에 남아있다가 재전달되는지 (RabbitMQ Management UI의 Unacked 카운트)
# 2) 재시작된 워커(또는 다른 레플리카)가 같은 메시지를 다시 처리하는지
# 3) 의도적으로 3회 실패하는 메시지를 만들어 DLQ로 격리되는지 (jobs.dlq 큐 확인)

4. Harbor 사설 레지스트리 + Trivy 스캔

컨테이너 레지스트리란: docker build로 만든 이미지는 그 자체로는 클러스터의 다른 노드에서 쓸 수 없다 - 이미지를 어딘가에 올려두고, 각 노드가 그곳에서 내려받도록 해야 한다. Docker Hub 같은 공개 레지스트리를 그대로 써도 되지만, 사내 코드가 담긴 이미지를 외부에 공개하고 싶지 않다면 직접 운영하는 사설 레지스트리가 필요하다. Harbor는 그 사설 레지스트리 구현체이고, 이미지를 푸시할 때 자동으로 취약점을 스캔하는 Trivy가 내장되어 있다.

helm repo add harbor https://helm.goharbor.io
helm install harbor harbor/harbor \
  --set expose.type=ingress \
  --set expose.ingress.hosts.core=harbor.local \
  --set externalURL=https://harbor.local \
  --set trivy.enabled=true

Harbor 프로젝트(네임스페이스 개념)를 하나 만들고, 그 프로젝트에 "취약점이 발견되면 배포 차단" 정책을 건다.

# Harbor 프로젝트별 정책 - Critical/High 취약점 발견 시 pull 자체를 차단
curl -X PUT "https://harbor.local/api/v2.0/projects/app/scanner" \
  -u admin:<password> \
  -d '{"vulnerability_prevent_pull": true, "severity": "high"}'

이 정책이 바로 ISMS-P 2.10(시스템·서비스 보안관리)에서 요구하는 "취약한 이미지의 배포를 막는 절차"에 대한 증적이 된다.

5. Jenkins - 빌드 파이프라인

CI(지속적 통합)란: 코드를 커밋할 때마다 사람이 수동으로 빌드·테스트하는 대신, 그 과정을 자동으로 실행해주는 것이다. Jenkins는 그 자동화를 실행하는 도구 중 하나로, Git 저장소의 변경을 감지해 정의된 파이프라인(빌드→테스트→이미지 생성)을 순서대로 실행한다.

// Jenkinsfile
pipeline {
  agent any
  stages {
    stage('Test') {
      steps { sh 'pytest' }
    }
    stage('Build') {
      steps {
        sh 'docker build -t harbor.local/app/api-server:${GIT_COMMIT} ./api-server'
      }
    }
    stage('Push to Harbor') {
      steps {
        sh '''
          docker login harbor.local -u $HARBOR_USER -p $HARBOR_PASS
          docker push harbor.local/app/api-server:${GIT_COMMIT}
        '''
      }
    }
    stage('Wait for Trivy Scan') {
      steps {
        // Harbor가 푸시된 이미지를 자동 스캔 - 결과가 나올 때까지 폴링
        sh '''
          harbor-cli scan wait --project app --repo api-server --tag ${GIT_COMMIT}
        '''
      }
    }
    stage('Update Manifest') {
      steps {
        sh '''
          cd k8s-manifests
          kustomize edit set image api-server=harbor.local/app/api-server:${GIT_COMMIT}
          git commit -am "deploy: api-server ${GIT_COMMIT}"
          git push
        '''
      }
    }
  }
}

Wait for Trivy Scan 스테이지가 Harbor 도입으로 추가된 부분이다 - Critical 취약점이 있으면 여기서 파이프라인이 실패하고, 매니페스트 갱신(배포 트리거)까지 진행되지 않는다. 마지막 스테이지가 핵심 - Jenkins가 클러스터에 직접 배포하지 않고, 매니페스트 저장소에 이미지 태그만 갱신해서 커밋한다. 실제 배포는 ArgoCD가 담당한다.

6. ArgoCD - GitOps 배포

CD(지속적 배포)와 GitOps란: CI가 "이미지를 만드는 것"까지라면, CD는 "그 이미지를 실제 클러스터에 반영하는 것"이다. Jenkins가 클러스터에 직접 kubectl apply를 실행하는 방식(push 배포)도 가능하지만, 그러면 "지금 클러스터에 뭐가 떠 있는지"의 유일한 근거가 Jenkins의 실행 로그뿐이게 된다. GitOps는 발상을 뒤집는다 - Git 저장소에 선언된 상태를 "정답"으로 삼고, ArgoCD가 그 정답과 클러스터의 실제 상태를 계속 비교하며 다르면 자동으로 맞춘다.

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-server
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.local/k8s-manifests.git
    targetRevision: main
    path: api-server
  destination:
    server: https://kubernetes.default.svc
    namespace: app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true    # 클러스터 상태가 Git과 달라지면 자동으로 되돌림

selfHeal: true가 push 배포와의 가장 큰 차이다 - 누군가 클러스터에서 리소스를 수동으로 고쳐도 ArgoCD가 Git 상태로 되돌린다.

검증 방법

argocd app get api-server           # Sync/Health 상태 확인
argocd app history api-server       # 배포 이력이 Git 커밋과 1:1로 매칭되는지 확인

# drift 감지 테스트
kubectl scale deployment api-server --replicas=5   # 수동으로 클러스터 상태 변경
argocd app get api-server           # OutOfSync로 표시되는지, selfHeal로 원래 replica 수로 돌아오는지 확인

# Harbor 취약점 차단 테스트
# 알려진 취약점이 있는 오래된 베이스 이미지로 빌드해서 푸시 → Wait for Trivy Scan 단계에서 파이프라인이 실패하는지 확인

배포 소요 시간은 git push 시점부터 argocd app getSynced/Healthy가 되는 시점까지를 재서 기록해둔다 - Jenkins push 배포 방식과 비교할 수치 기준이 된다.

(선택 확장) CloudNativePG로 PostgreSQL 운영

개념: PostgreSQL을 그냥 파드 하나로 띄우면, 그 파드가 죽었을 때 자동으로 복구해주는 것도, 백업을 챙겨주는 것도 없다 - 전부 사람이 직접 만들어야 한다. CloudNativePG는 PostgreSQL 전용 Kubernetes Operator다. Operator란 "사람이 반복적으로 하던 운영 작업(장애 감지·failover 판단·백업 실행)을 대신 수행하는 컨트롤러"를 뜻한다 - Cluster라는 리소스 하나를 선언하면 그 뒤의 운영을 오퍼레이터가 대신한다.

kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.24.0.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: app-postgres
spec:
  instances: 3          # 프라이머리 1 + 레플리카 2
  storage:
    size: 20Gi
  backup:
    retentionPolicy: "7d"

instances: 3으로 선언하는 것만으로 프라이머리가 죽었을 때 레플리카 중 하나를 자동으로 승격시키는 failover가 구성된다.

# failover 검증
kubectl cnpg status app-postgres          # 현재 프라이머리 확인
kubectl delete pod app-postgres-1 --grace-period=0 --force   # 프라이머리 강제 종료
kubectl cnpg status app-postgres          # 다른 인스턴스가 프라이머리로 승격됐는지 확인

체크리스트

  • api-server, inference-worker 최소 구현 (/healthz, /metrics 표준화)
  • RabbitMQ StatefulSet 배포, ack/nack + DLQ(재시도 3회) 구현
  • 워커 파드 강제 종료 → 메시지 재투입 검증, DLQ 격리 검증
  • Harbor 사설 레지스트리 구축 + Trivy 스캔 + 취약점 발견 시 배포 차단 정책
  • Jenkins 파이프라인: 빌드 → 테스트 → Harbor 푸시 → Trivy 스캔 대기
  • ArgoCD 도입, GitOps 전환
  • 배포 소요 시간 before/after 측정
  • (선택 확장) CloudNativePG로 PostgreSQL 운영 + failover 검증