
제한된 액세스, 관리형 비밀(Secrets), 자가 치유 배포: 우리의 Kubernetes 전환 과정
요약
Rhesis 팀이 Cloud Run에서 Kubernetes로 인프라를 마이그레이션하며 겪은 과정과 최종 아키텍처를 다룹니다. 보안 강화를 위해 네트워크 봉쇄, 관리형 비밀 관리, 자가 치유 배포 시스템을 구축한 사례를 설명합니다.
핵심 포인트
- Cloud Run의 한계를 극복하기 위한 Kubernetes 전환
- WireGuard VPN을 활용한 Kubernetes API 서버의 네트워크 보안 강화
- External Secrets Operator를 통한 체계적인 비밀 관리
- ArgoCD와 플랫폼 툴링을 활용한 선언적 인프라 관리
Md Asaduzzaman Miah
2026년 7월 21일 • 읽기 시간 8분
서론 (Introduction)
Rhesis의 인프라는 즉시 사용 가능한 클라우드 배포 솔루션인 GCP의 Cloud Run에 의존하며 시작되었습니다. 이는 소규모 팀이 인프라에 엔지니어링 시간을 소비하지 않고도 빠르게 제품을 출시하고 저렴하게 운영할 수 있도록 선택되었으며, 하나의 클라우드 프로젝트가 모든 환경을 커버했습니다. 당시 규모에서는 합리적인 설정이었으며, 이를 넘어선 것은 실패가 아니라 예상된 결과였습니다. 환경들은 이미 초과해버린 신뢰 경계(trust boundary)를 공유하고 있었고, 100개의 비밀(secret) 저장소 제한이 있는 GitHub Actions secrets에 모든 것을 담아두었던 비밀 관리(secrets management) 방식 또한 한계에 부딪혔습니다. Kubernetes로의 마이그레이션은 규모 확장에 앞서 진행한 강화 작업이었으며, 우리가 곧 맞이하게 될 고객 관계에 영향을 미치기 전에, 그리고 우리만의 일정에 맞춰 수행할 수 있는 여유가 있을 때 그 격차를 메우기 위한 조치였습니다.
아래의 Figure 1은 우리가 도달한 최종 상태를 보여줍니다: 네트워크 및 클러스터 레이어와 그 위에서 실행되는 애플리케이션 서비스입니다.
관리형 Kubernetes 클러스터 내부에서 실행되는 플랫폼 툴링과 Rhesis 애플리케이션 서비스를 보여주는 아키텍처 개요 다이어그램
Figure 1: 아키텍처 개요(Architecture Overview). 관리형 Kubernetes 클러스터 내부에서 실행되는 플랫폼 툴링(ArgoCD, kGateway, cert-manager, External Secrets Operator, 관측성 스택(observability stack)) 및 Rhesis 애플리케이션 서비스(Backend, Frontend, Worker, DB), 이들이 의존하는 외부 시스템(secrets engine, DNS provider, Let's Encrypt, S3, GitHub), 그리고 관리자가 WireGuard VPN을 통해 클러스터에 접속하고 사용자가 앱 게이트웨이를 통해 접속하는 방식.
최종 아키텍처 (Final Architecture)
네트워크 봉쇄 (Locking down the network)
Rhesis는 이제 환경당 하나씩, 프라이빗 Kubernetes 클러스터에서 실행됩니다. Kubernetes API 서버는 퍼블릭 엔드포인트(public endpoint)를 갖지 않으며, 승인된 네트워크를 통해서만 접근할 수 있습니다. 그리고 승인된 유일한 네트워크는 전용 WireGuard VPN뿐입니다. 클러스터 네트워크 앞에 배치된 베스천 호스트(bastion host)도 없습니다. VPN 피어(peer)가 클러스터 네트워크로 직접 연결됩니다. 피어는 선언적(declaratively)으로 정의되며, 각 피어는 정적 IP 및 접근 가능한 환경 목록과 명시적으로 연결됩니다. 액세스는 암묵적이지 않습니다. dev 환경에 접근할 수 있는 사람이라고 해서 staging이나 production 환경으로 가는 경로를 자동으로 얻지는 못합니다.
이러한 봉쇄 조치는 과거에 무심코 퍼블릭으로 노출되었던 영역에도 동일하게 적용됩니다. 모든 클러스터는 두 개의 별도 인그레스 클래스(ingress classes)를 노출합니다. 하나는 퍼블릭 로드 밸런서(public load balancer)를 기반으로 하는 외부(external) 클래스이고, 다른 하나는 WireGuard 네트워크를 통해서만 접근 가능한 프라이빗 로드 밸런서(private load balancer)를 기반으로 하는 내부(internal) 클래스입니다. 인터넷에 노출될 필요가 없는 모든 것, 즉 대시보드, 배포 도구, 대부분의 경우 개발(development) 및 스테이징(staging) 환경은 내부(internal) 뒤로 배치됩니다. 아래의 그림 2(Figure 2)는 이러한 분리를 보여줍니다. 외부 게이트웨이(external gateway)로 들어오는 퍼블릭 트래픽, WireGuard를 통해 내부 게이트웨이(internal gateway)로 터널링되는 관리자 및 내부 트래픽, 그리고 두 곳 모두에 연결된 cert-manager 및 external-dns를 확인할 수 있습니다. 이는 우리가 처음에 직면했던 문제에 대한 직접적인 해결책입니다. 이제 기본적으로 퍼블릭인 것은 아무것도 없으며, 의도적으로 해당 경로로 라우팅되어야만 합니다.
그림 2: 트래픽 라우팅(Traffic Routing), DNS 및 TLS. 관리자가 WireGuard VPN을 통해 내부 게이트웨이에 접근하고, 퍼블릭 사용자가 HTTPS를 통해 외부 게이트웨이에 접근합니다. cert-manager는 TLS 인증서를 발급하고 external-dns는 외부 게이트웨이 경로에 대한 DNS 레코드를 관리하며, ZeroSSL/Let's Encrypt는 Cloudflare DNS를 통해 ACME 검증을 처리합니다.
비밀 정보(Secrets)와 액세스, 재구축
Secrets(비밀 정보)는 더 이상 Git이나 CI에 존재하지 않습니다. 대신 외부 비밀 엔진(external secret engine)에 저장되며, External Secrets Operator가 ClusterSecretStore를 통해 이를 클러스터 내부의 네이티브 Kubernetes Secrets로 동기화합니다. 인증은 어딘가 저장소에 놓여 교체되거나 유출되기를 기다리는 정적 클라우드 키(static cloud key)가 아니라, 워크로드 ID(workload identity)를 통해 이루어집니다.
각 환경은 해당 환경에 필요한 비밀 정보로만 범위가 제한된(scoped) 고유의 클라우드 ID를 부여받습니다. 이는 정책 문서가 아닌 인프라 구조(plumbing) 자체에 의해 강제되는 최소 권한 원칙(least privilege)입니다. 즉, 개발용(dev) 자격 증명이 탈취되더라도 프로덕션(production) 비밀 정보로 접근할 수 있는 경로가 없습니다. 해당 자격 증명 뒤에 있는 ID에 프로덕션 권한이 부여된 적이 없기 때문입니다. 아래 그림 3은 그 경로를 보여줍니다. 한쪽에는 외부 비밀 엔진이 있고, 다른 한쪽에는 Kubernetes Secrets로 동기화하는 Operator가 있으며, 백엔드(backend)와 워커(worker)가 해당 비밀 정보를 직접 소비합니다.

그림 3: External Secrets Operator, Rhesis 애플리케이션 및 데이터베이스. 외부 비밀 엔진이 External Secrets Operator로 동기화하면, Operator가 Kubernetes Secrets를 생성 및 업데이트합니다. 이와 별개로, 앱 게이트웨이(app gateway)가 프론트엔드(Frontend)와 백엔드(Backend)로 라우팅하며, 프론트엔드는 API를 통해 백엔드를 호출하고, 백엔드와 워커는 모두 PostgreSQL 및 Valkey에서 데이터를 읽습니다.
실제 상태(live state)의 무결성 유지
이제 모든 배포는 ArgoCD를 통해 진행됩니다. 각 클러스터의 희망 상태(desired state)는 Git에 선언되며, ArgoCD는 Pruning(가지치기) 및 Self-healing(자가 치유) 기능을 활성화한 상태로 실제 클러스터와 희망 상태를 지속적으로 조정(reconcile)합니다. 따라서 Git 외부에서 수동으로 수행된 kubectl 변경 사항은 문서화된 내용과 조용히 어긋나는 대신, 자동으로 감지되어 원래 상태로 되돌려집니다.
이는 직접적인 감사(Audit) 결과로 이어집니다. 즉, "프로덕션에서 실행 중인 것"과 "배포 리포지토리(deployment repo)에 커밋된 것"은 사실상 동일한 질문이 되며, 이를 증명할 공유된 이력을 갖게 됩니다. 오늘날의 모든 배포는 차이점(diff), 작성자, 타임스탬프가 자동으로 첨부된 커밋을 남깁니다. 아래 다이어그램은 변경 사항이 거치는 전체 경로를 보여줍니다: 애플리케이션 리포지토리에서 시작하여, CI와 컨테이너 레지스트리(container registry)를 거쳐, 배포 리포지토리로 들어간 뒤, ArgoCD를 통해 클러스터로 전달됩니다.
그림 4: 배포 자동화(Deployment Automation). CI는 이미지를 빌드하여 컨테이너 레지스트리에 푸시하는 동시에 배포 리포지토리의 이미지 참조를 업데이트합니다. 그러면 클러스터는 레지스트리에서 이미지를 직접 가져오고(pull), ArgoCD는 배포 리포지토리에 선언된 내용에 따라 클러스터를 동기화(sync) 및 조정(reconcile)합니다.
모니터링 및 디버깅 (Monitoring and debugging)
우리는 이전에 존재하지 않았던 관측성(Observability) 스택을 추가했습니다: 메트릭(metrics) 및 알림(alerting)을 위한 Prometheus, 모든 노드에서 데몬셋(DaemonSet)으로 실행되는 수집기(collector)인 Grafana Alloy, 그리고 Alloy가 로그를 전송하는 로그 저장소(log store)인 Loki입니다. 공개될 필요가 없는 다른 모든 요소와 마찬가지로 Grafana 자체도 내부 인그레스 클래스(internal ingress class) 뒤에 위치하므로, 시스템의 가시성을 확보하면서도 시스템을 노출하는 대가를 치르지 않았습니다. 아래 다이어그램은 데이터 흐름을 보여줍니다: 애플리케이션이 메트릭을 Prometheus로 직접 전달하고, Alloy가 로그를 수집하여 Loki로 전달하며, 두 데이터 모두 단일 확인 지점인 Grafana에 도달합니다.
그림 5: 관측성 (Observability). 애플리케이션이 Prometheus에 메트릭을 공급하고, Grafana Alloy가 로그를 수집하여 Loki로 전달하며, 두 데이터 모두 Grafana에서 공유된 뷰로 통합됩니다.
비용 측면 (The Cost Picture)
투입된 비용
세 명의 인원이 나누어 작업하여 총 4개월이 소요되었습니다. 우리는 환경 설계를 위해 프리랜서 DevOps 엔지니어인 Pit Wegner를 영입했고, 구현은 직접 담당했습니다. 그와 함께 한 명이 조정 및 감독을 이끌었습니다. 세 명의 노력이 투입되어, 제품 출시를 중단하지 않으면서도 단 한 분기 만에 회사의 배포 파이프라인과 전체 액세스 모델을 재구축했습니다.
또한, 한 가지 측면에서는 일상적인 유지보수가 더 어려워졌습니다. GitOps 도구 위에 프라이빗 클러스터 (Private clusters)와 VPN 레이어가 추가되면서, 단일 Cloud Run 배포 버튼을 사용할 때보다 움직여야 할 구성 요소가 더 많아졌습니다. 소규모 팀에게는 이러한 오버헤드가 직접적으로 느껴집니다. 새로운 엔지니어가 클러스터를 살펴보기도 전에 단순히 클라우드 로그인을 하는 것이 아니라 VPN 액세스 설정부터 완료해야 하기 때문입니다.
애플리케이션 트러블슈팅 (Troubleshooting)은 더 쉬워졌습니다. 이전에는 제대로 된 관측성 스택 (Observability stack)이 없어서, 운영 환경의 문제를 디버깅하려면 여러 서비스에 걸쳐 로그를 수동으로 하나씩 맞춰봐야 했습니다. 이제 Grafana와 Loki는 이전에는 존재하지 않았던 공유되고 쿼리 가능한 (Queryable) 뷰를 제공합니다. 배포 또한 더 예측 가능해졌습니다. ArgoCD를 통해 변경 사항을 푸시함으로써, 수동으로 서비스마다 진행하던 Cloud Run 푸시를 대신하여, 먼저 차이점(Diff)을 확인하고 논리적으로 추론하며 롤백할 수 있는 방식으로 대체되었습니다.
교훈 (Lessons Learned)
트레이드오프 (The trade-off)
우리가 Kubernetes로 전환한 이유는 Cloud Run이 확장(Scale)할 수 없어서가 아닙니다. Cloud Run은 충분히 잘 확장되었습니다. 우리가 전환한 이유는 "스테이징 환경이 공개되어 있고 어떤 엔지니어도 운영 환경에 접근할 수 있다"라는 문장이 1년 뒤에도 여전히 사실이기를 원치 않았기 때문이며, 민감도 계층화(Sensitivity tiering)가 없는 100개의 비밀값(Secret) 제한은 팀이 성장할수록 감당하기 점점 더 어려워지기 때문입니다. 비용은 실재했습니다. 4개월의 시간, 3명의 인력, 그리고 소규모 팀에게는 영구적으로 증가한 운영 표면적 (Operational surface area)이 그 대가였습니다.
그 결과로 얻은 것은 네트워크 액세스 범위가 지정 및 선언되고, 비밀(Secrets)이 환경별 ID를 통해 중앙에서 관리되며, 모든 변경 사항이 Git에 기록되고 자가 치유(Self-healing)되는 시스템이었습니다. 예상외로 디버깅(Debugging)은 이전보다 더 쉬워졌습니다.
마치며 (Wrapping Up)
우리가 피하고 싶었던 마이그레이션 버전은 블로그 포스트가 아닌 장애 보고서(Incident report)로 시작되는 형태였습니다. 결정 사항을 폐기하는 이유가 공개적인 실패 때문이 아니라, 팀의 규모가 커져 더 이상 적합하지 않기 때문이라는 점이 바로 앞서 계획하는 것과 사후 수습하는 것을 가르는 차이점입니다. 1~2년 전 팀에게 합리적이었던 환경 및 비밀(Secrets) 설정이 현재의 팀에게도 여전히 합리적인지 확인해 볼 가치가 있습니다.
자주 묻는 질문 (FAQ)
왜 Rhesis는 Cloud Run에서 Kubernetes로 마이그레이션했나요?
우리의 인프라는 환경 전체에 걸쳐 단일 신뢰 경계(Trust boundary)를 가진 공유 클라우드 프로젝트의 규모를 넘어섰고, 비밀(Secrets) 관리는 GitHub의 저장소당 100개 비밀 제한에 걸렸습니다. Kubernetes로의 마이그레이션은 규모 확장에 대비해 우리가 미리 수행한 보안 강화(Hardening)였으며, 우리가 곧 맞이하게 될 고객 관계에 영향을 미치기 전에 그 격차를 해소하는 작업이었습니다.
마이그레이션에 얼마나 걸렸으며, 누가 작업했나요?
4개월이 걸렸습니다. 환경 설계를 위해 프리랜서 DevOps 엔지니어인 Pit Wegner를 영입했고, 이후 구현은 저희가 직접 담당했으며, 한 명이 전체 노력을 조정하고 감독했습니다.
마이그레이션 이후 비밀(Secrets)은 어떻게 관리되나요?
비밀(Secrets)은 관리형 비밀 저장소(Managed secrets store)에 저장되며, 워크로드 ID(Workload identity)를 통해 인증된 ClusterSecretStore를 통해 External Secrets Operator에 의해 클러스터로 동기화됩니다. 따라서 CI나 Git에 정적 클라우드 키(Static cloud keys)가 남지 않습니다.
Kubernetes 클러스터에 액세스할 수 있는 유일한 방법은 무엇인가요?
전용 WireGuard VPN입니다. Kubernetes API 서버에는 퍼블릭 엔드포인트(Public endpoint)가 없으며, 배스천 호스트(Bastion host)도 존재하지 않습니다. 액세스는 명시적인 환경별 권한을 가진 선언된 WireGuard 피어(Peers)를 통해서만 가능합니다.
Kubernetes로의 전환이 트러블슈팅(troubleshooting)을 더 어렵게 만들었나요?
아니요, 오히려 더 쉬워졌습니다. 마이그레이션을 통해 Cloud Run 환경에서는 존재하지 않았던 Prometheus, Loki, Grafana Alloy와 같은 관측성 스택(observability stack)이 추가되었으며, 이를 통해 팀은 처음으로 로그와 메트릭(metrics)에 대해 공유 가능하고 쿼리 가능한(queryable) 뷰를 갖게 되었습니다.
참고 문헌 (References)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기


