본문으로 건너뛰기
← Systems Notebook / OPS · AI
실습 기록사내 플랫폼

서버 구축을 넘어 플랫폼까지 지원하기 위해

인프라에서 플랫폼까지 지원 범위를 넓히기 위해 준비한 사내 실습 환경. 단계별 시작점부터 모델 배포, 요청과 관측, 복원 범위를 살펴봅니다.

홍범약 15분 게시

구축·검증 기록 –

중소기업에서는 새로운 분야마다 인력을 충원하기보다, 기존 인력이 더 넓은 범위를 맡아주기를 기대하는 경우가 많다고 느끼고 있습니다. 서버와 스토리지, OS, 때로는 VM까지 다루는 업무에서도 그 위에 올라가는 플랫폼을 이해하고 지원할 수 있는 역량이 점차 필요해지고 있습니다.

이러한 배경에서 기존 인프라 지원 범위를 플랫폼까지 넓히는 것을 목표로 삼았습니다. 서비스들이 어떻게 연결되고, 어떤 상태여야 정상적으로 사용할 수 있으며, 문제가 생겼을 때 어디부터 확인해야 하는지까지 함께 이해하고 지원할 수 있도록 준비하고 있습니다.

이를 위해 교육에서 배운 내용을 바탕으로 사내에 실습 플랫폼을 구축했습니다. 함께 공부하는 사람들이 초기 구성부터 최종 구성까지 단계별 상태를 살펴볼 수 있도록 KVM 스냅샷을 준비하고, 각 단계에서 수행할 작업과 확인해야 할 결과도 정리해 두었습니다. 처음부터 과정을 따라가거나 필요한 단계로 돌아가 다시 실습할 수 있도록 준비한 환경입니다.

이 글에서는 플랫폼 지원이라는 목표에 맞춰 실습 범위를 어떻게 정했는지, 교육 내용을 실제 장비에 적용하면서 무엇을 바꿨는지, 동료들이 각 단계에서 어떤 결과를 확인할 수 있도록 준비했는지를 설명하고자 합니다. 구성과 검증 결과는 2026년 9월 7일부터 9일까지 남긴 기록을 기준으로 설명합니다. 실제 관리 화면은 9월 28일에 다시 접속해 캡처했으며, 화면에서 확인할 수 있는 상태와 당시의 실습 결과를 나눠 설명하고 있습니다.

플랫폼까지 지원한다는 것은 무엇을 더 확인한다는 뜻일까요

서버의 전원이 켜지고 OS에 접속할 수 있으면 다음 작업을 시작할 기반이 마련됩니다. VM이 정상적으로 실행되고 필요한 디스크와 네트워크를 사용할 수 있는지도 확인할 수 있습니다. 플랫폼으로 지원 범위를 넓히면 그 위에서 실행되는 서비스까지 질문이 이어집니다.

예를 들어 배포한 프로그램이 사용자 요청을 받지 못한다면 어느 구간을 확인해야 할까요. 새 이미지는 만들어졌는데 실제 서비스가 이전 버전으로 응답한다면 무엇을 비교해야 할까요. 로그인에는 성공했지만 필요한 기능을 사용할 수 없다면 계정과 권한 중 어디를 살펴봐야 할까요.

이런 질문에 답하려면 서비스가 실행되는 위치, 구성 요소 사이의 연결, 저장되는 데이터, 접근 권한을 함께 볼 수 있어야 합니다. 서버와 OS를 다루며 쌓은 경험에 배포와 서비스 동작을 확인하는 방법을 더하는 것이 이번 준비의 방향입니다.

그래서 실습의 목표도 각 도구의 설치 여부에서 한 걸음 더 나아가도록 잡았습니다. 무엇을 변경하면 어느 구성 요소가 반응하는지, 정상적인 결과는 어디에서 볼 수 있는지, 결과가 다를 때 어떤 기록부터 확인할지를 설명할 수 있는 환경을 만들고자 했습니다.

교육 내용을 사내 장비 위에 연결했습니다

기존에 정리한 교육 자료에는 Kubernetes, CI/CD, 인증, 관측, 저장소의 원리와 실습 절차가 있습니다. GitLab·Harbor·Argo CD를 연결하는 교육 글도 이미지 생성부터 배포와 실제 응답 확인까지 다루고 있습니다. 이번 작업에서는 그 흐름을 사내 장비에서 다시 구성하고, 함께 실습할 수 있는 상태와 결과를 준비했습니다.

실습 대상은 이미 학습된 모델을 배포하고 교체하는 과정으로 정했습니다. GitLab과 Harbor, Argo CD가 연결된 배포 경로를 만들고, vLLM이 모델을 실행하며, 응답과 관측 기록까지 확인하도록 구성했습니다. 모델 학습 파이프라인은 이번 구축 범위에 포함하지 않았습니다.

장비 구성은 KVM 물리 호스트 두 대와 별도의 GPU 서버 한 대입니다. KVM 위에 control-plane VM 한 대와 CPU worker VM 세 대를 만들었고, A100 PCIe 80GB 한 장이 있는 서버는 베어메탈 GPU worker로 참여시켰습니다. 플랫폼 서비스는 CPU worker에, 모델 실행은 GPU worker에 배치한 구성입니다.

이 배치를 이해하면 플랫폼 서비스와 모델 실행이 어떤 자원을 사용하는지 구분할 수 있습니다. 동시에 control-plane이 한 대라는 점, GPU worker가 VM이 아니라는 점도 알 수 있습니다. 나중에 장애 범위나 복원 대상을 판단할 때 필요한 정보입니다.

구성 살펴보기 / 01 플랫폼 서비스와 GPU가 실행되는 위치

물리 장비의 배치에서 클러스터와 실행 위치로 범위를 좁혀 봅니다.

장비별 실행 경계 · 박스 안은 해당 장비의 구성

  1. KVM 호스트 A
    • control-plane VM
    • CPU worker VM 01
  2. KVM 호스트 B
    • CPU worker VM 02
    • CPU worker VM 03
  3. 베어메탈 GPU 서버
    • GPU worker
    • A100 PCIe 80GB × 1
두 KVM 호스트의 VM 네 대와 별도 GPU 서버

각 KVM 호스트가 VM 두 대씩을 실행합니다. GPU 서버는 VM으로 만들지 않고 베어메탈 worker로 참여시켰습니다.

구분할 점KVM 호스트 자체와 그 위의 Kubernetes VM 노드는 서로 다른 관리 대상입니다.

두 KVM 호스트의 VM 네 대와 별도 GPU 서버. 각 KVM 호스트가 VM 두 대씩을 실행합니다. GPU 서버는 VM으로 만들지 않고 베어메탈 worker로 참여시켰습니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. 두 KVM 호스트의 VM 네 대와 별도 GPU 서버

    각 KVM 호스트가 VM 두 대씩을 실행합니다. GPU 서버는 VM으로 만들지 않고 베어메탈 worker로 참여시켰습니다.

    장비별 실행 경계 · 박스 안은 해당 장비의 구성 KVM 호스트 A (control-plane VM · CPU worker VM 01) / KVM 호스트 B (CPU worker VM 02 · CPU worker VM 03) / 베어메탈 GPU 서버 (GPU worker · A100 PCIe 80GB × 1)

    KVM 호스트 자체와 그 위의 Kubernetes VM 노드는 서로 다른 관리 대상입니다.

  2. VM 네 대와 GPU 서버가 하나의 클러스터를 구성

    논리적으로는 Kubernetes 노드 다섯 대입니다. control-plane은 한 대이며, CPU worker와 GPU worker는 서로 다른 역할의 워크로드를 실행합니다.

    Kubernetes 클러스터의 노드 구성 Control-plane × 1 (VM · API server / etcd) / CPU worker × 3 (VM · 플랫폼 서비스) / GPU worker × 1 (베어메탈 · 모델 실행)

    단일 control-plane 구성입니다. API 고가용성을 구현한 사례는 아닙니다.

  3. 플랫폼은 CPU worker에, 모델은 GPU worker에

    서비스가 실패했을 때 먼저 어느 노드의 워크로드인지 구분합니다. 모델 파일의 로컬 읽기 경로도 GPU 서버에 있습니다.

    플랫폼 서비스 실행 위치 CPU worker VM (세 노드에 서비스 배치) → 실행 → 플랫폼 Pod (GitLab · Harbor · 인증 · 관측)

    모델 실행 위치 GPU worker (containerd · NVIDIA runtime) → 실행 → vLLM Pod (A100 · GPU 서버의 모델 캐시 사용)

    베어메탈 GPU 서버의 OS와 모델 캐시는 KVM VM 스냅샷에 포함되지 않습니다.

교육의 구성을 모든 부분에서 그대로 옮긴 것은 아닙니다. 다음 차이는 글을 읽거나 실습을 따라갈 때 함께 알아두어야 합니다.

항목 기존 교육에서 다룬 구성 이번 사내 실습 구성
Kubernetes 기반 RKE2 기반 교육 kubeadm 기반 클러스터
CI 이미지 빌드 Rootless BuildKit Kubernetes executor와 Docker-in-Docker
모델 작업 Ray·MLflow·KServe를 연결하는 학습·배포 예제 준비된 모델을 vLLM으로 배포·교체
서빙 검증 대상 RayJob·MLflow와 KServe V2 추론 경로 vLLM 모델 전환과 SSE 응답·로그·trace 연결

이 표는 이번에 구성한 환경과 교육 예제를 구분하기 위한 것입니다. 각 방식의 우열을 비교한 결과는 아닙니다. 특히 빌드 방식이 다르기 때문에 교육의 Rootless BuildKit 설정과 이번 Runner 설정을 같은 것으로 읽어서는 안 됩니다.

처음부터 다시 시작할 수 있는 지점을 나눴습니다

함께 공부하는 사람마다 필요한 시작점은 다를 수 있습니다. VM의 OS 준비부터 해보고 싶은 사람이 있고, Kubernetes 초기화 과정을 익히고 싶은 사람도 있습니다. 이미 만들어진 플랫폼에서 배포와 관측을 연습하고 싶은 경우도 있습니다.

이런 학습 경로를 준비하기 위해 VM 상태를 네 단계로 나눠 저장했습니다. 각 지점은 특정 작업을 시작하기 위한 상태이면서, 앞선 작업을 마쳤을 때 도달해야 할 상태이기도 합니다.

복원 지점 저장한 상태 이 단계에서 이어갈 실습
S0 VM 네 대의 OS·네트워크·guest-agent·추가 디스크 준비 OS가 준비된 상태에서 Kubernetes 구성 준비 시작
S1 containerd·kubeadm·kubelet과 필요한 설정·이미지 준비, init/join 전 클러스터 초기화, worker 참여, CNI 구성
S2 control-plane 한 대와 CPU worker 세 대 Ready, 기본 통신 확인 CPU 클러스터 위에 플랫폼을 구축하는 과정
S3-platform-ready CI/CD·SSO·Gateway·관측·스토리지 구성이 완료된 VM 상태 구성된 플랫폼을 사용하며 배포와 서비스 동작 확인
단계별 학습 지도

어느 상태에서 무엇을 연습할 수 있을까

저장한 단계와 도달할 결과를 연결해 봅니다. 버튼은 설명만 바꾸며 서버에 연결하거나 복원을 실행하지 않습니다.

S0생성·재기동 확인 / 실제 복원 미시험
시작 상태
VM 네 대의 OS·네트워크·guest-agent·추가 디스크를 확인한 상태입니다. Kubernetes 구성 전입니다.
해볼 작업
containerd와 Kubernetes 패키지, sysctl 등 클러스터 초기화에 필요한 준비 과정을 익힙니다.
확인할 결과
접속·OS·시간 동기화·guest-agent와 통신을 확인한 뒤, 패키지와 이미지 준비를 마쳐 S1 상태를 목표로 합니다.
실제 검증 범위
2026-09-07 생성·재기동 후 OS와 통신 검사를 통과했습니다. S0로 실제 되돌리는 개별 시험은 수행하지 않았습니다.

S0 OS 준비. 생성·재기동 확인 / 실제 복원 미시험

복원 지점의 대상은 VM입니다. 물리 KVM 호스트와 베어메탈 GPU의 OS·드라이버·모델 캐시는 함께 되돌아가지 않습니다.

네 단계의 설명을 한 번에 읽기
  1. S0 · OS 준비
    시작 상태
    VM 네 대의 OS·네트워크·guest-agent·추가 디스크를 확인한 상태입니다. Kubernetes 구성 전입니다.
    해볼 작업
    containerd와 Kubernetes 패키지, sysctl 등 클러스터 초기화에 필요한 준비 과정을 익힙니다.
    확인할 결과
    접속·OS·시간 동기화·guest-agent와 통신을 확인한 뒤, 패키지와 이미지 준비를 마쳐 S1 상태를 목표로 합니다.
    실제 검증 범위
    2026-09-07 생성·재기동 후 OS와 통신 검사를 통과했습니다. S0로 실제 되돌리는 개별 시험은 수행하지 않았습니다.
  2. S1 · 클러스터 준비
    시작 상태
    containerd·kubeadm·kubelet과 설정·이미지가 준비돼 있습니다. kubeadm init과 worker join을 실행하기 전입니다.
    해볼 작업
    control-plane 초기화, CPU worker 참여, CNI 적용과 노드 간 통신 확인을 연습합니다.
    확인할 결과
    control-plane 한 대와 CPU worker 세 대가 Ready인지 확인하고 HTTP·DNS·Service 통신까지 점검해 S2 상태에 도달합니다.
    실제 검증 범위
    2026-09-07 준비 결과를 확인하고 S1을 생성했으며 VM 네 대의 재기동을 확인했습니다. S1 개별 복원은 미시험입니다.
  3. S2 · CPU 클러스터
    시작 상태
    CPU 노드 네 대가 Ready이고 기본 통신을 확인했습니다. GPU 참여와 플랫폼 설치가 포함되기 전의 상태입니다.
    해볼 작업
    GPU 참여와 플랫폼 구축으로 범위를 넓혀 봅니다. 기존 GPU workload와 인증 상태는 VM 복원과 별도로 맞춰야 합니다.
    확인할 결과
    노드 준비 상태와 통신에 더해, 설치한 서비스의 저장·배포·실제 요청 결과를 확인하는 과정으로 이어집니다.
    실제 검증 범위
    2026-09-07 GPU 참여 전 실제 S2 복원을 수행했습니다. 저장 후 만든 파일·ConfigMap 제거, 네 노드 Ready, 통신 검사 12개를 확인했습니다.
  4. S3 · 플랫폼 구성
    시작 상태
    S3-platform-ready는 CI/CD·SSO·Gateway·관측·스토리지와 관리자 계정을 구성한 뒤 VM 네 대를 저장한 지점입니다.
    해볼 작업
    준비된 플랫폼에서 모델 배포와 전환, 사용자 접근, 실제 응답과 로그·trace를 연결해 확인하는 연습을 진행합니다.
    확인할 결과
    노드·PVC·Argo 상태와 로그인, 실제 SSE 응답과 관측 기록을 함께 확인합니다. VM 밖의 GPU 상태도 따로 점검해야 합니다.
    실제 검증 범위
    2026-09-09 생성·재기동 후 다섯 노드 Ready, PVC 13개 Bound, Argo·SSO·S3 IAM, SSE 세 번과 관측 연결을 확인했습니다. 실제 S3 복원은 미시험입니다.

예를 들어 클러스터 초기화와 네트워크 구성을 익히려면 S1을 시작점으로 삼고 S2의 상태를 목표로 할 수 있습니다. 이미 준비된 Kubernetes에 플랫폼을 올리는 과정을 다루려면 S2에서 시작하는 식입니다. S3는 구성된 서비스를 사용하며 동작을 살펴볼 수 있도록 마련한 지점입니다.

함께 볼 수 있는 도달 상태도 준비했습니다. OS 단계에서는 접속과 네트워크, 클러스터 단계에서는 노드 상태와 노드 사이의 통신, 플랫폼 단계에서는 실제 배포와 요청 결과를 확인하도록 했습니다. 작업을 끝냈다는 판단을 화면의 표시 하나에 맡기지 않고, 그 단계에서 가능해야 하는 동작과 연결하려는 목적입니다.

네 지점 모두 생성과 재기동을 확인했으며, 실제 저장 지점으로 되돌리는 시험은 S2에서 수행했습니다. 저장 이후 VM에 만든 파일과 Kubernetes ConfigMap이 복원 후 사라졌는지 확인했고, CPU 노드 네 대의 Ready 상태와 HTTP·DNS·Service 통신 검사 12개도 통과했습니다. S0·S1·S3의 개별 되돌림 시험은 후속 확인 항목으로 남아 있습니다.

따라서 단계별 실습을 위한 복원 지점을 준비했다는 것과 모든 단계의 반복 복원을 검증했다는 것은 구분해서 기록하고 있습니다. 이 차이까지 다음에 환경을 사용하는 사람이 알 수 있어야 어느 상태에서 시작할지 판단할 수 있습니다.

각 단계에서 확인할 결과를 함께 남겼습니다

이번 구축 과정에는 노드 상태만으로 다음 단계에 넘어가기 어려웠던 사례가 있습니다. 노드가 Ready이고 ping도 성공했지만, 다른 worker의 Pod로 보내는 HTTP 요청은 실패했습니다. 기록에서는 Flannel VXLAN 경로의 checksum offload 문제를 재현했고, 관련 설정을 조정한 뒤 통신 결과를 다시 확인했습니다.

이 경험은 실습의 도달 조건을 구체적으로 만드는 데 도움이 됩니다. Kubernetes가 설치됐다는 사실에 더해, 실제로 필요한 통신이 되는지 확인해야 플랫폼을 올릴 다음 단계를 진행할 수 있습니다. 실습 안내에도 정상 상태의 이름과 함께 그 상태에서 성공해야 하는 요청을 남겨두었습니다.

모델 배포에서는 같은 생각을 응답 검증으로 이어갔습니다. 이번 실습은 Gemma를 배포한 뒤 Qwen으로 전환하고, 다시 Gemma로 돌아오는 순서로 진행했습니다. 각 배포에서 무엇이 달라졌고 어떤 모델이 응답하는지를 비교할 수 있는 흐름입니다.

CI는 validate, build, promote, verify의 네 단계로 구성했습니다. 준비한 프로필과 이미지 정보를 확인하고 이미지를 빌드한 뒤, 수동 promote를 통해 배포 Git의 release.yaml을 갱신합니다. Argo CD가 그 변경을 반영하면 verify 단계에서 해당 commit의 상태와 실제 모델 응답을 확인합니다.

구성 살펴보기 / 02 코드 변경에서 배포와 응답 검증까지

이미지 생성, 배포 의도 전달, 실제 결과 확인을 나누어 읽습니다.

이미지 생성과 등록

  1. 코드 Git GitLab · serving-code
  2. GitLab Runner validate · build
  3. Harbor OCI 이미지 · digest 고정
실행 이미지를 만들고 Harbor의 digest로 식별

코드 Git에서 CI가 시작됩니다. Kubernetes executor와 Docker-in-Docker를 사용하는 Runner가 실행 이미지를 준비하며, 같은 이미지는 검증한 digest로 재사용할 수 있습니다.

구분할 점모델 가중치는 실행 이미지에 넣지 않습니다. RustFS의 모델 파일을 별도로 관리합니다.

실행 이미지를 만들고 Harbor의 digest로 식별. 코드 Git에서 CI가 시작됩니다. Kubernetes executor와 Docker-in-Docker를 사용하는 Runner가 실행 이미지를 준비하며, 같은 이미지는 검증한 digest로 재사용할 수 있습니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. 실행 이미지를 만들고 Harbor의 digest로 식별

    코드 Git에서 CI가 시작됩니다. Kubernetes executor와 Docker-in-Docker를 사용하는 Runner가 실행 이미지를 준비하며, 같은 이미지는 검증한 digest로 재사용할 수 있습니다.

    이미지 생성과 등록 코드 Git (GitLab · serving-code) → CI job → GitLab Runner (validate · build) → build / push → Harbor (OCI 이미지 · digest 고정)

    모델 가중치는 실행 이미지에 넣지 않습니다. RustFS의 모델 파일을 별도로 관리합니다.

  2. 수동 promote가 Git을 바꾸면 Argo CD가 반영

    promote는 선택한 모델과 이미지 digest를 release.yaml에 commit합니다. Argo CD가 배포 Git의 main을 감시하고 Kubernetes API에 원하는 상태를 반영합니다.

    배포 선언의 전달 방향 CI promote (수동 승인 단계) → commit → 배포 Git (serving-deploy · release.yaml) → 선언 전달 → Argo CD (main 감시 · Helm 렌더) → apply → Kubernetes API (Deployment · Service · ConfigMap)

    CI는 배포 Git을 갱신하고 Argo 상태를 읽습니다. CI가 직접 Kubernetes에 배포하지 않습니다.

  3. 선택한 commit과 실제 모델 응답을 함께 확인

    verify는 Argo의 정확한 배포 commit이 Synced/Healthy인지 확인한 뒤 내부 Service로 SSE를 세 번 호출합니다. Tempo에서 client와 vLLM span의 부모 연결까지 확인합니다.

    한 GPU의 모델 교체 Deployment (선택한 모델 profile) → Recreate → vLLM Pod (GPU 한 장 · 모델 한 개)

    CI의 내부 검증 요청 CI verify (짧은 순차 요청 세 번) → SSE 요청 → 모델 Service (클러스터 내부 경로) → 요청 전달 → vLLM API (응답 model ID · usage · DONE)

    모델 로딩 동안 요청이 중단됩니다. 외부 Gateway를 통한 검증은 별도로 수행했습니다.

이 과정에서 CI가 하는 Git 변경과 Argo CD의 배포 동작을 구분했습니다. verify는 Argo API를 조회해 정확한 배포 commit을 기다립니다. CI가 Argo에 배포를 직접 명령하는 것과 배포 결과를 읽어 확인하는 것은 다른 동작입니다.

배포 대상으로 함께 관리한 것은 실행 이미지와 모델 파일입니다. Harbor에는 실행 환경인 OCI 이미지를 두고, RustFS에는 모델 manifest와 가중치를 두었습니다. 모델을 준비하는 initContainer가 manifest와 파일의 SHA-256을 확인한 뒤 GPU 서버의 로컬 캐시에 저장하도록 구성했습니다. 기존 저장소·버전 관리 교육 글에서 다룬 역할 구분을 이번 모델 실행 경로에 적용한 부분입니다.

2026년 9월 7일의 기록에서는 세 번의 배포가 모두 네 단계를 통과했습니다. 배포마다 짧은 SSE 요청을 세 번씩 보내 총 아홉 번의 응답과 client·server trace 연결을 확인했습니다. 마지막 Gemma 복귀에서는 기존 캐시를 사용해 다운로드 없이 파일 해시를 확인한 뒤 모델을 실행한 기록도 남겼습니다.

이 결과는 모델 전환과 검증 경로가 동작했다는 기능 확인입니다. 동시 사용자 부하나 모델 품질을 비교한 벤치마크는 아닙니다. 또한 GPU 한 장에서 Recreate 방식으로 모델을 교체하므로 로딩 중에는 요청이 중단됩니다. 실습하는 사람도 이 구간을 예상하고, 새 모델의 응답이 돌아오는 시점을 확인할 수 있어야 합니다.

GitLab Pipeline #3의 validate, build, promote, verify 네 단계가 Passed로 표시된 실제 화면

GitLab에 남아 있는 9월 7일 Pipeline #3의 실행 이력입니다. validate → build → promote → verify 네 단계의 성공 여부를 한 화면에서 확인할 수 있습니다. 9월 28일에 이력을 다시 열어 캡처했으며, 이날 파이프라인을 다시 실행한 결과는 아닙니다.

화면 캡처 원본 화면 확대 ↗
Argo CD model-serving의 Healthy, Synced 상태와 Deployment, ReplicaSet, Pod 연결을 보여주는 실제 화면

9월 28일의 Argo CD 화면입니다. model-serving은 Healthy이며 main의 b991fa6에 Synced된 상태입니다. Deployment에서 ReplicaSet과 Pod로 이어지는 관계도 확인할 수 있습니다. 현재 배포는 초기 실습 이후 Qwen NVFP4로 변경된 상태이며, 이 화면만으로 모델 응답이나 전체 CI의 재검증을 의미하지는 않습니다.

화면 캡처 원본 화면 확대 ↗

사용자의 요청에서 관측 기록까지 따라갈 수 있도록 했습니다

예를 들어 모델이 내부에서는 응답하지만 외부에서는 연결되지 않는다면, 모델 실행과 외부 진입 경로를 나눠 확인해야 합니다. 이런 상황을 설명할 수 있도록 사용자의 요청이 모델에 도착하고 응답으로 돌아오는 경로를 정리했습니다.

이번 환경의 외부 요청은 MetalLB가 광고하는 주소로 들어와 NGINX 데이터 Pod를 거쳐 vLLM의 API에 도착합니다. vLLM 안에서는 FastAPI와 Uvicorn이 HTTP 요청을 처리하고, 엔진과 GPU 연산을 거쳐 생성한 토큰이 SSE로 돌아옵니다.

FastAPI는 별도로 만든 애플리케이션 서버가 아니라 vLLM 내부의 API 계층입니다. NGINX Gateway Fabric은 Service와 EndpointSlice 정보를 사용해 구성을 만들고, 확인한 NGINX 설정의 실제 upstream은 vLLM Pod IP였습니다. 요청이 통과하는 경로와 그 경로를 설정하는 구성 요소를 나눠 볼 수 있도록 도식에 반영했습니다.

구성 살펴보기 / 03 요청이 지나는 경로와 경로를 만드는 설정

실제 HTTP 경로, backend 발견, vLLM 내부 실행을 구분합니다.

실제 요청 경로

  1. 클라이언트 SDK · SSE 검증기
  2. MetalLB VIP LAN · L2 광고
  3. NGINX 데이터 Pod × 2
  4. vLLM Pod FastAPI · Uvicorn
NGINX는 vLLM의 Pod IP로 직접 프록시

클라이언트는 MetalLB가 LAN에 광고하는 VIP로 요청합니다. NGINX 데이터 Pod가 HTTPRoute에 따라 vLLM Pod IP의 8000 포트로 전달합니다.

구분할 점확인한 NGINX upstream은 Pod IP입니다. 모델 Service는 이 프록시 경로의 중간 경유지가 아닙니다.

NGINX는 vLLM의 Pod IP로 직접 프록시. 클라이언트는 MetalLB가 LAN에 광고하는 VIP로 요청합니다. NGINX 데이터 Pod가 HTTPRoute에 따라 vLLM Pod IP의 8000 포트로 전달합니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. NGINX는 vLLM의 Pod IP로 직접 프록시

    클라이언트는 MetalLB가 LAN에 광고하는 VIP로 요청합니다. NGINX 데이터 Pod가 HTTPRoute에 따라 vLLM Pod IP의 8000 포트로 전달합니다.

    실제 요청 경로 클라이언트 (SDK · SSE 검증기) → HTTP → MetalLB VIP (LAN · L2 광고) → 전달 → NGINX (데이터 Pod × 2) → Pod IP:8000 → vLLM Pod (FastAPI · Uvicorn)

    확인한 NGINX upstream은 Pod IP입니다. 모델 Service는 이 프록시 경로의 중간 경유지가 아닙니다.

  2. Service와 EndpointSlice는 backend를 발견하는 정보

    NGINX Gateway Fabric이 Kubernetes의 backend 정보를 읽어 NGINX 설정에 반영합니다. MetalLB의 IP 할당과 ARP 광고도 실제 HTTP 처리를 담당하는 데이터 Pod와 구분합니다.

    backend 발견과 설정 반영 · HTTP 요청 경로와 별개 Service · EndpointSlice (모델 backend 정보) → backend 발견 → NGINX Gateway Fabric (controller) → 설정 반영 → NGINX 데이터 Pod (생성된 upstream 설정)

    주소 제어 MetalLB (controller · speaker) → IP 할당 · ARP → VIP (LAN에서 접근하는 주소)

    그림의 화살표는 이 단계에서 설정 정보의 전달을 뜻합니다. 사용자 요청이 controller를 통과하지 않습니다.

  3. vLLM 내부 API에서 엔진과 GPU 실행으로

    FastAPI와 Uvicorn은 vLLM 컨테이너 내부의 OpenAI 호환 HTTP 계층입니다. 엔진의 tokenizer·scheduler·KV cache와 GPU 연산을 거쳐 생성한 토큰은 API와 NGINX를 역방향으로 지나 SSE로 돌아갑니다.

    vLLM 요청 처리와 GPU 실행 FastAPI · Uvicorn (vLLM Pod 내부 · HTTP / ASGI) → 생성 요청 → vLLM 엔진 (tokenizer · scheduler · KV cache) → 연산 → GPU 실행 (PyTorch · CUDA · A100)

    별도 제작한 FastAPI 서버는 없습니다. NGINX 응답 buffering을 끄고 SSE 전달을 확인했습니다.

응답이 정상적으로 돌아온 뒤에는 그 요청이 남긴 기록을 찾도록 준비했습니다. Prometheus에서는 요청과 자원 상태의 지표를 보고, Loki에서는 실행·접근 로그를 찾으며, Tempo에서는 요청의 trace를 확인합니다. Grafana에서는 이 자료를 함께 살펴볼 수 있습니다. 기본 개념은 기존 관측성 교육 글에 연결하고, 실습에서는 방금 보낸 요청과 기록이 어떻게 이어지는지에 집중합니다.

첫 토큰이 돌아오기까지의 시간과 전체 응답 시간도 남겼습니다. 이를 GPU 연산 시간과 동일하게 해석하지는 않습니다. 클라이언트에서 측정한 값에는 연결과 대기, 모델의 입력 처리, 네트워크 등 여러 구간이 함께 들어 있습니다. 숫자를 읽을 때 어디에서 잰 값인지까지 설명하는 것이 실습의 일부입니다.

구성 살펴보기 / 05 지표·로그·trace를 각각 수집하고 함께 조회

화살표는 데이터와 조회 결과의 방향입니다. 요청 방향과 구분해서 읽습니다.

지표 데이터와 조회 결과

  1. 워크로드 · exporter vLLM · NGINX · GPU / 클러스터
  2. Prometheus HTTP scrape · 시계열 저장
  3. Grafana 대시보드
Prometheus가 워크로드와 자원 지표를 수집

vLLM·NGINX와 DCGM 등에서 요청, 대기열, GPU 사용률과 메모리 지표를 수집합니다. Grafana는 PromQL로 조회하며 클라이언트에서 잰 TTFT·E2E와 서버 지표의 측정 경계를 구별합니다.

구분할 점scrape 요청은 Prometheus가 워크로드로 보냅니다. 지표 하나를 개별 요청의 전체 trace로 읽지 않습니다.

Prometheus가 워크로드와 자원 지표를 수집. vLLM·NGINX와 DCGM 등에서 요청, 대기열, GPU 사용률과 메모리 지표를 수집합니다. Grafana는 PromQL로 조회하며 클라이언트에서 잰 TTFT·E2E와 서버 지표의 측정 경계를 구별합니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. Prometheus가 워크로드와 자원 지표를 수집

    vLLM·NGINX와 DCGM 등에서 요청, 대기열, GPU 사용률과 메모리 지표를 수집합니다. Grafana는 PromQL로 조회하며 클라이언트에서 잰 TTFT·E2E와 서버 지표의 측정 경계를 구별합니다.

    지표 데이터와 조회 결과 워크로드 · exporter (vLLM · NGINX · GPU / 클러스터) → /metrics 응답 → Prometheus (HTTP scrape · 시계열 저장) → PromQL 결과 → Grafana (대시보드)

    scrape 요청은 Prometheus가 워크로드로 보냅니다. 지표 하나를 개별 요청의 전체 trace로 읽지 않습니다.

  2. 실행 로그와 접근 로그를 요청의 ID로 찾기

    Alloy가 수집 대상 Pod의 로그를 Loki에 보냅니다. CI·vLLM 실행 기록과 NGINX access log에서 run ID와 traceparent를 찾아 해당 요청의 기록을 연결합니다.

    로그 수집과 조회 결과 Pod 로그 (실행 로그 · NGINX access log) → 수집 → Alloy (수집 대상 Pod) → push → Loki (run ID · trace ID) → LogQL 결과 → Grafana (Explore · 로그 조회)

    로그에 trace ID가 있다는 사실과 Tempo에 실제 span이 저장됐다는 사실은 각각 확인했습니다.

  3. 클라이언트 요청과 vLLM span의 부모 연결 확인

    W3C traceparent로 client와 vLLM span을 연결합니다. Alloy를 거쳐 Tempo에 저장된 span을 조회해 부모 ID가 실제 요청과 일치하는지 확인했습니다.

    span 전달과 조회 결과 Client · vLLM (CLIENT / SERVER spans) → OTLP → Alloy (OTLP 수신) → OTLP gRPC → Tempo (span 저장) → 조회 결과 → Grafana (trace 조회)

    NGINX 자체 span은 없습니다. traceparent를 전달하고 access log에 기록하는 범위입니다.

관측 범위에도 구분이 있습니다. 이번 환경에서는 client와 vLLM span을 연결했고, NGINX는 자체 span 대신 access log에 traceparent를 남깁니다. 모든 구성 요소가 하나씩 span을 만든 것처럼 읽어서는 안 됩니다.

Gateway를 추가한 뒤에는 외부 진입 경로에서 SSE 요청 세 번과 로그·trace 연결을 별도로 확인했습니다. 앞선 모델 전환 CI는 내부 Service 경로를 사용했으며, Gateway 추가 검증에서 전체 모델 전환을 다시 실행한 것은 아닙니다. 두 검증을 나눠 기록해 어느 요청 경로를 확인했는지 알 수 있도록 했습니다.

로그인과 권한도 실제 사용 범위에 맞춰 확인했습니다

동료가 플랫폼을 사용하려면 관리 화면에 접근할 수 있어야 합니다. 이번에는 Keycloak을 GitLab·Argo CD·Grafana에 연결하고, 같은 로그인 기반에서 각 애플리케이션의 역할이 적용되도록 구성했습니다.

실습 기록에는 두 교육 계정으로 로그인한 뒤 관리자와 읽기 역할의 차이를 확인한 결과가 있습니다. 첫 애플리케이션에서 Keycloak 로그인을 마친 뒤 다른 두 애플리케이션에서도 추가 비밀번호 입력 없이 인증되는지, 인증 이후 실제 권한은 어떻게 다른지를 확인했습니다.

구성 살펴보기 / 04 같은 로그인과 애플리케이션별 권한

브라우저 인증 흐름, 역할 적용, SSO 연결 범위를 나누어 봅니다.

브라우저 이동

  1. 관리 앱 GitLab · Argo CD · Grafana
  2. Keycloak 로그인 · SSO 세션
  3. 앱 callback code 교환 후 앱 세션 생성
Keycloak 인증 후 앱이 자체 세션을 생성

브라우저가 앱에서 Keycloak issuer로 이동해 인증합니다. callback으로 돌아오면 앱이 code를 token으로 교환하고 자체 세션을 만듭니다. Keycloak의 계정과 realm은 별도 PostgreSQL에 저장합니다.

구분할 점사용자별 첫 비밀번호 입력 한 번과 나머지 두 앱의 추가 입력 없는 인증을 확인했습니다.

Keycloak 인증 후 앱이 자체 세션을 생성. 브라우저가 앱에서 Keycloak issuer로 이동해 인증합니다. callback으로 돌아오면 앱이 code를 token으로 교환하고 자체 세션을 만듭니다. Keycloak의 계정과 realm은 별도 PostgreSQL에 저장합니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. Keycloak 인증 후 앱이 자체 세션을 생성

    브라우저가 앱에서 Keycloak issuer로 이동해 인증합니다. callback으로 돌아오면 앱이 code를 token으로 교환하고 자체 세션을 만듭니다. Keycloak의 계정과 realm은 별도 PostgreSQL에 저장합니다.

    브라우저 이동 관리 앱 (GitLab · Argo CD · Grafana) → issuer 이동 → Keycloak (로그인 · SSO 세션) → 인증 후 복귀 → 앱 callback (code 교환 후 앱 세션 생성)

    사용자별 첫 비밀번호 입력 한 번과 나머지 두 앱의 추가 입력 없는 인증을 확인했습니다.

  2. 같은 계정이어도 앱별 정책이 사용 권한을 결정

    두 교육 계정으로 세 앱에 로그인해 관리자와 읽기 역할의 차이를 확인했습니다. 인증 성공만으로 모든 앱의 관리자 권한을 얻는 것은 아닙니다.

    OIDC로 연결한 관리 앱과 역할 Keycloak (같은 로그인 기반) → OIDC → 각 앱의 정책 (GitLab · 프로젝트 역할 · Argo CD · admin / readonly · Grafana · Admin / Viewer)

    CI의 project token, Runner 인증, Argo 조회 계정은 사람의 SSO 세션과 별도로 관리합니다.

  3. 관리 앱 SSO와 모델 API의 인증 범위 구분

    OIDC로 연결한 대상은 GitLab·Argo CD·Grafana입니다. Harbor와 RustFS 관리 콘솔에는 로컬 인증을 사용하며, Keycloak을 모델 API 인증에 연결하지 않았습니다.

    접근 대상별 인증 경계 Keycloak OIDC (GitLab · Argo CD · Grafana) / 로컬 인증 (Harbor 관리 콘솔 · RustFS 관리 콘솔) / 모델 API (Keycloak OIDC 미적용)

    관리 화면의 SSO가 동작한다고 모델 API까지 같은 인증으로 보호되는 것은 아닙니다.

이를 통해 로그인 성공과 기능 사용 권한을 나눠 살펴볼 수 있습니다. Keycloak과 Argo CD 권한을 다룬 교육 글의 내용을 실제 관리 화면과 연결해 보는 부분입니다.

SSO의 적용 대상은 위 세 애플리케이션입니다. Harbor와 RustFS 관리 콘솔은 로컬 인증을 사용하며, 모델 API에는 Keycloak OIDC를 적용하지 않았습니다. CI에서 사용하는 token과 조회 계정도 사람의 로그인 세션과 별도로 관리합니다. 이 범위를 명확하게 알고 있어야 사용자가 어느 서비스에서 어떤 인증을 거치는지 설명할 수 있습니다.

돌아갈 지점을 준비하면서 저장 범위도 구분했습니다

스냅샷으로 돌아갈 지점을 만들 때는 무엇이 저장되는지부터 확인해야 합니다. 이 환경에는 컨테이너 이미지, 모델 원본, GPU 서버의 모델 캐시, 플랫폼 서비스의 데이터가 서로 다른 위치에 있습니다.

Harbor의 이미지와 RustFS의 모델 파일은 배포에 필요한 자료입니다. GitLab·Keycloak 등의 상태와 관측 데이터는 VM 내부의 local-path PVC에 저장됩니다. GPU 서버의 모델 캐시는 VM 밖에 있습니다. RustFS 역시 이번 구성에서는 local-path를 사용하는 단일 replica이며, 노드 간 공유와 고가용성을 제공하는 저장소로 검증한 상태는 아닙니다.

구성 살펴보기 / 06 배포 입력, 서비스 데이터, 복원 범위

같은 저장소로 묶지 않고, 무엇을 저장하고 어디까지 되돌리는지 구분합니다.

실행 이미지 전달

  1. Harbor OCI 이미지 · digest
  2. GPU 노드의 containerd vLLM 컨테이너 실행 이미지

모델 파일 전달

  1. RustFS S3 객체 · manifest / 가중치
  2. model-cache init manifest · SHA-256 검사
  3. GPU 로컬 캐시 vLLM이 읽는 모델 파일
실행 이미지와 모델 가중치가 각각 다른 경로로 도착

containerd는 Harbor에서 고정한 digest의 OCI 이미지를 pull합니다. model-cache initContainer는 RustFS의 manifest와 모델 파일을 확인하고 GPU 서버의 로컬 캐시를 준비합니다.

구분할 점Gemma 복귀에서는 다운로드 0B와 전체 파일 해시 확인 후 캐시 재사용을 기록했습니다.

실행 이미지와 모델 가중치가 각각 다른 경로로 도착. containerd는 Harbor에서 고정한 digest의 OCI 이미지를 pull합니다. model-cache initContainer는 RustFS의 manifest와 모델 파일을 확인하고 GPU 서버의 로컬 캐시를 준비합니다.

단계를 선택해 설명을 읽는 도식입니다. 실시간 서버 상태나 패킷 흐름을 표시하지 않습니다.

전체 구성 텍스트로 읽기
  1. 실행 이미지와 모델 가중치가 각각 다른 경로로 도착

    containerd는 Harbor에서 고정한 digest의 OCI 이미지를 pull합니다. model-cache initContainer는 RustFS의 manifest와 모델 파일을 확인하고 GPU 서버의 로컬 캐시를 준비합니다.

    실행 이미지 전달 Harbor (OCI 이미지 · digest) → 이미지 전달 → GPU 노드의 containerd (vLLM 컨테이너 실행 이미지)

    모델 파일 전달 RustFS (S3 객체 · manifest / 가중치) → 파일 전달 → model-cache init (manifest · SHA-256 검사) → 검증 후 준비 → GPU 로컬 캐시 (vLLM이 읽는 모델 파일)

    Gemma 복귀에서는 다운로드 0B와 전체 파일 해시 확인 후 캐시 재사용을 기록했습니다.

  2. 플랫폼 상태는 VM 내부의 local-path에 저장

    GitLab·Harbor·Keycloak·관측 데이터와 RustFS 객체는 VM 내부 local-path PVC에 저장합니다. 노드 로컬 저장소와 노드 간 공유 스토리지는 같은 구성이 아닙니다.

    서비스 데이터의 저장 관계 플랫폼 서비스 (GitLab · Harbor · RustFS · 인증 · 관측) → PVC mount → local-path PVC (기록 당시 13개 Bound) → 저장 위치 → VM 내부 디스크 (노드 로컬 저장 경로)

    RustFS는 단일 replica입니다. Rook/Ceph는 디스크와 준비 조건만 확인했으며 설치하지 않았습니다.

  3. S3-platform-ready는 네 VM을 함께 저장한 지점

    2026년 9월 9일에 플랫폼 구성 후 S3를 생성하고 VM 재기동과 서비스 동작을 확인했습니다. 스냅샷 이후 GPU 서버와 PC에 남는 상태는 별도로 대조해야 합니다.

    S3-platform-ready의 저장 경계 포함 · VM 네 대 (VM OS · 실습용 추가 디스크 · Kubernetes 설정 · etcd · VM 내부 local-path PVC) / 제외 · VM 밖의 상태 (물리 KVM 호스트 OS · 네트워크 · GPU OS · 드라이버 · 모델 캐시 · PC 파일의 자동 되돌림)

    S3 생성·재기동은 확인했지만 S3로 실제 되돌리는 시험은 수행하지 않았습니다. 실제 복원 검증은 S2입니다.

S3-platform-ready에는 VM의 OS와 Kubernetes 설정·etcd, VM 내부 local-path PVC에 있는 플랫폼 데이터가 포함됩니다. 반면 물리 KVM 호스트의 OS와 네트워크 설정, 베어메탈 GPU 서버의 OS·드라이버·모델 캐시는 포함되지 않습니다. 구성된 플랫폼 단계로 돌아가더라도 GPU 쪽 상태를 함께 확인해야 하는 이유입니다.

VM 네 대도 같은 저장 지점을 하나의 세트로 다룹니다. 서로 다른 시점의 VM을 임의로 섞어 실행하는 방식으로 준비하지 않았습니다. 복원 이후에는 VM의 기동 여부에 더해 클러스터와 서비스, 실제 요청이 기대한 상태로 돌아왔는지 확인하도록 절차를 정리했습니다.

S3는 9월 9일에 생성한 뒤 VM 재기동과 플랫폼·모델 요청의 동작을 확인했습니다. 앞에서 구분한 실제 되돌림 시험과는 별개입니다. Rook/Ceph는 실습용 디스크와 준비 조건을 확인한 단계로, 설치와 스토리지 동작까지 완료한 구성에는 포함하지 않았습니다.

이처럼 복원 가능한 대상을 설명하는 일도 플랫폼 지원을 준비하는 과정에 들어 있습니다. 저장된 시점과 포함된 데이터, 따로 다뤄야 하는 장비를 알아야 실습을 다시 시작할 때 무엇을 확인할지 정할 수 있습니다.

Harbor hpe-lab/vllm 저장소에 보관된 이미지 digest와 CI 태그가 표시된 실제 화면

Harbor의 hpe-lab/vllm 저장소입니다. 9월 7일에 저장된 실행 이미지의 digest와 CI 태그를 확인할 수 있습니다. 모델 가중치는 별도의 RustFS에 두고, Harbor에서는 실행 환경의 이미지를 식별하도록 역할을 나눴습니다. 화면은 9월 28일에 확인한 보관 상태입니다.

화면 캡처 원본 화면 확대 ↗

함께 설명하고 지원할 수 있는 범위를 넓혀가고 있습니다

이번에 준비한 것은 사내에서 같은 구성을 살펴보고, 단계별 작업과 결과를 비교하며, 필요한 지점부터 다시 익힐 수 있는 실습 환경입니다. 물리 배치, 배포, 추론 요청, 로그인, 관측, 저장소의 여섯 도식도 이 환경을 서로 다른 관점에서 설명하기 위해 정리했습니다.

동료가 배포를 수행한 뒤 어떤 모델이 응답하는지 확인하고, 그 요청의 로그와 trace를 찾아 설명할 수 있다면 플랫폼의 연결 관계를 이해하는 데 도움이 될 것입니다. 처음 구축하는 과정과 이미 구성된 서비스를 사용하는 과정을 나누어 준비한 이유도 여기에 있습니다.

환경을 구축하고 기능을 검증한 결과는 기록으로 남아 있습니다. 이 환경을 활용한 학습이 실제 지원 역량으로 얼마나 이어지는지는 앞으로 함께 확인할 부분입니다. 남아 있는 복원 시험을 보완하고 실습 과정에서 설명이 부족한 지점을 정리하면서, 서버와 OS에서 시작한 지원 범위를 플랫폼까지 넓히기 위한 준비를 이어가고 있습니다.

기록과 화면의 시점

본문의 구축·검증 결과는 2026년 9월 7일부터 9일까지의 기록을 기준으로 합니다. 실제 화면은 각 캡션에 표시한 날짜에 확인한 상태입니다.

Systems Notebook의 운영 기록 더 보기 ↗