티스토리 뷰

반응형

쿠버네티스 실습: SaaS 운영관리(Admin Console) 구축

Next.js + NestJS + RBAC + Grafana API + Stripe Dashboard 통합

앞선 글에서는 SaaS Billing(청구/과금) 시스템을 완벽히 구축해
테넌트별 사용량 → 가격 정책 → Stripe 자동결제까지 이어지는
실제 SaaS 비즈니스 수준의 프로덕션 아키텍처를 완성했습니다.

이번 글에서는 이 모든 구성을 하나의 Admin Console(운영 대시보드) 로 통합합니다.

즉, 운영 관리자가 웹 UI에서:

  • 테넌트 생성/삭제
  • Price Plan 등록/변경
  • API Key 발급/비활성화
  • RateLimit 정책 수정
  • 사용량 그래프 조회(실시간 Grafana)
  • 청구내역 조회 및 Stripe invoice 연동
  • 보안정책 변경(OPA/Kyverno 템플릿 적용)

까지 모두 관리할 수 있게 만드는 실습입니다.


1. 전체 구조

[Admin Portal (Next.js)]
        │
        ▼
[NestJS Admin API] ─── Prisma ─── PostgreSQL
        │                     │
        │                     ├── 테넌트, 요금제, API Key, 정책 저장
        │
        ├── Stripe API ↔ Billing
        │
        ├── Grafana API ↔ 실시간 모니터링
        │
        ├── ArgoCD API ↔ 테넌트별 배포 제어
        │
        └── Vault API ↔ Secret, Token 관리

Admin Console은 단순 UI가 아니라
SaaS 전체를 운영 가능한 통합 제어판(Control Plane) 역할입니다.


2. 관리자를 위한 RBAC 설계

관리자 계층도 권한을 분리해야 합니다.

Role 권한

super_admin 모든 기능(정책/요금/시스템)
billing_admin 요금제, 인보이스 관리
ops_admin RateLimit/Quota, 인프라 관리
viewer 대시보드 & 조회만

NestJS에서 RBAC Guard 구현:

roles.guard.ts

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.get<string[]>('roles', ctx.getHandler());
    if (!required) return true;

    const req = ctx.switchToHttp().getRequest();
    const role = req.user.role;
    return required.includes(role);
  }
}

@Roles(‘super_admin’) 같은 데코레이터로 제어.


3. Admin API: 테넌트 생성

테넌트를 생성하면 동시에 다음을 자동화해야 합니다.

  1. DB에 Tenant row 생성
  2. Stripe Customer 생성
  3. Namespace(team-a) 생성(Kubernetes)
  4. ArgoCD Project 생성
  5. Vault Secret Path 생성
  6. 기본 RateLimit/Quota 정책 설정
  7. 기본 Price Plan 적용

tenant.controller.ts

@Post()
@Roles('super_admin')
async createTenant(@Body() dto: CreateTenantDto) {
  const tenant = await this.tenantService.create(dto);
  await this.infraService.createNamespace(tenant.id);
  await this.infraService.createArgoProject(tenant.id);
  await this.billingService.createStripeCustomer(tenant.id, dto.email);
  await this.policyService.applyDefaultRateLimit(tenant.id);
  return tenant;
}

4. Kubernetes 연동 — 관리자 페이지에서 Namespace 생성

반응형

infra.service.ts

import k8s = require('@kubernetes/client-node');

@Injectable()
export class InfraService {
  private kc = new k8s.KubeConfig();
  private k8sApi: k8s.CoreV1Api;

  constructor() {
    this.kc.loadFromDefault();
    this.k8sApi = this.kc.makeApiClient(k8s.CoreV1Api);
  }

  async createNamespace(tenant: string) {
    const ns = { metadata: { name: `tenant-${tenant}` }};
    await this.k8sApi.createNamespace(ns);
  }
}

Kubernetes API 사용은 공식 라이브러리(@kubernetes/client-node) 기준.


5. API Key 발급/회수

Admin Console에서 버튼 클릭 → Key 재발급/비활성화

apikey.service.ts

async rotateKey(tenantId: string) {
  const newKey = randomUUID();
  await this.redis.set(`apikey:${tenantId}`, newKey);
  return newKey;
}

NestJS Controller:

@Post('rotate/:id')
@Roles('ops_admin')
async rotate(@Param('id') tenantId: string) {
  return this.apikeyService.rotateKey(tenantId);
}

Next.js UI:

<button onClick={() => rotateKey(tenantId)}>Rotate API Key</button>

6. Price Plan CRUD 화면

NestJS:

@Post("plan")
@Roles('billing_admin')
async createPlan(@Body() dto: CreatePlanDto) {
  return this.prisma.plan.create({ data: dto });
}

Next.js에서는 표(View) + 수정UI + 저장:

<form onSubmit={handleSubmit}>
  <input value={plan.monthlyBaseFee}/>
  <input value={plan.pricePerRequest}/>
  <button type="submit">Save</button>
</form>

7. Stripe Invoice 조회

NestJS:

@Get('invoices/:tenantId')
@Roles('billing_admin','viewer')
async getInvoices(@Param('tenantId') tid: string) {
  const tenant = await this.prisma.tenant.findUnique({ where: { id: tid }});
  return stripe.invoices.list({ customer: tenant.stripeCustomerId });
}

Next.js에서 인보이스 목록 렌더:

{invoices.data.map(inv => (
  <tr key={inv.id}>
    <td>{inv.number}</td>
    <td>{inv.amount_due / 100}</td>
    <td>{inv.status}</td>
  </tr>
))}

8. Grafana API 연동(사용량/지표 실시간 조회)

Grafana API Token 필요.

NestJS Service:

async getGrafanaPanel(panelId: number) {
  return axios.get(`https://grafana/api/panels/${panelId}`, {
    headers: { Authorization: `Bearer ${process.env.GRAFANA_TOKEN}` }
  });
}

Next.js:

<img src={`/admin/grafana/panel/${panelId}`} />

또는 iframe:

<iframe src={`${GRAFANA_URL}/d/${dashboardId}?orgId=1&refresh=5s`} />

9. RateLimit 정책 수정(Admin UI → Istio EnvoyFilter Patch)

관리 UI에서 JSON Patch로 EnvoyFilter 수정.

NestJS:

async updateRateLimit(tenantId: string, tokens: number, perMinutes: number) {
  return this.k8sApi.patchNamespacedCustomObject(
    'networking.istio.io','v1alpha3','istio-system','envoyfilters','tenant-local-ratelimit',
    [{ op: "replace", path: "/spec/configPatches/0/patch/value/token_bucket/max_tokens", value: tokens }]
  );
}

→ 관리 UI에서 토큰 수 조정 → 즉시 반영됨.


10. Vault Secret 관리

테넌트별 DB 비밀번호/접속 토큰 저장:

await vault.write(`secret/data/${tenantId}/db`, { data: { password: pw }});

Admin UI에서 rotate 버튼 제공 → 즉시 Vault 갱신 → Deployment rollout.


11. 관리자용 로그/이벤트 뷰어 (Loki)

NestJS → Loki API:

async getTenantLogs(tenantId: string) {
  const query = `{tenant="${tenantId}"}`;
  return axios.get(`${LOKI_URL}/loki/api/v1/query_range`, { params: { query }});
}

Next.js에서 테이블 렌더링:

<pre>{JSON.stringify(logs, null, 2)}</pre>

12. SaaS 운영에서 실제 필요한 메뉴

최종 Admin Console 메뉴 구성 예시:

Dashboard
 ├── System Status
 ├── Tenant Overview
Tenants
 ├── Create Tenant
 ├── Tenant Settings
 ├── API Keys
Plans & Billing
 ├── Price Plan Editor
 ├── Stripe Invoices
Rate Limit / Quota
 ├── Current Policies
 ├── Adjust Rate Limit
 ├── Quota Setup
Monitoring
 ├── Grafana Usage Charts
 ├── Logs (Loki)
 ├── Alerts (Prometheus)
Security
 ├── Vault Secrets
 ├── OPA / Kyverno Policies
Infrastructure
 ├── Namespace / ArgoCD Projects
 ├── Deployments & Rollouts

이 구조는 실제 기업용 SaaS 플랫폼 관리자 페이지의 구성과 거의 동일합니다.


13. 운영 시나리오 예시

상황 Admin Console에서 수행

신규 고객 온보딩 버튼 1번 → Tenant 생성 → Namespace/Plan/Key 자동 생성
고객이 요금제 변경 요청 Price Plan 수정 → 다음 결제주기부터 적용
API Key 유출 Admin → “Key Rotate” 클릭 즉시 반영
사용량 폭주 RateLimit 조정 / KEDA로 확장 트리거
보안 문제 발생 로그 조회 → OPA 정책 강화 버튼 실행
백엔드 장애 ArgoCD에서 재배포(ReSync) 버튼 클릭

14. 정리

이번 글에서 구축한 Admin Console
단순한 UI가 아니라 SaaS 시스템 전체의 Control Plane 입니다.

✔ 테넌트 관리
✔ API Key 관리
✔ 청구/결제 Stripe 연동
✔ Grafana/Loki/Prometheus 통합 모니터링
✔ Rate Limit & Quota 관리
✔ Vault Secret Management
✔ ArgoCD & Kubernetes 배포 제어
✔ RBAC 기반 관리자 권한 체계

이제 진짜 ‘서비스’로서의 기능이 모두 갖춰졌습니다.
앞으로 DevOps/SRE/보안/비즈니스 의사결정 모두 이 Admin Console에서 이루어지게 됩니다.


다음 글 예고

다음 편에서는 “완전 자동화된 Multi-Tenant Provisioning Pipeline” 을 구축합니다.

신규 고객이 가입하면 → Namespace 생성 → API Key 발급 → RateLimit 적용 → Billing 연동까지
Terraform + ArgoCD + NestJS Provisioner + Vault 로 자동 실행되는
진짜 SaaS 수준의 자동 프로비저닝 시스템을 만들어냅니다.


 

쿠버네티스,SaaS,AdminConsole,NextJS,NestJS,Stripe,Grafana,Loki,ArgoCD,Vault

 

 


✅ 참고할 최신 자료

  • Stripe 공식 가이드: SaaS 과금 모델(구독, 사용량 기반 등)을 설명해 있으며, “요금 모델 선택”, “자동 청구”, “안정적 수익 구조” 측면을 잘 정리하고 있습니다. (Stripe)
  • Stripe 블로그: SaaS 지불 처리에서 직면하는 문제 및 해결책을 설명한 글 (예: 반복 결제, 실패 대응, 지역별 과금) (Stripe)
  • Stripe 활용 사례: 사용 기반 요금 모델(usage-based billing)에서의 설계와 유의사항에 대해 정리되어 있습니다. (iteratorshq.com)

⚠️ 보완 / 강화 제안

아래 항목들을 글에 추가하시면 독자가 실무 구현 시 놓치기 쉬운 부분까지 대비할 수 있어요.

  • 요금 모델 선택 및 구조화
    글에서 기본요금 + 사용량 단가 구조를 예시로 들었는데, 추가로 “Tiered Pricing”, “Volume Pricing”, “Add-on 기반 요금” 등 실제 SaaS에서 자주 쓰이는 모델을 간단히 정리하면 좋습니다. 참고자료에서도 이 부분이 강조되어 있어요. (iteratorshq.com)
  • 청구 처리 및 실패 대응 흐름
    예: 결제 실패 시 재시도, 카드 유효기간 만료, 구독 해지 시의 처리, 영수증 발행과 세금 계산. Stripe 관련 자료에서 이 부분이 중요하게 다뤄집니다. (Stripe)
  • 사용량 집계 정확성 및 지연 고려
    API 사용량을 Redis Stream → Worker로 집계하는 구조인데, 실제 운영에서는 이벤트 누락, 중복 집계, 지연(latency) 등이 문제가 됩니다. 이 부분을 “데이터 청소(Cleaning) + 중복 제거 + 타임존 고려” 등으로 보완하면 좋습니다.
  • 투명성 있는 과금 제공
    고객(테넌트)에게 과금 내역을 명확히 보여주는 것도 중요합니다 — “사용량 단위(예: API 호출 수) vs 청구 금액” 비교, 무료 티어, 할인 적용 등. Stripe 자료에서도 “사용 단위 정의(clear usage unit)”가 강조되어 있어요. (iteratorshq.com)
  • 보안/컴플라이언스 측면
    결제정보, 고객 정보가 포함되므로 PCI-DSS, 지역별 세금(예: 부가가치세 VAT) 등 준수사항을 간단히 언급하면 신뢰도가 올라갑니다.
  • 테넌트별 과금 자동화 및 확장 고려사항
    다수의 테넌트를 운용할 때 “사용량 증가 → 요금 상승”이 자동으로 반영되도록 설계해야 하며, 테넌트별 과금 이력 저장, 과금 리포트 제공, 테넌트별 요금 정책 변경(업그레이드/다운그레이드) 대응 흐름이 들어가면 더 좋습니다.

 

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
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 29 30
글 보관함
반응형