CloudNet@ Team Study/EKS Workshop 4th Cohort

GitOps Infrastructure SaaS Application Tier Strategy

"Everything about infra" 2026. 4. 28. 02:07
포스팅은 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 테이블도 같이 프로비저닝

 

핵심 : Producer 요청은 pool-1 공유 인스턴스가 처리하고,

Consumer는 tenant-5 전용 인스턴스가 처리한다는 것입니다.

 

Advanced tanat provisioing

Advanced 티어 템플릿을 생성해보겠습니다.

$ cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tier-templates/advanced_tenant_template.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: {TENANT_ID}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: {TENANT_ID}-advanced
  namespace: flux-system
spec:
  releaseName: {TENANT_ID}-advanced
  targetNamespace: {TENANT_ID}  # Deploying into the tenant-specific namespace
  interval: 1m0s
  chart:
    spec:
      chart: helm-tenant-chart
      version: "{RELEASE_VERSION}.x"
      sourceRef:
        kind: HelmRepository
        name: helm-tenant-chart
  values:
    tenantId: {TENANT_ID}
    apps:
      producer:
        envId: pool-1
        enabled: false # Pool deployment -- advanced tier shares resources with other tenants
        ingress:
          enabled: true
      consumer:
        enabled: true  # Silo deployment -- advanced tier has a dedicated deployment for each tenant
        ingress:
          enabled: true
        image:
          tag: "0.1" # {"\$imagepolicy": "flux-system:consumer-image-policy:tag"}
EOF

 

Advanced 테넌트를 추가할 폴더를 생성해봅시다.

mkdir -p /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/advanced

 

Advanced 테넌트 수동 프로비저닝

수동으로 테넌트를 온보딩하는 과정을 먼저 살펴볼 예정입니다.

advanced_tenant_template.yaml을 복사하고 변수를 실제 값으로 치환하여 새 테넌트 파일 생성:

export TENANT_ID=tenant-t1d6c
export RELEASE_VERSION=0.0

cd /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/
cp tier-templates/advanced_tenant_template.yaml tenants/advanced/$TENANT_ID.yaml

sed -i "s|{TENANT_ID}|$TENANT_ID|g" "tenants/advanced/$TENANT_ID.yaml"
sed -i "s|{RELEASE_VERSION}|$RELEASE_VERSION|g" "tenants/advanced/$TENANT_ID.yaml"

 

kustomization.yaml 새 테넌트의 Helm 릴리스를 가리키는 파일 생성

cat << EOF > tenants/advanced/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - $TENANT_ID.yaml
EOF

 

변경 사항을 커밋 및 Git에 푸쉬

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 설정만으로 배포 방식이 결정됨.

새로운 티어 추가 = 새 템플릿 파일 하나 작성으로 완결됨.

 


긴 글 읽어주셔서 감사합니다.

 

다음 Lab은 자동화된 테넌트 온보딩/오프보딩으로 찾아뵙도록 하겠습니다.