고객사 RDS 통합 모니터링 플랫폼 구축 — Part 1: 배경
사내 DB 모니터링 포털 시리즈 1편입니다. Grafana + VictoriaMetrics 풀스택을 한 달 만에 접고 AWS API + 자체 포털로 갈아엎은 의사결정, 현재 아키텍처로 회귀한 배경, 그리고 이를 Claude Code 의 도움을 받아 5주 만에 완성한 기록을 담았습니다.
30개 고객사 100여 대의 RDS 가 한 화면에 들어와 있습니다.
회사에서 운영 중인 RDS 100여 대를 매일 들여다보는 일이 있습니다. 고객사가 30개쯤 되고, 엔진은 MySQL, Aurora MySQL, PostgreSQL, Aurora PostgreSQL, Oracle, MariaDB 가 섞여 있습니다. 처음에는 고객사마다 AWS 콘솔에 일일이 로그인해서 CloudWatch 그래프를 보고, EOS(End-of-Support) 일정을 따로 엑셀로 관리하고, 주간 보고서를 손으로 적었습니다. 이걸 자동화해 보려고 시작한 작업이 도중에 한 번 방향을 크게 바꾸었고, 다시 손본 결과가 위 스크린샷입니다.
이 글에는 그 처음 갔던 길 과 다시 잡은 방향, 그리고 5주 동안 Claude Code 와 함께 이 시스템을 만들어 본 기록을 한 편에 담았습니다. 시리즈로 이어 갈 예정이라, 다음 편들에서는 APScheduler 9개 잡, 알림 체계, Cross-Account IAM, EOS 와 정기점검 자동화를 하나씩 다룰 생각입니다. 1편인 이 글은 전체 그림 이 주제입니다.
처음 생각했던 그림
처음에 떠올린 모니터링 그림은 시장에서 흔히 보이는 형태였습니다. 메트릭은 Prometheus 호환 시계열 DB 인 VictoriaMetrics 에 모으고, 시각화는 Grafana 에서 합니다. 고객사 RDS 메트릭은 각 DB 에 붙는 Exporter (mysqld_exporter, postgres_exporter, oracledb_exporter) 가 뽑아 주고, 그걸 Grafana Alloy 가 30초마다 scrape 해서 중앙 VictoriaMetrics 로 remote_write 합니다. 멀티테넌시는 VictoriaMetrics 의 accountID 파티션 과 vmauth Bearer 토큰, 그리고 Grafana Organization 세 겹으로 분리합니다.
설계 자체는 잘 알려진 패턴이라 망설일 이유가 없었습니다. K3s 단일 노드 위에 vminsert / vmselect / vmstorage 클러스터를 띄우고, vmauth 를 외부 진입점으로 두고, Grafana 는 Org 별로 viewer 계정을 묶었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[고객사 RDS]
│ scrape :9104 / :9187 / :9161
↓
[고객사 Bastion: Alloy + 3 exporter]
│ remote_write HTTPS :8427
↓
── 중앙 EC2 (k3s, t2.xlarge) ──
┌──────────┐ ┌────────────┐
│ vmauth │ → │ vminsert │
└──────────┘ └─────┬──────┘
↓
┌───────────────────┐
│ vmstorage │
│ (accountID part) │
└─────────┬─────────┘
↑
┌─────────┴─────────┐
│ vmselect │
└─────────┬─────────┘
↑
┌───────────────────┐
│ Grafana │
│ Org per Customer │
└───────────────────┘
1차 시도. K3s 단일 노드 위 VictoriaMetrics 와 Grafana 풀스택.
자동 온보딩 스크립트도 같이 짰습니다. 신규 고객사가 들어오면 한 줄 실행으로 vmauth READ/WRITE 토큰 한 쌍을 발급하고, Grafana Org 와 Datasource 와 대시보드 JSON 을 일괄 import 하고, viewer 계정을 만들어서 Org 멤버십을 묶고, Main Org 에서 빼는 5단계가 자동으로 돌았습니다. 처음 두 고객사에 대해서는 이게 정말 잘 동작했습니다.
문제는 세 번째 고객사부터 일어났습니다.
부딪힌 벽
그쪽 길을 접게 된 이유는 한 가지가 아니었습니다. 세 가지 문제가 거의 동시에 다가왔습니다.
첫 번째는 고객사 네트워크 다양성이었습니다. 30개 고객사를 둘러보면 환경이 제각각이었습니다. Bastion 이 있는 곳, 없는 곳, NAT Gateway 가 없는 곳, RDS 가 완전히 격리된 Private Subnet 에 있어서 외부와 통신 자체가 불가능한 곳. 대부분이 마지막 케이스에 가까웠습니다. 이건 Alloy 가 outbound HTTPS 8427 을 통해 중앙으로 remote_write 하는 그림에 정면으로 부딪힙니다. NAT GW 가 없으면 outbound 자체가 안 됩니다.
두 번째는 Transit Gateway 비용이었습니다. 위 문제를 풀려면 중앙 VPC 와 고객사 VPC 를 Transit Gateway 로 묶어야 합니다. AWS Transit Gateway 는 연결 단위로 시간당 과금이라, 고객사당 월 36 달러 정도가 고정으로 붙습니다. 여기에 데이터 처리 비용이 GB 당 0.02 달러입니다. 30개 고객사를 다 묶으면 연결료만 월 1,000 달러를 넘고, 데이터 처리 비용까지 합치면 수백만 원 단위가 됩니다. 모니터링 비용이 모니터링하려는 서비스의 가치를 넘어서는 순간 이었습니다.
세 번째는 운영 부담이었습니다. 두 고객사에 1차로 깔면서 부딪힌 이슈들이 15개 이상 쌓였습니다.
이슈의 총합을 누적하게 되면 오랜 시간이 흘러도 혼자 감당할 양이 아니라는 결론이 섰습니다. 두 번째 고객사에서 발견된 문제가 세 번째 고객사에서는 또 다른 모양으로 나타납니다. 환경이 다르기 때문입니다.
결국에는 풀스택 모니터링 솔루션을 무기한 보류하기로 마음먹었습니다. 그동안 짠 K3s manifest, Grafana 대시보드 템플릿 (MySQL/PG/Oracle 각각), exporter 설치 스크립트, vmauth 토큰 발급 자동화는 그대로 저 멀리.. Holding 해 두었습니다. 향후 고객사 환경이 일관되게 정리되거나, Transit Gateway 도입에 대한 별도 합의가 생기면 다시 꺼내 볼 수 있을 것 같습니다.
방향 전환
보류를 결정하고 나서 한 가지 발상에 도달했습니다. 고객사 네트워크에 손대지 말자. 메트릭 수집은 고객사 RDS 가 아니라 AWS 자체 에서 가져옵니다. CloudWatch 와 Performance Insights 는 이미 AWS 가 RDS 메트릭을 자체적으로 모으고 있고, 그걸 API 로 꺼내볼 수 있습니다. 그러면 고객사 네트워크는 한 줄도 건드릴 필요가 없습니다.
이걸 가능하게 만드는 게 AWS STS AssumeRole + Cross-Account IAM Role 입니다. 고객사 AWS 계정에 IAM Role 을 하나만 만들어 받습니다. Trust Policy 에 중앙 계정 Principal 과 ExternalId 를 박아둡니다. 중앙에서는 boto3 로 sts.assume_role() 을 호출해서 임시 자격증명을 받고, 그걸로 cloudwatch.get_metric_data(), pi.get_resource_metrics(), rds.describe_db_instances() 같은 read-only API 를 호출합니다. 고객사가 해야 할 일은 IAM Role JSON 두 개 등록 뿐입니다. exporter 도, Alloy 도, Bastion 도 필요 없습니다.
물론 잃는 것도 있습니다. Grafana 의 시각화 자유도가 가장 큽니다. PromQL 로 자유롭게 쿼리를 짜서 패널을 만들고, 변수 인터폴레이션과 정규식 매칭으로 다중 인스턴스를 한 화면에 묶는 자유도가 사라집니다. 대신 얻는 게 압도적이었습니다. 고객사 환경 다양성에 무관 하고, 추가 인프라 비용이 0 이며, exporter 호환성 문제가 영구히 사라집니다.
“AWS 가 이미 해 주는 일을 포털로 예쁘게 싸자.” 이 한 줄이 방향 전환의 전부였습니다.
시각화는 Grafana 대신 React 로 자체 포털을 만들기로 했습니다. 어차피 EOS 현황, 인스턴스 인벤토리, 정기점검 보고서 PDF 출력처럼 Grafana 가 잘 다루지 못하는 것들이 더 많이 필요해질 텐데, 차라리 처음부터 포털 형태로 가는 게 종합해서 보면 훨씬 단순한 길이었습니다.
현재 아키텍처
다시 만든 그림은 이렇습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Customer A AWS Customer B AWS
┌──────────────────┐ ┌──────────────────┐
│ RDS × N │ │ RDS × N │
│ IAM Role (Trust) │ │ IAM Role (Trust) │
└────────┬─────────┘ └────────┬─────────┘
│ │
└──────────────┬────────────────────┘
│
↓ STS AssumeRole + ExternalId
(boto3 / 45min cache)
── Central EC2 (Seoul, t2.xlarge) · k3s single node ──
ns: portal
┌──────────────────────────────────────────────┐
│ portal-backend (FastAPI :8000) │
│ ├─ APScheduler 9 jobs │
│ └─ boto3 (CW / PI / RDS API) │
├──────────────────────────────────────────────┤
│ portal-frontend (nginx · React static) │
├──────────────────────────────────────────────┤
│ postgres-metadata (metrics/events/alerts) │
└──────────────────────┬───────────────────────┘
│
┌───────────┴───────────┐
↓ ↓
[ Telegram Bot ] [ Google Chat ]
+ inline keyboard
현재 아키텍처. K3s 단일 노드의 portal 네임스페이스 하나에 모든 컴포넌트가 들어 있습니다.
큰 그림은 단순합니다. 중앙 EC2 한 대에 K3s 가 단일 노드로 돌고, 그 위에 네임스페이스 portal 이 하나 있습니다. 그 안에 세 개의 pod 가 있습니다.
- portal-backend: FastAPI 0.115 + SQLAlchemy 2.0 (async + asyncpg) + Alembic + APScheduler 3.10 + boto3 1.35. 백엔드 REST API 와 백그라운드 수집 스케줄러를 한 프로세스에서 돌립니다. 9개의 주기 작업이 여기서 돌면서 고객사 AWS API 를 호출하고 결과를 PostgreSQL 에 저장합니다.
- portal-frontend: React 18 + Vite + TanStack Query + Recharts 로 빌드한 정적 SPA 를 nginx 가 서빙합니다. 따로 EC2 나 별도 호스트를 쓰지 않고 같은 K3s 안에 띄웁니다.
- postgres-metadata: 메트릭, 이벤트, EOS 정보, 알람 이력을 영구 저장합니다. K3s StatefulSet 으로 돌아갑니다.
데이터 흐름은 다음 한 문단에 다 들어갑니다. APScheduler 가 주기마다 깨어나면, 활성화된 고객사 목록을 돌면서 각 고객사의 IAM Role ARN 으로 STS AssumeRole 을 호출해 임시 자격증명을 받습니다. 자격증명은 메모리에 45분 TTL 로 캐싱해 두기 때문에 매번 STS 를 때리지는 않습니다. 그 자격증명으로 boto3 클라이언트를 만들어서 CloudWatch, Performance Insights, RDS Describe API 를 호출하고, 결과를 PostgreSQL 에 저장합니다. 그게 끝입니다. 별도 알람 체크 잡 같은 게 따로 없고, 메트릭 수집이 끝난 같은 세션에서 즉시 임계치 평가를 돌려서, 알람 지연을 1분 이내로 묶었습니다. 알람은 Telegram 봇과 Google Chat 웹훅 두 채널로 동시에 나갑니다.
왜 K3s 였나
여기서 한 가지 결정 이유를 더 적어 두고 싶습니다. 컨테이너 오케스트레이션으로 왜 K3s 였는가 입니다. 단일 노드라면 Docker Compose 만으로도 충분하지 않냐고 물을 수 있습니다. 또는 그 반대로 진짜 운영하려면 EKS 가야 하는 거 아니냐고 물을 수도 있습니다. 둘 다 합리적인 질문이고, 한 번 다 따져 봤습니다.
조건은 이렇습니다.
- 단일 노드. EC2 t2.xlarge 한 대(4 vCPU, 15GB RAM). 노드를 늘릴 계획 없음.
- 혼자 운영. DBA 한 명이 코드 짜고 배포하고 장애도 봅니다.
- 최대한 가볍게. RAM 15GB 안에서 portal-backend, portal-frontend, postgres-metadata, 그리고 9개 APScheduler 잡까지 다 돌아야 합니다. 오케스트레이터 자체의 footprint 가 클수록 내가 쓸 메모리 가 줄어듭니다.
후보 세 가지를 봤습니다.
1) Upstream Kubernetes (kubeadm)
control plane 만으로도 1GB 이상의 RAM 을 잡습니다. etcd, controller-manager, scheduler, kube-apiserver, kubelet 이 모두 별도 프로세스로 떠 있고, 단일 노드에서는 의미가 없는 HA 컴포넌트 (etcd 클러스터, leader election) 까지 끌고 옵니다. 노드를 늘릴 계획이 있다면 합리적이지만, 그렇지 않다면 목적에 맞지 않는 환경이라고 생각했습니다.
2) MicroK8s
한때 진지하게 검토했습니다. snap 으로 설치하고 관리되는데, 이게 장점이자 단점입니다. 장점은 addon 토글이 편하다는 점입니다. 단점은 snap 의 자동 업데이트 정책이 내가 통제하지 않는 시점 에 컴포넌트를 갱신한다는 점입니다. 운영 중인 시스템이 새벽에 자기 마음대로 재시작될 가능성이 있다는 게 불편했습니다. Ubuntu 외 OS 호환성도 떨어지는 편입니다.
3) K3s
Rancher 가 만든 단일 바이너리 형태의 Kubernetes 배포판입니다. 바이너리 크기 약 70MB, control plane 메모리 footprint 가 512MB 이하로 알려져 있습니다. etcd 대신 SQLite 를 백엔드로 쓸 수 있고, ServiceLB 와 Traefik 이 내장이라 외부 컴포넌트 의존을 줄여 줍니다. 무엇보다 단일 노드 에지 워크로드 를 명시적으로 타깃팅하는 배포판입니다.
결과적으로 K3s 를 골랐습니다. t2.xlarge 안에 portal-backend / portal-frontend / postgres-metadata 까지 다 띄워도 control plane 의 메모리 점유가 상대적으로 작기 때문 인 것 같습니다. 그 차이가 그대로 APScheduler 9개 잡을 위한 여유 메모리로 돌아갑니다. boto3 SDK 가 메모리를 의외로 많이 잡고, PostgreSQL 도 워크로드에 따라 메모리를 더 가져갈 수 있는데, 그 모든 여유 가 오케스트레이터에서 빠진 만큼 생기는 셈입니다.
부수 효과도 있습니다. 단일 바이너리라 백업과 이관이 단순합니다. snap 같은 외부 트리거에 묶이지 않아서 운영 중에 예상치 못한 재시작 이 없습니다. 그리고 K3s 자체가 운영 중에 거의 안 보입니다. control plane 로그를 들여다볼 일이 거의 없다는 뜻입니다.
단일 노드라고 해서 Kubernetes 가 무조건 오버엔지니어링은 아닌 것 같습니다. 어떤 Kubernetes 인가 가 더 중요한 질문인 것 같습니다.
Claude Code 와 함께한 5주
마지막으로 짧은 회고를 남기고 싶습니다. 이 포털은 3주 동안 만들고 2주 동안 버그를 잡고 검증했습니다. 그 5주 내내 옆에 있던 동료는 한 명, Claude Code 였습니다.
저는 DBA 입니다. 평소 일은 SQL 을 들여다보고, 인덱스를 다듬고, 장애를 따라가는 쪽입니다. FastAPI 의 라우터 구조나 React 의 컴포넌트 흐름 같은 건 손에 익은 영역이 아니었습니다. 그래서 시작할 때 이걸 정말 혼자서 만들어 볼 수 있을까 하는 작은 의구심이 있었습니다.
5주가 지난 지금 돌아보면, Claude Code 와의 협업이 기획부터 개발과 검증, 그리고 매뉴얼 정리까지 전 과정을 가능하게 만들어 주었다는 게 가장 큰 발견인 것 같습니다. 몇 단계로 나눠서 적어 보면 이렇습니다.
기획. 고객사 30개, RDS 100여 대를 어떤 메트릭으로 잡을지, 어떤 알림을 어떤 채널로 보낼지, 어떤 화면을 우선순위에 둘지를 함께 그려 나갔습니다. 제가 DBA 가 매일 보고 싶은 정보 를 던지면, Claude Code 가 그것을 시스템 레벨의 컴포넌트와 데이터 흐름 으로 정리해 주는 흐름이었습니다. 손바닥에 있던 막연한 그림이 글과 다이어그램으로 옮겨지는 과정이 가장 즐거웠던 것 같습니다.
개발. 코드의 큰 부분은 Claude Code 가 작성했습니다. 저는 도메인 요구사항을 던지고, 결과를 검증하고, 방향이 맞는지 판단 하는 자리를 맡았습니다. 직접 손이 안 가는 영역 (예를 들어 SQLAlchemy async 세션 패턴, FastAPI dependency injection, React + TanStack Query 의 데이터 캐시) 은 함께 짜 가며 배웠습니다. 제가 모르는 코드가 운영에 들어가는 건 아닌지 가 가장 큰 걱정이었는데, 한 줄씩 같이 읽고 질문하고 검증하는 과정을 거쳐서 그 걱정이 조금씩 작아졌습니다.
검증. 5주의 마지막 2주가 여기에 들어갔습니다. PI(Performance Insights) API 의 까다로운 케이스 — 예를 들어 describe_dimension_keys 의 AdditionalMetrics 파라미터가 일부 엔진에서 예외를 던지는데, try/except 한 겹에 걸려서 두 sql_group 이 동시에 비는 미묘한 버그 — 같은 건 왜 이게 동시에 비지 라는 질문을 함께 던질 수 있어서 풀린 경우가 많았습니다. 가설을 던지고 같이 따져 봐 주는 동반자가 있다는 게 컸습니다.
매뉴얼. 매뉴얼은 0번부터 12번까지 13개로 쪼개져 있습니다. 이걸 처음부터 끝까지 일관된 톤으로 짧은 시간에 정리하는 건 저 혼자서는 거의 불가능했을 것 같습니다. 코드와 매뉴얼이 동시에 따라간 덕분에, 5주 뒤에 다시 펼쳐 보니 과거의 내가 미래의 나에게 친절했던 결과가 되어 있었습니다.
DBA 가 기획부터 개발까지 전 과정 을 직접 책임지고 시스템 하나를 세상에 내놓을 수 있는 시대가, 이미 시작된 것 같습니다. 그 가능성을 5주에 걸쳐 몸으로 한 번 느껴 본 게 이번 작업에서 가장 큰 수확이었습니다.
