블로그 불러오는 중...
문의 보내기
남겨주면 블로그 주인에게 바로 전달돼요.
오늘은 AWS EKS 환경에서의 CI/CD 를 알아본다.
오늘은 AWS 의 최영락 님께서 강의를 해 주셨다.
아래 사진이 오늘 실습을 해볼 과정이다.

실습의 기반 워크숍은 다음과 같다. https://catalog.workshops.aws/eks-saas-gitops/en-US
GitOps 는 Git 저장소를 단일 진실 공급원(Single Source of Truth)으로 두고, 클러스터의 상태를 Git 의 상태와 동기화하는 운영 방식이다.
이번 실습에서는 다음과 같은 컴포넌트를 사용한다.
[ec2-user@ip-10-0-1-93 environment]$ kubectl get ns
NAME STATUS AGE
argo-events Active 2d6h
argo-workflows Active 2d6h
aws-system Active 2d6h
default Active 2d7h
flux-system Active 2d6h
karpenter Active 2d6h
kube-node-lease Active 2d7h
kube-public Active 2d7h
kube-system Active 2d7h
kubecost Active 2d6h
onboarding-service Active 2d6h
pool-1 Active 2d6hgitea 설치 된 것을 확인하고
# Get Gitea IPs from the configuration
export GITEA_PRIVATE_IP=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.gitea_url}')
export GITEA_PUBLIC_IP=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.gitea_public_url}')
export GITEA_PORT="3000"
# Get Gitea admin password from Systems Manager Parameter Store
export GITEA_ADMIN_PASSWORD=$(aws ssm get-parameter --name "/eks-saas-gitops/gitea-admin-password" --with-decryption --query 'Parameter.Value' --output text)
# Display access information for web browser login
echo "=== Gitea Web Interface Access ==="
echo "Public URL (for browser access): $GITEA_PUBLIC_IP"
echo "Username: admin"
echo "Password: $GITEA_ADMIN_PASSWORD"
echo "=================================="
echo ""
echo "Use the PUBLIC URL above to access Gitea from your web browser."=== Gitea Web Interface Access ===
Public URL (for browser access): http://34.221.28.68:3000
Username: admin
Password: 8y)kOg4yWoSyo&Q%
==================================이렇게 출력이 되었다.

gitea 에 들어가 보면 다음과 같은 저장소들이 있었다.
# Extract Gitea configuration from ConfigMap
export GITEA_TOKEN=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.gitea_token}')
# Set up the repository paths used throughout the workshop
export REPO_PATH="/home/ec2-user/environment/microservice-repos"
export GITOPS_REPO_PATH="/home/ec2-user/environment/gitops-gitea-repo"
mkdir -p $REPO_PATH해당 명령어를 통해 gitea 에 접근할 수 있는 토큰등을 설정한다.
아래 명령어를 통해 git clone 을 받고 폴더를 확인해 본다.
[ec2-user@ip-10-0-1-93 microservice-repos]$ git clone http://admin:${GITEA_TOKEN}@${GITEA_PRIVATE_IP}:${GITEA_PORT}/admin/producer.git
git clone http://admin:${GITEA_TOKEN}@${GITEA_PRIVATE_IP}:${GITEA_PORT}/admin/consumer.git
git clone http://admin:${GITEA_TOKEN}@${GITEA_PRIVATE_IP}:${GITEA_PORT}/admin/payments.git
tree
Cloning into 'producer'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
Cloning into 'consumer'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
Cloning into 'payments'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
.
├── consumer
│ ├── Dockerfile
│ ├── consumer.py
│ └── requirements.txt
├── payments
│ ├── Dockerfile
│ ├── payments.py
│ └── requirements.txt
└── producer
├── Dockerfile
├── producer.py
└── requirements.txt플럭스 시스템이 gitea 를 감시해서 클러스터 상태와 일치하도록 구성되어 있다.
[ec2-user@ip-10-0-1-93 microservice-repos]$ kubectl -n flux-system get gitrepository -o yaml
apiVersion: v1
items:
- apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
annotations:
kustomize.toolkit.fluxcd.io/prune: Disabled
kustomize.toolkit.fluxcd.io/ssa: Ignore
creationTimestamp: "2026-04-24T05:47:48Z"
finalizers:
- finalizers.fluxcd.io
generation: 1
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/managed-by: flux-operator
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.7.5
fluxcd.controlplane.io/name: flux
fluxcd.controlplane.io/namespace: flux-system
name: flux-system
namespace: flux-system
resourceVersion: "1839817"
uid: 3ef2141a-52c9-4107-9b01-382ce16e5798
spec:
interval: 1m0s
ref:
name: refs/heads/main
secretRef:
name: flux-system
timeout: 60s
url: http://10.35.48.242:3000/admin/eks-saas-gitops.git
status:
artifact:
digest: sha256:77be0a1a9e7b8d8603383f7f9d00d9af257393e1c36bb8c48bf7edff077f3ff7
lastUpdateTime: "2026-04-24T05:51:53Z"
path: gitrepository/flux-system/flux-system/49b01638ae188f9e1a9bb9f967e9473baf13e9fc.tar.gz
revision: refs/heads/main@sha1:49b01638ae188f9e1a9bb9f967e9473baf13e9fc
size: 29962
url: http://source-controller.flux-system.svc.cluster.local./gitrepository/flux-system/flux-system/49b01638ae188f9e1a9bb9f967e9473baf13e9fc.tar.gz
conditions:
- lastTransitionTime: "2026-04-24T05:51:53Z"
message: stored artifact for revision 'refs/heads/main@sha1:49b01638ae188f9e1a9bb9f967e9473baf13e9fc'
observedGeneration: 1
reason: Succeeded
status: "True"
type: Ready
- lastTransitionTime: "2026-04-24T05:51:53Z"
message: stored artifact for revision 'refs/heads/main@sha1:49b01638ae188f9e1a9bb9f967e9473baf13e9fc'
observedGeneration: 1
reason: Succeeded
status: "True"
type: ArtifactInStorage
lastHandledReconcileAt: "2026-04-24T05:49:37.800653683Z"
observedGeneration: 1
- apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
creationTimestamp: "2026-04-24T05:47:56Z"
finalizers:
- finalizers.fluxcd.io
generation: 1
labels:
kustomize.toolkit.fluxcd.io/name: sources
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: terraform-v0-0-1
namespace: flux-system
resourceVersion: "3642"
uid: 9b8d2e0b-cf16-42da-995d-4ea76b54670b
spec:
interval: 300s
ref:
tag: v0.0.1
secretRef:
name: flux-system
timeout: 60s
url: http://10.35.48.242:3000/admin/eks-saas-gitops.git
status:
artifact:
digest: sha256:00cbf55dd85a7516b9a2cf826182830cfa25a8c9317670066588a8fb431921ea
lastUpdateTime: "2026-04-24T05:47:58Z"
path: gitrepository/flux-system/terraform-v0-0-1/2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2.tar.gz
revision: v0.0.1@sha1:2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2
size: 29937
url: http://source-controller.flux-system.svc.cluster.local./gitrepository/flux-system/terraform-v0-0-1/2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2.tar.gz
conditions:
- lastTransitionTime: "2026-04-24T05:47:58Z"
message: stored artifact for revision 'v0.0.1@sha1:2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2'
observedGeneration: 1
reason: Succeeded
status: "True"
type: Ready
- lastTransitionTime: "2026-04-24T05:47:58Z"
message: stored artifact for revision 'v0.0.1@sha1:2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2'
observedGeneration: 1
reason: Succeeded
status: "True"
type: ArtifactInStorage
observedGeneration: 1
kind: List
metadata:
resourceVersion: ""
[ec2-user@ip-10-0-1-93 microservice-repos]$ kubectl -n flux-system get gitrepository
NAME URL AGE READY STATUS
flux-system http://10.35.48.242:3000/admin/eks-saas-gitops.git 2d6h True stored artifact for revision 'refs/heads/main@sha1:49b01638ae188f9e1a9bb9f967e9473baf13e9fc'
terraform-v0-0-1 http://10.35.48.242:3000/admin/eks-saas-gitops.git 2d6h True stored artifact for revision 'v0.0.1@sha1:2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2'확인 해 보면 다음과 같이 설정이 된 것을 확인 해 볼 수 있다.
tree /home/ec2-user/environment/gitops-gitea-repo/terraform/modules/ -L 2
핵심 모듈은 tenant-apps 에 있다.
cd /home/ec2-user/environment/gitops-gitea-repo/
cat << EOF > terraform_test.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.100.0"
}
}
}
provider "aws" {}
module "test_tenant_apps" {
source = "./terraform/modules/tenant-apps"
tenant_id = "test"
enable_producer = true
enable_consumer = true
}
EOF초기화 하기 전에 배포 계획을 확인 해 본다.
terraform init
terraform plan | tee -a tfplan-1.txtPlan: 11 to add, 0 to change, 0 to destroy. 로 11개의 자원이 생성된다고 한다.
두번째로 enable_producer = false 로 변경해서 다시 plan 을 실행해 본다.
cat << EOF > terraform_test.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.100.0"
}
}
}
provider "aws" {}
module "test_tenant_apps" {
source = "./terraform/modules/tenant-apps"
tenant_id = "test"
enable_producer = false
enable_consumer = true
}
EOFterraform plan | tee -a tfplan-2.txt 로 실행해 본다.
Plan: 10 to add, 0 to change, 0 to destroy. 이번엔 이런 결과가 나온다. producer 가 빠진 만큼 자원이 1개 줄어든 것을 볼 수 있다.

Flux 가 git 저장소의 변화를 감지한다. 그 git 저장소 내용에 따라서 클러스터의 상태를 유지하게 된다.
아래는 해당 내용에 대한 플로우이다.
1. Git 저장소에 Terraform CRD 파일 추가 (git push)
│
▼
2. Flux가 변경 감지 → 조정 시작
│
▼
3. tf-controller가 Terraform CRD 감지
│
▼
4. tf-runner Pod 실행 → Git에서 Terraform 모듈 pull
│
▼
5. Terraform apply → SQS, DynamoDB, IRSA 생성
│
▼
6. 실행 상태/플랜을 Kubernetes Secret으로 저장flux 가 정상인지 확인한다.
kubectl get pod -n flux-system -l app.kubernetes.io/instance=tf-controller
kubectl get pod -n flux-systemcat << EOF > /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/example-tenant-terraform-crd.yaml
---
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
name: example-tenant
namespace: flux-system
spec:
path: ./terraform/modules/tenant-apps
interval: 1m
approvePlan: auto
destroyResourcesOnDeletion: true
sourceRef:
kind: GitRepository
name: terraform-v0-0-1
vars:
- name: tenant_id
value: example-tenant
- name: "enable_producer"
value: true
- name: "enable_consumer"
value: true
writeOutputsToSecret:
name: example-tenant-infra-output
EOFkubectl get GitRepository terraform-v0-0-1 -nflux-system -oyaml | grep -i spec -C10
명령어로 어떤 부분을 보고 있는 지 확인한다.

다시 kustomization.yaml 에 아까 만든 파일을 flux 가 인식하게 끔 변경해 본다.
cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- basic
- advanced
- premium
- example-tenant-terraform-crd.yaml
EOFgit 에 해당 변경사항을 푸시한다.
cd /home/ec2-user/environment/gitops-gitea-repo/
git pull origin main
git status
git add .
git commit -am "Added example terraform CRD for testing"
git push origin main기다려도 되지만 빠르게 실행해 본다.
flux reconcile source git flux-system
해당 명령어를 입력하고 실제 반영되는지 확인 해 보자.
kubectl get po -n flux-system 를 입력하고 example-tenant-tf-runner 가 나올때까지 기다려 본다.
kubectl logs -n flux-system -l app.kubernetes.io/name=tf-runner -f
해당 명령어를 입력 시 다음과 같은 명령어로 로그를 확인한다.
우리가 만든 SQS, DynamoDB 가 실제로 잘 적용 되었는지 확인한다.
실제로 테라폼이 동작하면서 아래처럼 SQS 및 DynamoDB 테이블이 생성 된 것을 볼 수 있다.

이제는 삭제한다. 아래와 같이 추가한 내용을 삭제하고 푸시해 본다.

cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- basic
- advanced
- premium
EOF아래와 같이 변경사항을 커밋한다.
cd /home/ec2-user/environment/gitops-gitea-repo/
git pull origin main
git add .
git commit -m "Removed Terraform CRD and reference from kustomization.yaml"
git push origin mainflux reconcile source git flux-system 명령어를 통해 동기화를 한다.
kubectl logs po/example-tenant-tf-runner -nflux-system -f 명령어로 테라폼이 실행되는지 확인한다.

위 사진과 같이 테라폼이 실행되어 자원이 제거된 것을 확인할 수 있다.
실제 콘솔에서도 관련 자원이 삭제가 된 것을 확인할 수 있다.

그리고 해당 terraform apply 등 내용이 secret 에 저장이 된다.
kubectl get secret -n flux-system | grep -E 'tfplan|tfstate' 를 이용해 확인 할 수 있다.
tree /home/ec2-user/environment/gitops-gitea-repo/helm-charts/ 명령어로 헬름차트를 확인해 보자.

helm-charts/
├── helm-tenant-chart/ # 테넌트 애플리케이션용 (Producer + Consumer 묶음)
│ ├── Chart.yaml # 차트 메타데이터 (이름, 버전, 설명)
│ ├── values.yaml # 기본 설정값
│ └── templates/ # Kubernetes 매니페스트 템플릿
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ └── serviceaccount.yaml
└── application-chart/ # 애플리케이션 1:1 대응 (테넌트 구분 없음)cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/helm-charts/helm-tenant-chart/test-values.yaml
tenantId: "example-tenant"
apps:
producer:
enabled: true
consumer:
enabled: true
EOFtest-value 로 헬름 차트를 렌더링을 해 보자.
helm template example-tenant ./helm-charts/helm-tenant-chart --values ./helm-charts/helm-tenant-chart/test-values.yaml
추가로 다시 false 로 변경 이후에 렌더링 해보자.
cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/helm-charts/helm-tenant-chart/test-values.yaml
tenantId: "example-tenant"
apps:
producer:
enabled: false
consumer:
enabled: true
EOFhelm template example-tenant ./helm-charts/helm-tenant-chart --values ./helm-charts/helm-tenant-chart/test-values.yaml
로 또 렌더링을 해 보자. 두개의 차이를 확인 해 보자.
이런 식으로 values 를 관리하는 형태로 helm 의 value 값을 설정해서 관리한다.
# Get values from configmap
AWS_ACCOUNT_ID=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.account_id}')
ECR_HELM_CHART_URL=$(kubectl get configmap saas-infra-outputs -n flux-system -o jsonpath='{.data.ecr_helm_chart_url}')
ECR_REGISTRY=$(echo $ECR_HELM_CHART_URL | cut -d'/' -f1)
ECR_REPOSITORY=$(echo $ECR_HELM_CHART_URL | cut -d'/' -f2-)
AWS_REGION=$(echo $ECR_HELM_CHART_URL | cut -d'.' -f4)
# Authenticate Docker to ECR
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY
# List artifacts (images) in the ECR repository
aws ecr list-images --repository-name $ECR_REPOSITORY --region $AWS_REGION0.0.1 로 태깅이 되어 있는 것을 볼 수 있다.

HelmRelease 는 Flux 가 제공하는 CRD 로, Helm 릴리스를 선언적으로 관리할 수 있게 해 준다.
HelmRelease (Git에 선언)
│
▼
Flux Helm Controller
│
▼
HelmRepository (ECR의 Helm 차트 참조)
│
▼
Kubernetes 클러스터에 실제 배포HelmRelease 를 만들어 본다.
cat << EOF > /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/example-tenant-helmrelease.yaml
apiVersion: v1
kind: Namespace
metadata:
name: example-tenant
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: example-tenant-premium
namespace: flux-system
spec:
releaseName: example-tenant-premium
targetNamespace: example-tenant
storageNamespace: example-tenant
interval: 1m0s
chart:
spec:
chart: helm-tenant-chart
version: "0.x"
sourceRef:
kind: HelmRepository
name: helm-tenant-chart
values:
tenantId: example-tenant
apps:
producer:
enabled: true
consumer:
enabled: true
EOF마지막 줄에 하나 추가한다.
cat << EOF >> /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/kustomization.yaml
- example-tenant-helmrelease.yaml
EOF
나의 경우 마지막 부분이 indentation 이 한칸 뒤로 가 있어서 적용이 안되었었다. indentation 을 맞추어 준다.
커밋 + 푸시한다.
cd /home/ec2-user/environment/gitops-gitea-repo/
git pull origin main
git add .
git commit -m "Added HelmRelease for example-tenant"
git push origin main동기화 한다.
flux reconcile source git flux-system
kubectl logs po/example-tenant-tf-runner -nflux-system -f 로그를 확인한다.

네임스페이스, 네임스페이스 내 자원, AWS 자원 등을 아래 명령어로 확인 한다.
kubectl get namespaces
kubectl get all -n example-tenant
aws dynamodb list-tables
aws sqs list-queues
생성 된 것을 볼 수 있다.
rm /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/example-tenant-helmrelease.yaml
sed -i '/example-tenant-helmrelease.yaml/d' /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/kustomization.yaml
# 커밋 및 푸시
cd /home/ec2-user/environment/gitops-gitea-repo/
git pull origin main
git add .
git commit -m "Removed HelmRelease for example-tenant"
git push origin main
# 동기화
flux reconcile source git flux-system
kubectl logs po/example-tenant-tf-runner -nflux-system -f
# 자원 확인
kubectl get namespaces
kubectl get all -n example-tenant
aws dynamodb list-tables
aws sqs list-queues인프라 코드를 기준으로 실제 인프라가 맞춰지는 것을 볼 수 있었다.
티어별 다른 관리 형태로 관리하는 부분을 실습해 본다.

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테넌트 추가할 폴더를 생성한다.
mkdir -p /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/advanced
고급 테넌트를 수동 프로비저닝 한다.
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"cat << EOF > tenants/advanced/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- $TENANT_ID.yaml
EOFcd /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동기화를 해 준다.
flux reconcile source git flux-system

다음과 같이 생성 된 것을 볼 수 있다.
세 가지 워크플로우 템플릿이 사전 구성되어 있다. 확인 해 보자.
kubectl get workflowtemplates -n argo-workflows
[ec2-user@ip-10-0-1-93 gitops-gitea-repo]$ kubectl get workflowtemplates -n argo-workflows
NAME AGE
tenant-deployment-template 2d8h
tenant-offboarding-template 2d8h
tenant-onboarding-template 2d8h다음과 같이 템플릿 3개가 보이는 것을 볼 수 있다.

SQS 메시지 수신 (Argo Events + Sensor)
│
▼
온보딩 워크플로우 트리거
│
▼
Gitea 저장소 클론
│
▼
티어 템플릿 기반 테넌트 HelmRelease 파일 생성
(Basic / Advanced / Premium)
│
▼
Git 커밋 & 푸시
│
▼
Flux v2가 변경 감지 → EKS에 리소스 배포큐를 이용해서 Premium 테넌트를 온보딩 해보는 실습이다. 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"
}'워크플로우를 확인해 본다.
kubectl -n argo-workflows get workflow

ArgoCD 의 화면을 봐 보자.
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다음과 같이 확인 해 볼 수 있다.

그리고 자동으로 커밋의 경우에도 추가가 되어 있다.

tenant-2 (basic), tenant-3 (advanced) 도 동일한 방식으로 SQS 메시지로 온보딩 했다. 이제 티어별로 실제 Kubernetes 리소스가 어떻게 배포 되었는지 비교 해 보자.
# Premium 티어: Producer + Consumer 전용 배포
kubectl -n tenant-1 get deploymentNAME READY UP-TO-DATE AVAILABLE AGE
tenant-1-consumer 3/3 3 3 5m48s
tenant-1-producer 3/3 3 3 5m48s# Basic 티어: 전용 Deployment 없음 (pool-1 공유 사용)
kubectl -n tenant-2 get deploymentNo resources found in tenant-2 namespace.# Advanced 티어: Consumer 만 전용 배포
kubectl -n tenant-3 get deploymentNAME READY UP-TO-DATE AVAILABLE AGE
tenant-3-consumer 3/3 3 3 4m29s# Pool-1: Basic + Advanced 테넌트가 공유하는 환경
kubectl -n pool-1 get deploymentNAME READY UP-TO-DATE AVAILABLE AGE
pool-1-consumer 3/3 3 3 18h
pool-1-producer 3/3 3 3 18hpool-1 네임스페이스의 Ingress 를 확인 해 보면 Basic 티어 (tenant-2) 는 Producer/Consumer 라우팅이 모두 있고, Advanced 티어 (tenant-3) 는 Producer 라우팅만 있는 것을 볼 수 있다.
kubectl get ingress -n pool-1Tofu 컨트롤러가 생성한 Terraform 상태 파일도 같이 확인 해 본다.
kubectl get secrets -n flux-system | grep -i statetfstate-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여기서 한 가지 짚고 넘어 갈 점은, Basic 티어 (tenant-2) 에도 Terraform 상태가 존재 한다는 것이다. Kubernetes 리소스는 pool-1 을 공유 하지만, 테넌트 식별 정보 같은 일부 인프라 설정은 테넌트 별로 따로 관리 된다는 의미로 보면 될 것 같다.
모든 테넌트가 동일한 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" }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" }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" }응답의 environment 필드를 보면 각 티어의 배포 전략이 그대로 찍히는 것을 볼 수 있다.
| 테넌트 | 티어 | Producer 환경 | Consumer 환경 |
|---|---|---|---|
| tenant-1 | Premium | tenant-1 (전용) | tenant-1 (전용) |
| tenant-2 | Basic | pool-1 (공유) | pool-1 (공유) |
| tenant-3 | Advanced | pool-1 (공유) | tenant-3 (전용) |
Advanced 티어가 producer 는 공유 풀 (pool-1) 에서, consumer 는 전용 (tenant-3) 에서 응답 하는 것을 볼 수 있다. 처음에 티어 템플릿에서 producer.enabled: false, consumer.enabled: true 로 설정 했던 부분이 실제 응답으로 그대로 드러나는 셈이다.
이번엔 Producer 에 POST 를 보내고, Consumer 가 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": "2024-07-12T16:31:39+0000" }
}
],
"Count": 1
}producer_environment: pool-1 과 consumer_environment: tenant-3 이 같이 기록 된 것을 볼 수 있다. Advanced 티어의 혼합 모델이 데이터 레벨에서도 그대로 동작 한다는 걸 확인 할 수 있다.
온보딩과 동일한 이벤트 드리븐 방식으로 오프보딩도 처리 된다. 실습 2 에서 수동으로 만들었던 tenant-t1d6c 를 오프보딩 해 본다.
오프보딩 SQS 큐로 메시지를 보낸다.
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"
}'오프보딩 워크플로우가 생성 되었는지 확인 해 본다.
kubectl -n argo-workflows get workflow[ec2-user@ip-10-0-1-93 gitops-gitea-repo]$ kubectl -n argo-workflows get workflow
NAME STATUS AGE MESSAGE
tenant-offboarding-x7wr6 Succeeded 6m47s
tenant-onboarding-vxcn2 Succeeded 16m 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온보딩과 정확히 대칭으로 — SQS 메시지 한 건이면 테넌트 관련 자원 (HelmRelease, Terraform CRD, Git 파일) 이 모두 정리 되는 흐름이다.
오늘 실습을 통해 다음 흐름을 한 번에 확인 해 볼 수 있었다.
tenantID 헤더 기반 라우팅과 DynamoDB 까지의 데이터 흐름이 티어 별 배포 전략 그대로 응답에 찍히는 것직접 실습 하면서 가장 와닿았던 부분은 kustomization.yaml 의 indentation 한 칸 차이로 적용이 안 되었던 부분이었다. GitOps 는 결국 Git 에 들어간 그 값이 그대로 클러스터에 반영 되기 때문에, 코드 리뷰와 lint 가 일반 PR 보다 더 중요 해 진다는 걸 다시 한 번 느꼈다.
그리고 Advanced 티어의 응답에서 producer_environment 와 consumer_environment 가 서로 다른 값으로 찍히는 걸 직접 보니까, 티어 템플릿의 enabled: true/false 한 줄이 결국 SaaS 의 비용 구조와 격리 수준을 결정 한다는 게 좀 더 와 닿았다.
다음에는 이 구조를 홈랩의 RKE2 클러스터에도 비슷하게 적용 해 보고, ECR 대신 Harbor 를 사용 했을 때 어떤 차이가 있는 지 정리 해 볼 예정이다.