카테고리 없음

SCG의 서비스는 어떻게 배포될까? 2노드 Kubernetes와 GitOps 운영기

scg-skku 2026. 7. 22. 17:41

안녕하세요! 시스템컨설턴트그룹 장재원입니다. 

 

SCG는 학내에서 사용하는 여러 서비스를 직접 개발하고 운영하고 있습니다. 그러나 서비스를 하나 완성했다고 해서 일이 끝나는 것은 아닙니다. 서버에 어떻게 올릴지, 장애가 나면 어떻게 복구할지, 데이터는 어디에 보관할지, 새 버전은 어떤 방식으로 배포할지까지 정해야 비로소 실제 사용자가 쓸 수 있는 서비스가 됩니다.

 

예전에는 이런 서비스들이 VM과 개별 서버에 나뉘어 올라가 있었습니다. 서비스마다 접속해서 프로세스를 띄우고, 설정 파일을 관리하고, 장애가 나면 담당자가 서버부터 찾아야 했죠.

 

이후 컨테이너 기반 운영을 확대하면서 서비스를 Kubernetes로 옮기기 시작했고, 2026년 7월에는 기존 VM에서 운영하던 서비스들의 이관도 대부분 마쳤습니다.

 

이번 글에서는 “Kubernetes가 좋다”는 일반적인 이야기가 아니라, 2026년 7월 현재 SCG가 Kubernetes를 실제로 어떻게 운영하고 있는지 정리해보려고 합니다. 먼저 한 가지를 분명히 하자면, 이 글에서 설명하는 것은 완성된 이상형이 아니라 현재의 운영계입니다. 잘 돌아가는 부분도 있고, 아직 해결해야 할 숙제도 있습니다.

Kubernetes란?

Kubernetes는 여러 서버에서 컨테이너를 배포하고 관리하는 오케스트레이션 도구입니다.

 

컨테이너 하나를 직접 실행하는 일은 어렵지 않습니다. 하지만 서비스가 많아지면 이야기가 달라집니다. 어떤 서버에 컨테이너를 띄울지, 죽은 컨테이너를 누가 다시 실행할지, 외부 요청을 어느 컨테이너로 보낼지, 설정과 비밀 값은 어떻게 주입할지 같은 문제를 계속 다뤄야 합니다.

 

Kubernetes에서는 이런 상태를 YAML로 선언합니다. 예를 들어 “이 이미지를 사용하는 Pod를 하나 유지해 줘”, “이 도메인으로 들어온 요청을 이 Service에 연결해 줘”라고 적어두면, 컨트롤 플레인이 실제 상태를 선언한 상태에 맞추려고 계속 조정합니다.

SCG가 Kubernetes에서 얻고자 한 것도 바로 이 지점이었습니다. 서버마다 다른 수동 작업을 줄이고, 서비스의 실행 상태를 코드와 설정으로 남기는 것입니다.

현재 클러스터: 작지만 실제 서비스를 운영하는 2노드 구성

현재 운영 클러스터는 Rocky Linux 9.2가 설치된 베어메탈 서버 두 대로 구성되어 있습니다. 한 노드는 컨트롤 플레인이면서 워커 역할도 함께 수행하고, 나머지 한 노드는 워커로 동작합니다. 두 노드를 합치면 물리 코어 32개와 메모리 256GB 규모입니다.

인프라 문서의 서비스 목록에는 Kubernetes에서 동작하는 애플리케이션과 운영 구성 요소가 20개 등록되어 있습니다. 이 숫자는 Deployment나 Pod의 개수와 같지는 않습니다. 하나의 서비스가 프론트엔드와 백엔드, 데이터 저장소처럼 여러 리소스로 나뉠 수 있기 때문입니다.

현재 구성을 계층별로 정리하면 다음과 같습니다.

계층사용 중인 구성역할

운영체제 Rocky Linux 9.2 두 베어메탈 노드의 호스트 OS
컨테이너 오케스트레이션 Kubernetes Pod 배치와 상태 조정
Pod 네트워크 Flannel 노드와 Pod 사이의 클러스터 네트워크
외부 IP 제공 MetalLB 베어메탈 환경에서 LoadBalancer IP 제공
HTTP 진입점 NGINX Ingress Controller 도메인과 경로에 따라 Service로 라우팅
인증서 cert-manager, Let's Encrypt TLS 인증서 발급과 갱신 자동화
영구 스토리지 Longhorn 분산 볼륨과 복제본 관리
배포 관리 GitHub Actions, Kustomize, Argo CD 이미지 빌드부터 선언형 배포까지 자동화
비밀 값 관리 HashiCorp Vault, Argo CD Vault Plugin Git에 비밀 값을 남기지 않고 배포 시 주입
관측성 Prometheus, Grafana, Alertmanager 등 메트릭 수집, 대시보드, 알림

 

여기서 중요한 점이 하나 있습니다. 노드가 두 대라고 해서 클러스터 전체가 고가용성인 것은 아닙니다. 현재 컨트롤 플레인과 etcd는 단일 노드에 있고, 여러 애플리케이션도 한 개의 복제본을 기준으로 운영됩니다. 워커 한 대의 장애를 흡수할 여지는 있지만, 컨트롤 플레인 장애까지 자동으로 견디는 구조는 아닙니다. 이 부분은 뒤에서 다시 이야기하겠습니다.

사용자의 요청은 Pod까지 어떻게 들어올까?

SCG는 클라우드 사업자의 Load Balancer가 없는 베어메탈 환경에서 Kubernetes를 운영합니다. 그래서 외부 트래픽은 대략 다음 순서로 흐릅니다.

  1. 사용자가 서비스 도메인으로 접속합니다.
  2. MetalLB가 할당한 외부 IP로 요청이 들어옵니다.
  3. NGINX Ingress Controller가 도메인과 경로를 확인합니다.
  4. Ingress 규칙과 연결된 Kubernetes Service가 요청을 받습니다.
  5. Service가 실제 애플리케이션 Pod로 트래픽을 전달합니다.

TLS 인증서는 cert-manager가 Let's Encrypt와 연동해 발급하고 갱신합니다. 애플리케이션별 Ingress에서는 경로 재작성, CORS, 리다이렉트처럼 서비스에 필요한 규칙도 함께 선언합니다. 덕분에 애플리케이션 코드가 외부 네트워크의 세부 구성을 모두 알 필요는 없습니다.

클러스터 내부의 Pod 네트워크는 Flannel이 담당합니다. 단순하고 운영 경험이 많이 쌓인 구성이지만, 현재는 서비스 트래픽과 노드 간 통신이 같은 물리 네트워크 인터페이스를 공유합니다. 즉, Kubernetes 안에서 네트워크가 논리적으로 나뉘어 보여도 마지막에는 같은 1Gbps 링크를 지나게 됩니다. 이 점은 실제 장애를 겪으면서 꽤 중요한 교훈이 되었습니다.

데이터는 Longhorn으로 지킨다

컨테이너의 로컬 파일은 Pod가 다시 만들어질 때 사라질 수 있습니다. 데이터베이스나 대시보드처럼 상태를 보존해야 하는 구성 요소에는 영구 볼륨이 필요합니다.

현재 SCG는 CSI(Container Storage Interface) 구현으로 Longhorn을 사용합니다. Longhorn은 Kubernetes 노드의 디스크를 이용해 분산 볼륨을 만들고, 볼륨 데이터를 여러 복제본으로 보관합니다.

SCG 클러스터는 노드가 두 대이기 때문에 기본 복제본 수를 2로 맞췄습니다. 복제본 3개를 요청해도 서로 다른 세 번째 노드가 없기 때문입니다. 한 노드나 디스크에 문제가 생겼을 때 다른 노드의 복제본을 활용할 수 있다는 장점이 있지만, 이것 역시 클러스터 전체의 고가용성을 보장한다는 뜻은 아닙니다. 스토리지 복제와 컨트롤 플레인 고가용성은 서로 다른 문제입니다.

배포의 중심은 GitOps

SCG의 Kubernetes 설정에서 가장 눈에 띄는 부분은 서비스별 config 저장소입니다. GitHub 조직에는 이름에 config가 들어간 저장소가 현재 22개 있고, 하나의 거대한 중앙 저장소보다 각 서비스가 자신의 배포 설정을 관리하는 형태에 가깝습니다.

대부분의 config 저장소는 Kustomize의 baseoverlays 구조를 사용합니다.

base/
└── api/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    └── kustomization.yaml

overlays/
├── prod/
│   └── api/
│       ├── namespace.yaml
│       ├── secret.yaml
│       └── kustomization.yaml
└── qa/
    └── api/
        └── ...

base에는 환경과 무관하게 재사용할 Deployment, Service, Ingress를 두고, overlays에서는 운영 환경과 QA 환경의 네임스페이스, 도메인, 이미지 태그 같은 차이를 덧씁니다. 같은 리소스를 복사해서 조금씩 다르게 관리하는 대신, 공통 부분과 차이만 분리하는 방식입니다.

운영 환경의 overlay는 다음과 같은 형태입니다. 실제 저장소마다 서비스 이름과 경로는 다르지만, 공통 리소스를 가져와 네임스페이스와 이미지 버전을 환경에 맞게 지정한다는 원리는 같습니다.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: sample-prod

resources:
  - namespace.yaml
  - secret.yaml
  - ../../../base/api

images:
  - name: sample-api
    newName: <registry>/sample-api
    newTag: <commit-sha>

실제 배포 흐름은 다음과 같습니다.

  1. 애플리케이션 저장소에 코드가 반영됩니다.
  2. GitHub Actions가 컨테이너 이미지를 빌드해 레지스트리에 푸시합니다.
  3. 같은 워크플로가 config 저장소의 Kustomize 이미지 태그를 새 커밋 SHA로 바꿉니다.
  4. 변경된 config가 GitHub에 커밋됩니다.
  5. Argo CD가 Git의 변경을 감지하고 클러스터 상태와 비교합니다.
  6. AutoSync가 새 설정을 적용하고, 더 이상 선언되지 않은 리소스는 정리하며, 수동으로 달라진 리소스는 원래 상태로 되돌립니다.

Argo CD에서는 이를 다음과 같은 정책으로 선언합니다.

syncPolicy:
  automated:
    prune: true
    selfHeal: true

여기서 prune은 Git에서 제거된 리소스를 클러스터에서도 제거하고, selfHeal은 누군가 클러스터에서 직접 바꾼 값을 Git에 선언된 상태로 복구합니다. 결국 배포의 기준은 특정 서버에 남은 파일이나 담당자의 기억이 아니라 Git이 됩니다.

 

이 구조 덕분에 일반적인 배포에서 운영 서버에 SSH로 접속해 이미지를 직접 바꿀 필요가 없습니다. 배포 이력도 config 저장소의 커밋으로 남기 때문에, 어떤 이미지가 언제 운영에 반영됐는지 추적하기 쉬워집니다.

비밀 값은 Git 밖에서 주입한다

데이터베이스 비밀번호나 API 키까지 YAML에 그대로 적을 수는 없습니다. SCG는 HashiCorp Vault와 Argo CD Vault Plugin을 이용해 비밀 값을 분리합니다.

Git의 Secret 템플릿에는 실제 값 대신 Vault 경로와 플레이스홀더만 남깁니다.

apiVersion: v1
kind: Secret
metadata:
  name: sample-secret
  annotations:
    avp.kubernetes.io/path: <vault-path>
stringData:
  SOME_SECRET: <secret-key>

Argo CD가 매니페스트를 생성할 때 플러그인이 Vault에서 값을 읽어 플레이스홀더를 치환합니다. Git 저장소만 읽어서는 실제 비밀 값을 알 수 없고, 환경별 값도 애플리케이션 설정과 분리할 수 있습니다.

 

다만 Vault 자체의 복구 절차와 접근 권한 관리도 운영의 일부입니다. 도구를 붙였다고 보안이 자동으로 완성되는 것은 아닙니다. 비밀 값이 문서나 다른 설정 파일로 새지 않는지, 권한과 복구 자료가 적절히 관리되는지 계속 점검해야 합니다.

보이지 않으면 운영할 수 없다

현재 메트릭 수집과 대시보드는 Prometheus와 Grafana를 중심으로 구성하고, Alertmanager와 Slack 알림을 통해 이상 징후를 전달합니다. Kubernetes 노드뿐만 아니라 데이터베이스와 오브젝트 스토리지에도 exporter 또는 Prometheus endpoint를 연결해 서비스 바깥의 상태까지 함께 보고 있습니다.

 

로그·메트릭·트레이스를 Grafana 생태계로 모으는 작업도 진행 중입니다. 다만 일부 관측성 구성은 아직 모놀리식 아키텍처와 단일 복제본으로 운영되고 있습니다. 90일 보관 정책을 두어 장애를 되짚어볼 수 있는 기반은 만들었지만, 모니터링 시스템 자체의 고가용성은 앞으로 보강해야 합니다.

 

관측성의 필요성을 크게 느낀 사건도 있었습니다. 과거 특정 구성 요소의 오류와 로그가 서로 증폭되면서 클러스터 브리지의 트래픽이 1Gbps 물리 링크 한계에 가까워졌고, 외부 서비스와 관리 접속까지 함께 영향을 받은 적이 있습니다. 애플리케이션이 아니라 “로그를 수집하는 과정”도 장애의 원인이 될 수 있다는 것을 직접 확인한 셈입니다.

 

이후에는 필요한 로그만 수집하고, 과도한 쿼리 로그를 제한하며, 네트워크 사용량에 알림을 거는 방향으로 운영 기준을 다듬고 있습니다. 로그는 많을수록 좋은 것이 아니라, 장애를 설명할 만큼 충분하면서도 시스템을 압도하지 않아야 했습니다.

Kubernetes가 모든 문제를 해결해주지는 않았다

Kubernetes로 옮긴 뒤 배포와 복구 방식이 일관된 것은 분명합니다. 하지만 Kubernetes를 설치했다는 사실만으로 고가용성, 보안, 관측성이 자동으로 생기는 것은 아니었습니다.

현재 SCG 클러스터가 풀어야 할 숙제는 비교적 명확합니다.

  • 컨트롤 플레인 고가용성: 단일 컨트롤 플레인과 etcd는 여전히 단일 장애 지점입니다.
  • 애플리케이션 복제: 여러 서비스가 한 개의 Pod를 기준으로 운영되어, 무중단 업데이트와 장애 대응 범위에 한계가 있습니다.
  • 네트워크 분리: 서비스 트래픽, 스토리지 복제, 노드 관리 트래픽이 한 물리 링크를 공유합니다.
  • 관측성 고가용성: 장애를 보는 도구 자체도 여러 복제본과 안정적인 저장 구조가 필요합니다.
  • 설정 일관성: 오래된 레지스트리 참조와 deprecated된 Kustomize 문법을 정리하고, 잘못된 경로나 누락된 파일을 CI에서 미리 검증해야 합니다.

이 목록은 Kubernetes 도입의 실패라기보다, 현재 상태를 선언적으로 볼 수 있게 되면서 더 선명해진 다음 단계에 가깝습니다. 예전에는 각 서버 안에 흩어져 있던 차이가 이제 저장소와 대시보드에서 드러나기 시작했습니다.

다음 클러스터는 Talos Linux로

현재 운영 중인 Rocky Linux 2노드 클러스터와 별개로, SCG는 Talos Linux를 사용하는 차기 서버를 준비하고 있습니다. 새 장비는 물리 코어 48개와 메모리 544GB 규모이며, 2026년 7월 현재 아직 운영 클러스터에는 투입되지 않은 상태입니다.

 

Talos Linux는 Kubernetes 운영을 위해 만들어진 immutable OS입니다. 일반적인 서버처럼 SSH로 접속해 패키지와 설정을 하나씩 바꾸기보다, API와 선언형 설정을 통해 노드를 관리합니다. 지금까지의 GitOps 운영 방식을 노드 계층까지 더 일관되게 확장할 수 있다는 점이 기대되는 부분입니다.

 

차기 구성에서는 단순히 더 큰 서버를 추가하는 것보다, 세 개 이상의 etcd 멤버를 통한 컨트롤 플레인 고가용성, 네트워크 경로 분리, 스토리지와 모니터링의 장애 범위를 함께 검토하고 있습니다. 다만 이는 아직 목표 구성이고, 이 글에서 설명한 현재 운영계와는 구분해야 합니다.

마무리

SCG의 Kubernetes는 거대한 클라우드 환경도, 모든 계층이 이중화된 완성형 플랫폼도 아닙니다. 두 대의 베어메탈 서버에서 시작해 실제 학내 서비스를 옮기고, 배포와 설정을 Git으로 모으고, 장애를 겪을 때마다 다음 운영 기준을 추가해온 결과에 가깝습니다.

 

그 과정에서 가장 크게 달라진 것은 “누가 서버에 들어가서 무엇을 했는가”보다 “우리가 원하는 상태가 무엇인가”를 먼저 이야기하게 되었다는 점입니다. GitHub Actions가 이미지를 만들고, config 저장소가 버전을 기록하고, Argo CD가 클러스터를 그 상태에 맞추는 흐름은 담당자가 바뀌어도 남습니다.

 

물론 아직 할 일은 많습니다. 단일 컨트롤 플레인을 고가용성으로 바꾸고, 네트워크 병목을 줄이고, 관측성 구성도 장애를 견디게 만들어야 합니다. 다음에는 Talos Linux 기반 차기 클러스터를 준비하면서 어떤 선택을 했고, 기존 클러스터에서 무엇을 가져가고 무엇을 바꿨는지도 정리해보겠습니다.

 

읽어주셔서 감사합니다!