Kubernetes로 안전한 멀티테넌트(Multi-Tenant) 플러그인 아키텍처 구축기

Harrison Jung·2025년 9월 24일

Kubernetes로 안전한 멀티테넌트(Multi-Tenant) 플러그인 아키텍처 구축기

안녕하세요! 오늘은 저희 챗봇 서비스에 외부 시스템(ERP, CRM 등)을 연동할 수 있는 'API 연동 플러그인' 기능을 개발하며 고민하고 해결했던 과정을 공유하고자 합니다. 목표는 사용자가 직접 작성하거나 AI가 생성한 코드를 우리 시스템 위에서 안전하게 실행하는 것이었고, 이 과정에서 Kubernetes를 어떻게 활용하여 보안과 격리, 동적 리소스 관리를 구현했는지 자세히 다뤄보겠습니다.

1. 목표: 안전한 코드 실행 환경

가장 큰 도전 과제는 "어떻게 사용자의 코드를 안전하게 실행할 것인가?" 였습니다. 사용자의 코드가 다른 사용자나 시스템 전체에 영향을 주어서는 안 되기 때문에, 강력한 샌드박스(Sandbox) 환경이 필수적이었습니다. 저희는 Kubernetes의 다양한 기능을 활용하여 여러 겹의 보안 계층을 쌓기로 했습니다.

첫 번째 방어선: Namespace와 NetworkPolicy

가장 먼저, 플러그인 코드가 실행될 환경을 worker라는 별도의 Namespace로 분리하여 논리적 격리 단위를 만들었습니다. 그리고 NetworkPolicy를 적용하여 네트워크 트래픽을 엄격하게 제어했습니다.

  • Ingress(인바운드) 차단: 기본적으로 외부에서의 모든 접근을 차단합니다. 단, 내부의 console 서버와 같이 내부 시스템에서 API를 호출해야 하는 경우를 위해, 특정 Namespace(예: turing)로부터의 트래픽만 허용하도록 설정했습니다.
  • Egress(아웃바운드) 허용: 플러그인이 외부 파트너사의 API를 호출해야 하므로, 아웃바운드 트래픽은 모두 허용했습니다.
# k8s-api-plugin.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: worker-internal-ingress-policy
  namespace: worker
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    # 'turing' 네임스페이스의 Pod들만 접근 허용
    - namespaceSelector:
        matchLabels:
          kubernetes.io.metadata.name: turing
    ports:
      - protocol: TCP
        port: 80
  egress:
  - {} # 모든 아웃바운드 허용

두 번째 방어선: Pod 보안 컨텍스트 (SecurityContext)

Pod 레벨에서는 SecurityContext를 활용하여 컨테이너가 할 수 있는 작업을 최소한으로 제한했습니다. 이는 "최소 권한의 원칙"을 적용한 핵심적인 부분입니다.

  • Root 권한 실행 방지: runAsNonRoot: truerunAsUser: 1001 등으로 root가 아닌 일반 유저로만 컨테이너가 실행되도록 강제했습니다.
  • 권한 상승 방지: allowPrivilegeEscalation: false로 컨테이너 내부 프로세스가 더 높은 권한을 획득하는 것을 막았습니다.
  • 읽기 전용 파일 시스템: readOnlyRootFilesystem: true로 설정하여, 악성 코드가 시스템 파일을 변경하거나 임의의 파일을 쓰는 행위를 원천 차단했습니다.
  • Capability 제거: capabilities: { drop: ["ALL"] }을 통해 컨테이너에 부여될 수 있는 모든 리눅스 커널 권한을 제거하여 공격 표면을 최소화했습니다.

2. 과제: 동적 리소스 관리와 격리

보안 환경을 구축한 뒤, 수많은 사용자들이 각자의 플러그인을 배포해야 하는 멀티테넌시 환경을 고려해야 했습니다. 처음에는 모든 플러그인이 하나의 ConfigMap을 공유하는 방식을 생각했지만, 이는 다른 사용자의 코드에 영향을 줄 수 있어 위험했습니다.

해결: 리소스 이름 동적 생성

저희는 각 챗봇별로 고유한 리소스를 생성하는 방식으로 이 문제를 해결했습니다.

  • 동적 ConfigMap: 기존의 정적인 user-code-cm 대신, api-plugin-code-<chatbot-id>와 같이 각 챗봇별로 고유한 이름의 ConfigMap을 생성합니다.
  • 동적 Deployment: Deployment 역시 api-plugin-<chatbot-id> 형식의 이름을 가지며, 볼륨 설정에서 자신에게 맞는 동적 ConfigMap을 참조하도록 했습니다.

이 로직은 Node.js의 @kubernetes/client-node 라이브러리를 사용하여 gke-deployment.js 모듈에 구현했습니다. 배포 시에는 chatbotUiduserCode를 받아 고유한 이름의 ConfigMapDeployment를 생성하고, 삭제 시에는 관련된 모든 리소스를 함께 정리하여 리소스 유출을 방지합니다.

// gke-deployment.js의 일부

// GKE에 Workload/Service 배포
async function deployWorkload(chatbotUid, userCode) {
    // ...
    const appName = `api-plugin-${chatbotUid}`;
    const codeName = `api-plugin-code-${chatbotUid}`; // 고유한 ConfigMap 이름 생성

    // 매니페스트 생성
    const configMapManifest = getConfigMapManifest(codeName, userCode);
    const deploymentManifest = getDeploymentManifest(appName, codeName, /*...*/);
    // ...
}

// GKE에서 Workload/Service 삭제
async function deleteWorkload(chatbotUid) {
    // ...
    const appName = `api-plugin-${chatbotUid}`;
    const codeName = `api-plugin-code-${chatbotUid}`;

    // Deployment, Service, ConfigMap 모두 삭제
    // ...
}

3. 또 다른 과제: ConfigMap 변경 시 자동 재배포

개발을 진행하던 중, 사용자가 코드를 수정하고 재배포해도 실행 중인 Pod이 업데이트되지 않는 문제를 발견했습니다. Kubernetes Deployment는 참조하는 ConfigMap의 내용 변경을 감지하지 못하기 때문이었죠.

해결: Timestamp Annotation 패턴

이 문제를 해결하기 위해 "Timestamp Annotation" 패턴을 도입했습니다.

  1. deployWorkload 함수가 호출될 때마다 Date.now()를 사용하여 현재 시간의 타임스탬프를 생성합니다.
  2. 이 타임스탬프 값을 Deployment의 Pod 템플릿(spec.template)의 annotations에 기록합니다.
  3. Deployment는 자신의 템플릿 어노테이션이 변경된 것을 감지하고, Pod들을 새 버전으로 교체하는 롤링 업데이트를 자동으로 시작합니다.

이 간단한 방법으로, 재배포 시 항상 최신 코드가 담긴 ConfigMap을 사용하는 새로운 Pod이 실행되도록 보장할 수 있었습니다.

// gke-deployment.js의 일부

function getDeploymentManifest(appName, codeName, deployTimestamp) {
    return {
        // ...
        "template": {
            "metadata": {
                "annotations": {
                    "redeploy/timestamp": deployTimestamp // 이 값이 바뀔 때마다 재배포!
                }
            },
            // ...
        }
    }
}

async function deployWorkload(chatbotUid, userCode) {
    // ...
    const deployTimestamp = Date.now().toString(); // 재배포를 위한 타임스탬프 생성
    const deploymentManifest = getDeploymentManifest(appName, codeName, deployTimestamp);
    // ...
}

마무리하며

지금까지 Kubernetes 환경에서 외부 코드를 안전하게 실행하기 위한 멀티테넌트 플러그인 아키텍처를 구축한 경험을 공유해 드렸습니다. Namespace, NetworkPolicy, SecurityContext를 통해 강력한 보안 샌드박스를 구축하고, 동적 리소스 생성 및 정리로 테넌트 간 격리를 보장했으며, Annotation 패턴을 이용해 ConfigMap 변경 시 자동 재배포 문제를 해결했습니다.

이 과정을 통해 Kubernetes가 제공하는 다양한 기능들을 깊이 있게 활용하며, 안전하고 확장성 있는 시스템을 만드는 즐거움을 다시 한번 느낄 수 있었습니다. 이 글이 비슷한 고민을 하는 다른 개발자분들께 작은 도움이 되기를 바랍니다.

감사합니다.

profile
차세대 생성형 AI 위키 서비스 "두루미스 위키"를 만들고 있는 개발자

0개의 댓글