포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는 AWS EKS Workshop Study 내용을 참고하여 작성합니다.
이번 챕터에서는 워크숍에서 제공해준 GitOps 인프라 중
실습 2번 항목인 SaaS 티어 전략에 대해서 다뤄볼 예정입니다.
01. Tier 구조 개요
첫 챕터에서 SaaS 배포 모델에 대해서 설명드렸었습니다. 사일로, 풀, 하이브리드 방식이 있는데요.
SaaS 애플리케이션은 일반적으로 다양한 고객군을 지원하기 위해 Tier 구조를 채택하였습니다.
현실적으로 생각해보면 월 $10짜리 플랜과 월 $1,000짜리 플랜이 같은 인프라 자원을 점유한다면 비용 구조가
무너집니다. 반대로 모든 고객에게 전용 인프라를 붙여주면 소규모 고객은 과도한 비용을 지불하게 되겠죠?
이 문제를 해결하는 것이 티어(Tier) 전략입니다. 그리고 이번 실습에서는 이 전략을 Kubernetes의
Helm Release 템플릿 하나로 구현하는 방법을 살펴보도록 하겠습니다.
동일한 Helm 차트를 사용하면서도 템프릿의 values 값을 티어별로 다르게 설정함으로써, 각 테넌트의 Kubernetes 리소스가 서비스 수준에 정확히 맞게 배포되도록 의도적으로 설계한 것입니다.
앞서 사일로(Silo)와 풀(Pool) 배포 방식에 대해서 설명드렸는데요, 실습에 앞서 간단히 설명드리자면
1) 풀 모델 (공유 모델)
여러 테넌트가 동일한 Kubernetes 네임스페이스, 동일한 마이크로서비스 인스턴스, 동일한 AWS 리소스
(SQS, DynamoDB)를 공유합니다. HTTP 헤더(tenantID)로 요청을 식별하고 라우팅합니다.
장점 : 리소스 효율 극대화, 운영 단순화
단점 : 노이지 네이버(Noisy Neighbor) 문제, 격리 수준이 낮다.
2) 사일로 모델 (전용 모델)
테넌트마다 전용 네임스페이스, 전용 마이크로서비스, 전용 AWS 리소스를 프로비저닝한다.
장점 : 완전한 격리, 테넌트별 SLA 보장 가능
단점 : 리소스 낭비, 테넌트 수 증가에 따른 운영 복잡도 상승.
실무에서는 이 두 모델을 고객군에 따라 조합하는 것이 핵심입니다.
📌 세 가지 티어의 설계 철학
이번 실습에서 구현한 티어 구조는 다음과 같습니다.
티어
환경 유형
Producer
Consumer
AWS 리소스
비용
Basic
완전 공유
pool-1 (공유)
pool-1 (공유)
공유
낮음
Advanced
하이브리드
pool-1 (공유)
전용 네임스페이스
Consumer만 전용
중간
Premium
완전 전용
전용
전용
전용
높음
왜 이런 경계선으로 설계했을까?
Advanced 티어 설계에서 핵심 판단은 "어느 컴포넌트를 전용으로 분리할 것인가"입니다.
Producer: 요청을 받아 SQS에 메시지를 넣는 역할 입니다.
트래픽 패턴이 상대적으로 균일하고 테넌트 간 격리 필요성이 낮다. → 공유 Pool 유지
Consumer: SQS에서 메시지를 꺼내 DynamoDB에 쓰는 역할입니다.
데이터 처리 격리가 중요하고, 전용 SQS 큐와 DynamoDB 테이블이 필요하다. → 전용 Silo
이 판단은 단순히 비용 절감이 아니고, 워크로드의 특성에 따라 격리 수준을 다르게 적용하는 설계 원칙입니다.
02. Helm Release 템플릿 확인
Basic vs Premium 티어 비교
동일한 Helm 차트를 사용하면서 values 설정만 달리하여 각 티어의 특성을 구현함.
1) Basic tenant template.yaml 확인
targetNamespace: pool-1 → 테넌트 전용 ns 없음
producer.enabled: false / consumer.enabled: false → Pod 안 만듦
envId: pool-1 → pool-1 공유 인스턴스로 라우팅
Ingress만 pool-1에 생성
2) Premium tenant template.yaml 확인
targetNamespace: {TENANT_ID} → 전용 ns 생성
producer.enabled: true / consumer.enabled: true → 전용 Pod 배포
envId 없음 → 자신의 ns에 직접 배포
전용 SQS/DynamoDB도 Terraform이 같이 생성
즉, 아래 표와 같이 정리를 하자면
구성 요소
Basic tier
Premium tier
배포 모델
Pool (공유)
Silo (전용)
Kubernetes namespace
pool-1 (공유)
테넌트 전용
Producer
pool-1 (공유)
전용 배포
Consumer
pool-1 (공유)
전용 배포
SQS que
공유
전용
DynamoDB table
공유
전용
Ingress
테넌트별 라우팅반
전용
Cost
낮음
전용
격리 수준
낮음
전용
Advanced 티어 정의
Basic(공유)과 Premium(전용) 두 가지 티어를 살펴봤습니다.
이제 새로운 고객군의 요구 사항을 수용하기 위한 Advanced 티어를 직접 설계하고 구현해 볼 예정이에요.
★ Advanced 티어의 설계 방향
Producer: 공유 (pool-1 Pool 환경 활용) → 요청 패턴이 비교적 균일한 워크로드
Consumer: 전용 (테넌트 전용 네임스페이스) → 데이터 처리 격리 필요
목표: Silo(전용)와 Pool(공유) 모델의 장점을 사용 패턴에 맞게 조합
왼쪽 HelmRelease values가 오른쪽 실제 K8s 리소스로 어떻게 매핑되는지 보여주는 다이어그램 입니다.
HelmRelease values를 해석해볼까요?
producer.enabled: false + envId: pool-1 : Producer Pod를 새로 안 만들고 pool-1 거 갖다 쓰네요. producer.ingress.enabled: true : pool-1으로 라우팅하는 Ingress 룰은 만듦 consumer.enabled: true : Consumer Pod는 전용으로 새로 만듦 consumer.ingress.enabled: true : Consumer 전용 Ingress 룰 만듦
k8s 리소스로 실제 배포 결과는 아래와 같습니다.
1) pool-1 네임스페이스
tenant-5 이름의 Ingress + Producer Service가 pool-1 안에 생깁니다. 이게 pool-1의 공유 Producer의 Deployment/HPA/ServiceAccount로 트래픽을 보냅니다. Consumer도 pool-1에 Deployment/HPA/SA가 있는데 점선(흐릿)인 이유는, pool-1 공유 Consumer가 있긴 한데 Advanced 테넌트는 안 씁니다.
2) tenant-5 네임스페이스
Consumer 전용 namespace가 생기며, Deployment/HPA/ServiceAccount 배포됨 Terraform이 전용 SQS 큐 + DynamoDB 테이블도 같이 프로비저닝
cd /home/ec2-user/environment/gitops-gitea-repo/
git pull origin main
git add .
git commit -am "Adding tenant-t1d6c with Advanced Tier"
git push origin main
Git push 하면 아래의 과정을 거쳐 배포가 됩니다.
git push origin main
│
▼
Gitea (http://10.35.48.94:3000) 에 코드 반영
│
▼
FluxCD GitRepository 감시 중
→ flux reconcile source git flux-system 으로 강제 트리거
→ refs/heads/main@sha1:9cb79325 변경 감지
│
▼
FluxCD Kustomization 처리
→ tenants/advanced/kustomization.yaml 읽음
→ tenant-t1d6c.yaml 발견
│
▼
K8s에 리소스 적용
→ Namespace: tenant-t1d6c 생성
→ HelmRelease: tenant-t1d6c-advanced 생성 (flux-system)
│
▼
Helm이 helm-tenant-chart 배포 (targetNamespace: tenant-t1d6c)
→ producer.enabled: false → Producer Pod 안 만들고 Ingress만 pool-1에 생성
→ consumer.enabled: true → Consumer Deployment/HPA/SA 생성
│
▼
OpenTofu Controller가 Terraform CRD 감지
→ 전용 SQS 큐 생성
→ 전용 DynamoDB 테이블 생성
Advanced 테넌트가 배포되었는지 확인해볼까요?
Namespace 정상 생성 완료, HelmRelase True 확인.
Consumer Pod 3개 전부 Running 상태 확인, Producer Pod 없음 확인(pool-1 공유 사용)
전용 테넌트 이므로 새 테넌트의 SQS큐와 DynamoDB 테이블도 확인해볼까요?
* 실제 AWS 콘솔에서도 확인이 가능합니다
Tofu 컨트롤러의 로그도 확인해보겠습니다.
직관적이지 않아서 제가 한번 간단하게 정리를 해보자면 아래와 같습니다.
16:55:27 - tenant-t1d6c Terraform CR 감지
→ "trigger namespace tls secret generation"
→ runner pod 상태 확인: not-found (아직 없음)
16:55:42 - runner pod 기동 완료
→ "setting up terraform"
→ "write backend config: ok"
→ "new terraform" (workingDir: /tmp/flux-system-tenant-t1d6c/terraform/modules/tenant-apps)
→ "generate vars from tf: ok"
→ "generate template: ok"
16:56:00 - terraform init 완료
→ "init reply: ok"
→ "tfexec initialized terraform"
→ "workspace select reply: ok"
→ "calling detectDrift ..."
16:56:21 - drift 감지 완료
→ "plan for drift: ok found drift: false"
※ 이미 SQS/DynamoDB가 존재하거나 신규 생성 완료된 상태
16:56:24 - 완료
→ "write outputs: ok, changed: false"
→ "Reconciliation completed. Generation: 1"
→ "requeue after interval: 1m0s" ← 1분마다 재조정
제가 테스트를 이미 한번 한 상태여서 이미 SQS와 DynamoDB가 존재하여 false 상태였습니다.
샘플 요청으로 검증 : 애플리케이션 정상 동작 확인
APP_LB=http://$(kubectl get ingress -n tenant-t1d6c -o json | jq -r .items[0].status.loadBalancer.ingress[0].hostname)
echo $APP_LB # ALB 접속 도메인 확인
http://k8s-tenantslb-b3cdded56f-1707518810.ap-northeast-2.elb.amazonaws.com
curl -s -H "tenantID: tenant-t1d6c" $APP_LB/producer | jq
curl -s -H "tenantID: tenant-t1d6c" $APP_LB/consumer | jq
ALB의 리소스 맵을 콘솔에서 확인해보면 아래와 같습니다.
위 ALB의 규칙을 아래와 같이 정리해보았습니다.
우선순위 1
조건: TenantID=tenant-t1... AND 경로=/producer 또는 /producer...
→ 대상그룹: k8s-pool1-tenantt1-f4f14b64ad (3개 대상)
→ pool-1의 공유 Producer Pod 3개로 전달 ✅
우선순위 2
조건: 경로=/consumer 또는 /consum... AND TenantID=tenant-t1...
→ 대상그룹: k8s-tenantt1-tenantt1-8be561a8b2 (3개 대상)
→ tenant-t1d6c 전용 Consumer Pod 3개로 전달 ✅
우선순위 default
→ 고정 응답 (매칭 안 되는 요청 처리)
이게 producer.enabled: false + envId: pool-1 / consumer.enabled: true 가 ALB 레벨에서 실제로 구현된 모습
curl 요청 시 정상적으로 아래와 같이 응답을 반환받았습니다.
envicronment 필드를 통해 Advanced 티어의 핵심 특성이 정확히 구현되었음을 확인할 수 있습니다.
producer : pool-1 공유 환경
consumer : tenant-t1d6c 전용 환경
💡 핵심 패턴
모든 티어가 동일한 단일 Helm 차트를 사용하며, values 설정만으로 배포 방식이 결정됨.