[CloudNeta] EKS 워크샵 스터디 (6) - GitOps와 SaaS 플랫폼 엔지니어링 Part 2 - SaaS 티어 전략과 테넌트 자동화
이번 게시글에서는 EKS 워크샵 스터디 제 6주차 내용을 작성합니다.
이 글은 2부로 나누어집니다.
실습 2. SaaS 티어 전략
SaaS 애플리케이션은 흔히 다양한 시장 세그먼트를 지원하도록 설계되며, 고객 프로필의 스펙트럼에 따라 별도의 가격 정책과 경험을 제공합니다. 이러한 고객 프로필을 일반적으로 티어(Tier)라고 부릅니다. 서로 다른 티어의 요구사항을 충족하려면 각 티어의 경험을 형성할 수 있는 아키텍처 구성 요소를 도입해야 합니다. 이 티어링 모델은 SaaS 솔루션의 비용, 운영, 관리, 그리고 안정성 전반에 걸쳐 영향을 미칩니다.
이번 워크샵의 티어 전략은 Helm 릴리즈 템플릿을 통해 구현됩니다. 각 템플릿은 테넌트별 서비스 수준을 반영하도록 설계되어 있으며, 티어에 맞게 조정된 리소스를 배포할 수 있게 해줍니다. 결국 Helm의 배포 동작은 이 사전 정의된 템플릿에 의해 제어되며, 각 테넌트의 쿠버네티스 리소스가 해당 서비스 수준과 정확히 일치하도록 보장합니다.

티어 템플릿 살펴보기
이전 실습에서 살펴봤듯이 테넌트 애플리케이션은 단일 Helm 차트로 패키징되어 있고, Tofu Controller와 Terraform CRD를 활용하여 필요한 마이크로서비스와 인프라를 배포할 수 있도록 구성되었습니다. 테넌트의 티어에 따라 특정 구성 요소가 서로 다른 방식으로 배포되며, 이를 정의하는 것이 바로 티어 템플릿입니다.
# advanced 는 직접 만들 예정입니다.
tree /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tier-templates
.
└── application-plane
└── production
└── tier-templates
├── basic_env_template.yaml
├── basic_tenant_template.yaml
└── premium_tenant_template.yaml
Basic 티어는 shared 환경으로, Premium 티어는 별도 테넌트로 구성해 배포할 예정입니다.
basic 에 대해
Basic 티어는 리소스를 공유하는 테넌트를 위해 설계되어 높은 비용 효율성을 제공합니다. 단, Basic 티어 테넌트를 프로비저닝하기 전에 이들을 수용할 풀(Pool) 환경을 먼저 생성해야 합니다.
basic_env_template.yaml은 여러 Basic 티어 테넌트를 수용할 수 있는 풀 환경을 생성하는 데 사용됩니다. Helm 릴리즈의 values는 프로듀서·컨슈머 마이크로서비스와 그에 필요한 인프라 리소스를 프로비저닝하도록 구성됩니다. 인그레스는 비활성화 상태인데, 이는 현재 테넌트가 배포되지 않은 공유 환경이기 때문입니다. 인그레스 기능은 개별 테넌트에 특화된 라우팅 전략에서만 활용됩니다.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {TENANT_ID}-basic
namespace: flux-system
spec:
releaseName: {TENANT_ID}-basic
targetNamespace: pool-1 # Deploying into the tenant-specific namespace
storageNamespace: pool-1
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 -- basic tier shares resources with other tenants
ingress:
enabled: true
consumer:
envId: pool-1
enabled: false # Pool deployment -- basic tier shares resources with other tenants
ingress:
enabled: true
pool 파일을 여러 개 두면 샤딩 전략을 구성하고 데이터를 분산해 장애 영향 범위를 최소화할 수 있겠죠.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- dummy-configmap.yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dummy-configmap-basic
namespace: default
data:
note: "This is a dummy ConfigMap for Flux to avoid empty kustomization error."
Kustomize 매니페스트는 위와 같은 형태로 구성합니다.
premium tier 에 대해
Premium 티어는 전용 리소스가 필요한 테넌트를 위해 설계됩니다. premium_tenant_template.yaml은 tenants/premium 폴더 하위 테넌트들의 Helm 릴리즈를 생성하는 데 사용됩니다. 현재는 온보딩된 Premium 티어 테넌트가 없으며, 이후 섹션에서 테넌트를 온보딩하는 방법을 다룹니다.
apiVersion: v1
kind: Namespace
metadata:
name: {TENANT_ID}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {TENANT_ID}-premium
namespace: flux-system
spec:
releaseName: {TENANT_ID}-premium
targetNamespace: {TENANT_ID}
storageNamespace: {TENANT_ID}
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:
enabled: true # Silo deployment -- premium tier has a dedicated deployment for each tenant
ingress:
enabled: true
image:
tag: "0.1" # {"$imagepolicy": "flux-system:producer-image-policy:tag"}
consumer:
enabled: true # Silo deployment -- premium tier has a dedicated deployment for each tenant
ingress:
enabled: true
image:
tag: "0.1" # {"$imagepolicy": "flux-system:consumer-image-policy:tag"}]
Premium 티어 테넌트의 Helm 릴리즈는 전용 쿠버네티스 네임스페이스에 테넌트의 모든 리소스를 프로비저닝하도록 values가 구성됩니다. 인그레스, 서비스, 그리고 인프라 리소스를 포함한 프로듀서와 컨슈머 마이크로서비스가 모두 배포됩니다.
정리
Basic 티어와 Premium 티어를 보다 명확하게 비교하면, 두 티어는 동일한 Helm 차트를 사용하되 Helm 릴리즈 템플릿을 통해 티어별로 다른 values 설정을 적용하는 방식으로 구분됩니다. 이를 통해 배포 전반의 일관성을 유지하면서도 티어에 맞는 리소스 구성을 보장합니다.
Advanced tier 제공해보기
Basic 티어와 Premium 티어의 개념을 살펴봤으니 이제 직접 실습해봅니다. 새로운 고객 그룹의 요구사항을 수용하기 위해 새로운 티어가 필요하다는 것을 팀이 파악했습니다. 이 티어는 전용 리소스와 공유 리소스를 혼합한 형태로, 프로듀서는 공유하되 컨슈머는 전용으로 운영합니다. 해당 고객들의 사용 패턴에 맞게 사일로(Silo/전용)와 풀(Pool/공유) 모델 각각의 장점을 취하는 방식입니다.
이 새로운 티어를 Advanced 티어라고 부르겠습니다. 새로운 티어 옵션을 생성하려면 어떤 단계를 거쳐야 할까요?
Advanced 티어 생성
첫 번째 단계는 Advanced 티어의 Helm 릴리즈 템플릿을 생성하는 것입니다. Premium 티어를 베이스라인으로 삼아 다음과 같은 변경을 적용합니다.
releaseName을premium에서advanced로 변경producer.enabled를true에서false로 변경- 풀 환경의 프로듀서를 사용하도록
envId: pool-1추가
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} # 테넌트 전용 네임스페이스에 배포
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) 배포 -- Advanced 티어는 다른 테넌트와 리소스를 공유
ingress:
enabled: true
consumer:
enabled: true # 사일로(Silo) 배포 -- Advanced 티어는 테넌트별 전용 배포
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 티어에 새 테넌트의 Helm 릴리즈를 생성하고, Flux와 Tofu Controller가 생성하는 리소스를 살펴봅니다.
먼저 advanced_tenant_template.yaml을 새 테넌트용으로 복사하고 TENANT_ID와 RELEASE_VERSION 변수를 치환합니다.
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"
실습 1에서 배운 것처럼, 새 테넌트 Helm 릴리즈를 가리키는 kustomization.yaml 파일도 생성해야 합니다.
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
Advanced 테넌트 확인
Flux Reconciliation을 강제 실행하여 프로세스를 빠르게 진행합니다.
flux reconcile source git flux-system
tenant-t1d6c 네임스페이스에서 컨슈머 디플로이먼트를 확인합니다.
kubectl get deployment -n tenant-t1d6c
컨슈머는 전용으로 배포되므로, 새 테넌트용 SQS 큐와 DynamoDB 테이블도 생성된 것을 확인할 수 있습니다.
aws dynamodb list-tables | grep tenant-t1d6c
aws sqs list-queues | grep tenant-t1d6c
DynamoDB 테이블과 SQS 큐가 바로 보이지 않는다면 Tofu Controller가 아직 리소스를 생성 중인 것이니, 잠시 기다린 후 다시 확인합니다.
실습 3에서 테넌트 프로비저닝 자동화와 샘플 애플리케이션 테스트 방법을 다룹니다. 지금은 애플리케이션이 정상적으로 동작하는지 요청을 보내 확인해봅니다.
APP_LB=http://$(kubectl get ingress -n tenant-t1d6c -o json | jq -r .items[0].status.loadBalancer.ingress[0].hostname)
curl -s -H "tenantID: tenant-t1d6c" $APP_LB/producer | jq
curl -s -H "tenantID: tenant-t1d6c" $APP_LB/consumer | jq
아래와 유사한 응답을 받을 수 있습니다. 프로듀서 마이크로서비스는 공유 환경인 pool-1에서, 컨슈머는 전용 환경인 tenant-t1d6c에서 실행되고 있는 것을 확인할 수 있습니다.
{
"environment": "pool-1",
"microservice": "producer",
"tenant_id": "tenant-t1d6c",
"version": "0.0.1"
}
{
"environment": "tenant-t1d6c",
"microservice": "consumer",
"tenant_id": "tenant-t1d6c",
"version": "0.0.1"
}
실습 3. 테넌트 온보딩/오프보딩 자동화
Argo Workflows는 테넌트 온보딩, 오프보딩, 배포 과정에서 변수 치환과 작업 자동화를 담당하는 워크플로우 오케스트레이터로 활용됩니다. 변경 사항을 Git 저장소에 커밋하는 것까지 자동화하며, 이후의 GitOps 파이프라인은 그 커밋을 트리거로 동작합니다.
현재 등록된 워크플로우 템플릿을 확인합니다.
kubectl get workflowtemplates -nargo-workflows
세 가지 워크플로우 템플릿이 존재합니다.
onboarding: 새 테넌트를 환경에 프로비저닝하는 워크플로우입니다.offboarding: 테넌트를 환경에서 제거하는 워크플로우입니다.deployment: 테넌트 HelmRelease의 버전을 업데이트하는 워크플로우입니다.
각 워크플로우는 이후 실습에서 더 자세히 다룹니다. 지금은 onboarding 워크플로우에 집중합니다. 동작 순서는 다음과 같습니다.

- Argo Events와 센서를 통해 SQS 메시지를 수신합니다.
- 온보딩 워크플로우를 트리거합니다.
- Gitea 서버에서 Git 저장소를 클론합니다.
- 템플릿(Basic, Advanced, Premium)을 기반으로 새 테넌트의 Helm 릴리즈를 생성합니다.
- 변경 사항을 Git에 푸시합니다.
Argo Workflows는 실습 1에서 수동으로 수행한 작업을 자동화하는 역할만 담당합니다. Git에서의 Reconciliation과 EKS 리소스 배포는 여전히 Flux v2가 책임집니다.
이번 실습에서는 Basic, Advanced, Premium의 서로 다른 티어로 새 테넌트를 프로비저닝하며 프로비저닝 프로세스가 어떻게 동작하는지 직접 확인해봅니다. 다음 단계로 이동하여 테넌트 프로비저닝을 시작하고 리소스를 자세히 살펴보겠습니다.
Premium 티어 테넌트 프로비저닝
이번 실습에서는 전용 리소스를 사용하는 Premium 티어 테넌트를 프로비저닝합니다. Premium 테넌트가 온보딩되면 워크로드 실행에 필요한 모든 리소스가 프로비저닝됩니다.
Argo Events 센서
테넌트 프로비저닝 워크플로우는 argo-events에 의해 트리거됩니다. Sensor CRD는 SQS 메시지를 수신하고, WorkflowTemplate을 기반으로 Workflow를 트리거하도록 구성되어 있습니다.
아래 명령어로 모든 워크플로우 정의를 확인합니다.
tree /home/ec2-user/environment/gitops-gitea-repo/control-plane/production/workflows/
SSH 프라이빗 키는 설치 과정에서 쿠버네티스 시크릿으로 로드되어, Argo Workflows 러너가 저장소에 커밋할 수 있습니다.
Premium 테넌트 프로비저닝
새 테넌트를 프로비저닝하려면 tenant_id, tenant_tier, release_version 등 필요한 메타데이터를 담아 Amazon SQS 큐에 메시지를 전송합니다.
export ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.argoworkflows_onboarding_queue_url}')
aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL \
--message-body '{
"tenant_id": "tenant-1",
"tenant_tier": "premium",
"release_version": "0.0"
}'
tenant_tier가 premium이므로 이 테넌트는 전용 리소스를 사용합니다. 이는 샘플 애플리케이션(프로듀서와 컨슈머)에서 전용 네임스페이스에 독립적인 마이크로서비스와 인프라 리소스가 배포됨을 의미합니다. 배포 프로세스는 각 테넌트 배포에 적합한 티어 설정을 적용하는 역할을 담당합니다.
테넌트 온보딩을 위해 생성된 워크플로우를 확인합니다.
kubectl -n argo-workflows get workflow
아래와 유사한 출력을 확인할 수 있습니다.
NAME STATUS AGE MESSAGE
tenant-onboarding-gzt4s Running 9s
Argo Workflows Web UI에서 워크플로우 실행 현황을 확인합니다.
ARGO_WORKFLOW_URL=$(kubectl -n argo-workflows get service/argo-workflows-server -o json | jq -r '.status.loadBalancer.ingress[0].hostname')
echo http://$ARGO_WORKFLOW_URL:2746/workflows
브라우저에서 해당 URL을 열면 워크플로우 현황을 확인할 수 있으며, 워크플로우를 클릭하면 각 단계의 실행 정보와 로그를 상세히 살펴볼 수 있습니다.
GitOps 변경 사항 검증
Gitea 저장소 접근
Gitea 웹 인터페이스에 접근하여 변경 사항을 검증합니다. 먼저 Gitea URL과 인증 정보를 가져옵니다.
export GITEA_PUBLIC_IP=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.gitea_public_url}')
export GITEA_ADMIN_PASSWORD=$(aws ssm get-parameter --name "/eks-saas-gitops/gitea-admin-password" --with-decryption --query 'Parameter.Value' --output text)
echo "=== Gitea 웹 인터페이스 접근 정보 ==="
echo "Public URL (브라우저 접근용): $GITEA_PUBLIC_IP"
echo "Username: admin"
echo "Password: $GITEA_ADMIN_PASSWORD"
echo "======================================"
브라우저에서 URL을 열고 eks-saas-gitops 저장소로 이동합니다. 워크플로우가 새 Premium 테넌트의 Helm 릴리즈 파일을 생성하고 Gitea GitOps 저장소에 푸시한 것을 확인할 수 있습니다.
Basic 티어 테넌트 프로비저닝
Basic 티어 테넌트는 리소스를 공유합니다. 샘플 애플리케이션에서 Basic 티어 테넌트들은 실습 2에서 설명한 것처럼 동일한 마이크로서비스 인스턴스와 인프라 리소스를 함께 사용합니다. basic_env_template.yaml은 여러 Basic 티어 테넌트를 수용할 수 있는 풀 환경을 생성하는 데 사용됩니다. 이 접근 방식은 여러 테넌트 간에 인프라를 공유함으로써 리소스 사용을 최적화하고 비용을 절감합니다.
Basic 테넌트 프로비저닝
Basic 티어에 새 테넌트를 프로비저닝하려면 ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL 큐에 필요한 메타데이터(tenant_id, tenant_tier, release_version)를 담아 메시지를 전송합니다.
aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL \
--message-body '{
"tenant_id": "tenant-2",
"tenant_tier": "basic",
"release_version": "0.0"
}'
Argo Workflows Web UI에서 워크플로우 확인
Argo Workflows Web UI를 열어 새 테넌트 온보딩을 위해 생성된 워크플로우를 확인합니다.
워크플로우의 단계는 동일하지만, 테넌트는 서로 다른 방식으로 배포됩니다. 이 차이는 실습 1에서 살펴본 것처럼 테넌트 설정을 캡슐화하는 템플릿 파일(Helm 릴리즈 및 Terraform)에 의해 제어됩니다.
GitOps 변경 사항 검증
Gitea 웹 인터페이스에 접근하거나 VSCode 서버 환경의 로컬 저장소를 확인하여 Gitea 저장소의 변경 사항을 직접 살펴볼 수 있습니다. 워크플로우 실행이 완료되면 온보딩 프로세스가 Gitea 저장소에 생성한 커밋을 확인할 수 있습니다.
이 단계들을 통해 새 Basic 티어 테넌트를 프로비저닝하고, GitOps 저장소에서 해당 워크플로우와 커밋을 확인하는 전 과정을 실습했습니다. Basic 티어 테넌트가 리소스를 공유하면서도 자동화된 워크플로우와 GitOps 방식으로 효과적으로 관리되는 구조를 직접 확인한 것입니다.
Advanced 티어 테넌트 프로비저닝
Advanced 티어 테넌트는 일부 리소스는 전용(사일로)으로, 일부는 공유(풀)로 운영하는 혼합 모델을 사용합니다. 이 접근 방식은 컴플라이언스 요구사항이나 노이지 네이버(Noisy Neighbor) 문제 완화 등의 이유로 특정 리소스를 반드시 전용으로 운영해야 하는 SaaS 사업자가 흔히 채택합니다. 이번 워크샵 샘플 솔루션에서 Advanced 티어 테넌트는 producer 마이크로서비스를 풀로 공유하고, consumer 마이크로서비스는 사일로로 전용 운영합니다.
티어 전략과 구현에 대한 자세한 내용은 실습 2를 참고합니다.
Advanced 테넌트 프로비저닝
tenant_tier를 advanced로 설정하여 tenant-3을 프로비저닝합니다.
aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL \
--message-body '{
"tenant_id": "tenant-3",
"tenant_tier": "advanced",
"release_version": "0.0"
}'
Argo Workflows Web UI에서 워크플로우 확인
Argo Workflows Web UI를 열어 새 테넌트 온보딩을 위해 생성된 워크플로우를 확인합니다.
워크플로우의 단계는 동일하지만, 테넌트는 서로 다른 방식으로 배포됩니다. 이 차이는 실습 1에서 살펴본 것처럼 테넌트 설정을 캡슐화하는 템플릿 파일(Helm 릴리즈 및 Terraform)에 의해 제어됩니다.
GitOps 변경 사항 검증
Gitea 웹 인터페이스에 접근하거나 VSCode 서버 환경의 로컬 저장소를 확인하여 변경 사항을 직접 살펴볼 수 있습니다. 워크플로우 실행이 완료되면 tenant-3에 대해 Gitea 저장소에 커밋된 파일을 확인합니다.
워크플로우가 새 테넌트를 Advanced 티어로 프로비저닝한 것을 확인할 수 있습니다. enable_producer는 false, enable_consumer는 true로 설정되어 있어 tenant-3은 사일로와 풀 리소스를 혼합하여 사용합니다. 구체적으로는 컨슈머 마이크로서비스를 위한 전용 네임스페이스 tenant-3이 구성되어 격리된 전용 리소스를 보장하는 한편, 프로듀서 마이크로서비스는 공유 네임스페이스 pool-1에 배포됩니다.
다음으로 Flux를 확인하고 테넌트 배포를 테스트합니다.
리소스 확인
GitOps 전략의 장점 중 하나는 모든 설정 파일이 Git 저장소에 존재한다는 것입니다. 먼저 원격 저장소를 로컬로 풀(Pull)하여 Argo Workflows가 새 테넌트를 온보딩하면서 적용한 변경 사항을 확인합니다.
cd /home/ec2-user/environment/gitops-gitea-repo
git pull origin main
온보딩한 모든 테넌트가 Git에 정의되어 있는 것을 확인할 수 있습니다. 저장소 구조를 살펴보며 테넌트 HelmRelease가 어디에 생성됐는지 확인합니다.
tree application-plane/production/tenants/
├── advanced
│ ├── dummy-configmap.yaml
│ ├── kustomization.yaml
│ ├── tenant-3.yaml
│ └── tenant-t1d6c.yaml
├── basic
│ ├── dummy-configmap.yaml
│ ├── kustomization.yaml
│ └── tenant-2.yaml
├── kustomization.yaml
└── premium
├── dummy-configmap.yaml
├── kustomization.yaml
└── tenant-1.yaml
Flux 확인
IDE 터미널에서 tenant-1, tenant-2, tenant-3의 HelmRelease를 포함한 모든 Flux 리소스를 조회합니다.
flux get helmreleases
NAME REVISION SUSPENDED READY MESSAGE
argo-events 2.4.3 False True Helm install succeeded
argo-workflows 0.40.11 False True Helm install succeeded
aws-load-balancer-controller 1.6.2 False True Helm install succeeded
karpenter 1.4.0 False True Helm install succeeded
kubecost 2.1.0 False True Helm install succeeded
metrics-server 3.11.0 False True Helm install succeeded
onboarding-service 0.0.1 False True Helm install succeeded
pool-1 0.0.1 False True Helm install succeeded
tenant-1-premium 0.0.1 False True Helm install succeeded
tenant-2-basic 0.0.1 False True Helm install succeeded
tenant-3-advanced 0.0.1 False True Helm install succeeded
tf-controller 0.16.0-rc.4 False True Helm install succeeded
위와 같이 모든 테넌트가 목록에 표시된다면 Flux가 3개 테넌트 모두의 리소스를 정상적으로 배포한 것입니다.
Flux는 각 오브젝트의 interval 속성을 통해 Reconciliation 주기를 설정할 수 있습니다. 새로운 Helm 릴리즈를 Git에 생성했는데 목록에 보이지 않는다면, 다음 명령어로 Flux가 Git 변경 사항을 즉시 확인하도록 강제할 수 있습니다.
flux reconcile source git flux-system
이제 각 테넌트 HelmRelease에 사용된 차트를 확인합니다.
flux get sources chart
NAME REVISION SUSPENDED READY MESSAGE
flux-system-argo-events 2.4.3 False True pulled 'argo-events' chart with version '2.4.3'
flux-system-argo-workflows 0.40.11 False True pulled 'argo-workflows' chart with version '0.40.11'
flux-system-aws-load-balancer-controller 1.6.2 False True pulled 'aws-load-balancer-controller' chart with version '1.6.2'
flux-system-karpenter 1.4.0 False True pulled 'karpenter' chart with version '1.4.0'
flux-system-kubecost 2.1.0 False True pulled 'cost-analyzer' chart with version '2.1.0'
flux-system-metrics-server 3.11.0 False True pulled 'metrics-server' chart with version '3.11.0'
flux-system-onboarding-service 0.0.1 False True pulled 'application-chart' chart with version '0.0.1'
flux-system-pool-1 0.0.1 False True pulled 'helm-tenant-chart' chart with version '0.0.1'
flux-system-tenant-1-premium 0.0.1 False True pulled 'helm-tenant-chart' chart with version '0.0.1'
flux-system-tenant-2-basic 0.0.1 False True pulled 'helm-tenant-chart' chart with version '0.0.1'
flux-system-tenant-3-advanced 0.0.1 False True pulled 'helm-tenant-chart' chart with version '0.0.1'
flux-system-tf-controller 0.16.0-rc.4 False True pulled 'tf-controller' chart with version '0.16.0-rc.4'
모든 테넌트 환경에 동일한 helm-tenant-chart가 사용되고 있으며, 모두 0.0.1 버전으로 배포되어 있습니다. 버전은 HelmRelease에서 제어하며, 차트 버전을 올리고 싶다면 해당 HelmRelease 매니페스트의 spec.chart.spec.version 값을 수정해 GitOps 흐름으로 롤아웃하면 됩니다.
클러스터 리소스 확인
테넌트별로 배포된 쿠버네티스 리소스를 직접 확인합니다.
# Premium 테넌트
kubectl -n tenant-1 get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
tenant-1-consumer 3/3 3 3 5m48s
tenant-1-producer 3/3 3 3 5m48s
# Basic 테넌트
kubectl -n tenant-2 get deployment
No resources found in tenant-2 namespace.
Basic 티어이므로 tenant-2 전용 네임스페이스나 전용 리소스가 존재하지 않습니다.
# Advanced 테넌트
kubectl -n tenant-3 get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
tenant-3-consumer 3/3 3 3 4m29s
프로듀서는 풀(Pool) 방식으로 운영되므로 tenant-3 네임스페이스에는 컨슈머 마이크로서비스만 생성됩니다.
이제 테넌트들이 공유하는 pool-1 네임스페이스의 디플로이먼트를 확인합니다.
# Pool-1 환경
kubectl -n pool-1 get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
pool-1-consumer 3/3 3 3 18h
pool-1-producer 3/3 3 3 18h
마지막으로 pool-1 네임스페이스에 생성된 인그레스 리소스를 확인합니다.
kubectl get ingress -npool-1
tenant-2는 프로듀서와 컨슈머 양쪽 모두의 라우트가 생성되어 있는 반면, Advanced 티어인 tenant-3은 프로듀서 라우트만 존재하는 것을 확인할 수 있습니다.
인프라 리소스 확인
Tofu Controller가 배포한 인프라 리소스도 동일하게 확인할 수 있습니다.
kubectl get secrets -nflux-system | grep -i state
tfstate-default-pool-1 Opaque 1 44m
tfstate-default-tenant-1 Opaque 1 23m
tfstate-default-tenant-2 Opaque 1 15m
tfstate-default-tenant-3 Opaque 1 10m
테넌트별로 생성된 Terraform 상태 파일 목록을 확인할 수 있습니다. 다음 단계로 이동하여 테넌트 마이크로서비스 배포를 테스트합니다.
테넌트 배포 테스트
프로듀서와 컨슈머 마이크로서비스로 구성된 각 테넌트의 샘플 애플리케이션에 접근해봅니다. 프로듀서에 POST 요청을 전송하고 컨슈머가 DynamoDB에 항목을 정상적으로 저장하는지 확인하여 인프라를 검증합니다.
마이크로서비스 테스트
먼저 애플리케이션 로드 밸런서 엔드포인트를 가져옵니다. 이 샘플에서는 모든 테넌트가 동일한 ALB를 사용하며, tenantID 헤더로 각 테넌트 환경에 라우팅합니다.
export APP_LB=http://$(kubectl get ingress -n tenant-1 -o json | jq -r .items[0].status.loadBalancer.ingress[0].hostname)
tenant-1 (Premium 티어) 테스트
curl -s -H "tenantID: tenant-1" $APP_LB/producer | jq
curl -s -H "tenantID: tenant-1" $APP_LB/consumer | jq
{
"environment": "tenant-1",
"microservice": "producer",
"tenant_id": "tenant-1",
"version": "0.0.1"
}
{
"environment": "tenant-1",
"microservice": "consumer",
"tenant_id": "tenant-1",
"version": "0.0.1"
}
Premium 티어인 tenant-1은 프로듀서와 컨슈머 모두 tenant-1 전용 환경에서 실행되고 있습니다.
tenant-2 (Basic 티어) 테스트
curl -s -H "tenantID: tenant-2" $APP_LB/producer | jq
curl -s -H "tenantID: tenant-2" $APP_LB/consumer | jq
{
"environment": "pool-1",
"microservice": "producer",
"tenant_id": "tenant-2",
"version": "0.0.1"
}
{
"environment": "pool-1",
"microservice": "consumer",
"tenant_id": "tenant-2",
"version": "0.0.1"
}
Basic 티어인 tenant-2는 프로듀서와 컨슈머 모두 공유 환경 pool-1에서 실행되고 있습니다.
tenant-3 (Advanced 티어) 테스트
curl -s -H "tenantID: tenant-3" $APP_LB/producer | jq
curl -s -H "tenantID: tenant-3" $APP_LB/consumer | jq
{
"environment": "pool-1",
"microservice": "producer",
"tenant_id": "tenant-3",
"version": "0.0.1"
}
{
"environment": "tenant-3",
"microservice": "consumer",
"tenant_id": "tenant-3",
"version": "0.0.1"
}
Advanced 티어인 tenant-3은 프로듀서는 공유 네임스페이스 pool-1에서, 컨슈머는 전용 네임스페이스 tenant-3에서 실행되고 있습니다.
인프라 검증
프로듀서에 POST 요청을 전송하고 컨슈머가 DynamoDB에 항목을 정상적으로 저장하는지 확인하여 인프라를 검증합니다.
프로듀서에 POST 요청 전송
curl --location --request POST "$APP_LB/producer" \
--header 'tenantID: tenant-3' \
--header 'tier: advanced'
tenant-3의 DynamoDB 테이블 이름 조회
테이블 이름에 랜덤 해시가 포함되어 있으므로 먼저 이름을 가져옵니다.
TABLE_NAME=$(aws dynamodb list-tables --region $AWS_REGION --query "TableNames[?contains(@, 'tenant-3')]" --output text)
DynamoDB에서 저장된 항목 확인
aws dynamodb scan --table-name $TABLE_NAME --region $AWS_REGION
항목이 정상적으로 저장됐다면 아래와 같은 결과를 확인할 수 있습니다.
{
"Items": [
{
"consumer_environment": {
"S": "tenant-3"
},
"producer_environment": {
"S": "pool-1"
},
"message_id": {
"S": "721accc6-e2c5-4885-b8a5-afc13c247cec"
},
"tenant_id": {
"S": "tenant-3"
},
"timestamp": {
"S": "2026-04-25T16:31:39+0000"
}
}
],
"Count": 1,
"ScannedCount": 1,
"ConsumedCapacity": null
}
이 단계들을 통해 각 테넌트의 마이크로서비스를 테스트하고, DynamoDB의 데이터 저장 여부를 확인하여 인프라 구성을 검증했습니다. 서로 다른 티어에 걸친 배포와 통합이 정상적으로 동작하고 있음을 확인한 것입니다.
테넌트 오프보딩
이 섹션에서는 온보딩 프로세스와 유사하게 Argo Workflows를 사용하여 테넌트를 오프보딩하는 과정을 살펴봅니다. 실습 2에서 프로비저닝한 tenant-t1d6c를 제거하고 연관된 모든 리소스가 정상적으로 정리되는지 확인합니다.
오프보딩 요청 전송
먼저 Amazon SQS 오프보딩 큐에 필요한 메타데이터(tenant_id, tenant_tier)를 담아 메시지를 전송합니다.
export ARGO_WORKFLOWS_OFFBOARDING_QUEUE_SQS_URL=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.argoworkflows_offboarding_queue_url}')
aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_OFFBOARDING_QUEUE_SQS_URL \
--message-body '{
"tenant_id": "tenant-t1d6c",
"tenant_tier": "advanced"
}'
Argo Workflows 확인
테넌트 오프보딩을 위해 생성된 워크플로우를 확인합니다.
kubectl -n argo-workflows get workflow
아래와 유사한 출력을 확인할 수 있습니다.
NAME STATUS AGE MESSAGE
tenant-offboarding-gzt4s Running 9s
Argo Workflows Web UI에서 워크플로우 확인
Argo Workflows Web UI를 열어 워크플로우 실행 현황을 확인합니다.
ARGO_WORKFLOW_URL=$(kubectl -n argo-workflows get service/argo-workflows-server -o json | jq -r '.status.loadBalancer.ingress[0].hostname')
echo http://$ARGO_WORKFLOW_URL:2746/workflows
브라우저에서 해당 URL을 열면 워크플로우 현황을 확인할 수 있으며, 워크플로우를 클릭하면 각 단계의 실행 정보와 로그를 상세히 살펴볼 수 있습니다.
GitOps 변경 사항 검증
워크플로우 실행이 완료되면 Gitea 웹 인터페이스에 접근하여 오프보딩 프로세스가 생성한 커밋을 확인합니다. 테넌트 리소스 제거와 관련된 커밋들을 확인할 수 있습니다.
마치며
이번 6주차에서는 GitOps 원칙을 EKS 기반 SaaS 환경에 적용하는 전 과정을 살펴봤습니다. Part 1에서는 Flux v2와 Tofu Controller로 Git 저장소를 단일 진실 공급원(SSoT)으로 두고 인프라/애플리케이션 상태를 선언적으로 관리하는 기반을 구성했고, Part 2에서는 Basic·Premium·Advanced 세 가지 티어 전략을 정의한 뒤 Argo Workflows로 테넌트 온보딩과 오프보딩을 자동화했습니다.
핵심은 결국 "개발자가 Git push만으로 자기 인프라를 셀프서비스로 다룰 수 있게 만드는 것"이며, 이를 위해 플랫폼 팀은 안전한 가드레일과 재사용 가능한 템플릿을 미리 설계해 두어야 한다는 점이었습니다. 이번 스터디에서 다룬 패턴은 실제 멀티테넌트 SaaS를 운영할 때 좋은 출발점이 될 수 있을 것입니다.