CloudNet@ Team Study/EKS Workshop 4th Cohort

Automated tenant onboarding in a SaaS multi-tenant EKS environment

"Everything about infra" 2026. 4. 29. 02:26
포스팅은 CloudNet@팀 서종호(Gasida)님이 진행하시는
AWS EKS Workshop Study 내용을 참고하여 작성합니다.

 

이번 챕터에서는 워크숍에서 제공해준 GitOps 인프라 중

실습 3번 항목인 테넌트 온보딩/오프로딩에 대해 학습해보도록 하겠습니다.

 

 

 

이번 실습에서는 Argo Workflows를 활용하여 위 과정을 완전히 자동화하는 방법을 살펴볼 예정입니다.

자동화의 핵심은 SQS 메시지 하나로 전체 온보딩 파이프라인이 시작되는 Event-Driven 구조입니다.

 

먼저 Lab을 실습해보기에 앞서, 위 전체 구조를 이해하고 넘어가고자 합니다.

  • 개발자가 마이크로서비스 소스를 커밋 → CI → Container Image 빌드
  • 각 마이크로서비스는 Git repo에 Helm Chart로 패키징되어 관리됨 (버전: 0.0.1)
  • AWS Cloud 안에 Infrastructure resources + Amazon EKS Cluster가 존재

💡 Argo Workflows 역할

Lab 실습 첫번째에서 수동으로 진행한 템플릿 복사 → 변수 치환 → Git commit → git push 과정을 자동화하는 것이 전부임.
Git 변경이 발생하면 그 이후는 여전히 Flux v2가 담당하여 EKS에 리소스를 배포합니다.

 

01. Verify Argo Workflows configuration

세 가지 워크플로우 템플릿이 사전 구성되어 있습니다.

템플릿 역할은 아래와 같습니다.

워크플로우 템플릿 역할
onboarding 새 테넌트를 환경에 프로비저닝
offboarding 테넌트를 환경에서 제거
deployment 테넌트 HelmRealse 버전 업데이트

 

전체 워크플로우 정의 파일은 다음 경로에서 확인이 가능합니다.

tree /home/ec2-user/environment/gitops-gitea-repo/control-plane/production/workflows/

/home/ec2-user/environment/gitops-gitea-repo/control-plane/production/workflows/
├── event-bus.yaml
├── kustomization.yaml
├── tenant-deployment-sensor.yaml
├── tenant-deployment-workflow-template.yaml
├── tenant-offboarding-sensor.yaml
├── tenant-offboarding-workflow-template.yaml
├── tenant-onboarding-sensor.yaml
└── tenant-onboarding-workflow-template.yaml

 

Check onboarding template content.

tenant-onboarding-sensor.yaml             ← EventSource + Sensor (이벤트 수신 & 트리거)
tenant-onboarding-workflow-template.yaml  ← WorkflowTemplate (실제 실행 로직)

Sensor가 Workflow를 호출하는 구조입니다. 역할이 명확히 분리되어 있어요. (아래 전체 yaml 코드)

더보기

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-onboarding-sensor.yaml 
---
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: aws-sqs-onboarding
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  sqs:
    tenant-provisioning:
      jsonBody: true
      region: "${aws_region}"
      queue: "argoworkflows-onboarding-queue"
      waitTimeSeconds: 20
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: aws-sqs-onboarding
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  dependencies:
    - name: tenant-provisioning-dep
      eventSourceName: aws-sqs-onboarding
      eventName: tenant-provisioning
  triggers:
    - template:
        name: tenant-onboarding-template
        k8s:
          operation: create
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: tenant-onboarding-
                namespace: argo-workflows
              spec:
                serviceAccountName: argoworkflows-sa
                entrypoint: tenant-provisioning
                synchronization:
                  mutex:
                    name: workflow
                arguments:
                  parameters:
                    - name: TENANT_ID
                      value: "" # ID of your tenant, use this patter eg. tenant-xx (tenant-10, tenant-11)
                    - name: TENANT_TIER
                      value: "" # Valid values are: silo, pool, hybrid
                    - name: RELEASE_VERSION
                      value: "" # I.E. 0.0 or 1.0
                    - name: REPO_URL
                      value: "http://${gitea_url}:3000/admin/eks-saas-gitops.git"
                    - name: GIT_USER_EMAIL
                      value: "admin@example.com"
                    - name: GIT_USERNAME
                      value: "admin"
                    - name: GIT_TOKEN
                      value: "${gitea_token}"
                    - name: GIT_BRANCH
                      value: "main" # Can change based on your configs
                templates:
                  - name: tenant-provisioning
                    steps:
                      - - name: clone-repository
                          templateRef:
                            name: tenant-onboarding-template
                            template: clone-repository
                      - - name: validate-if-tenant-exists
                          templateRef:
                            name: tenant-onboarding-template
                            template: validate-if-tenant-exists
                      - - name: create-tenant-helm-release
                          templateRef:
                            name: tenant-onboarding-template
                            template: create-tenant-helm-release
                volumeClaimTemplates:
                  - metadata:
                      name: workdir
                    spec:
                      storageClassName: gp2
                      accessModes: [ "ReadWriteOnce" ]
                      resources:
                        requests:
                          storage: 1Gi
          parameters:
            - src:
                dependencyName: tenant-provisioning-dep
                dataKey: body.tenant_id
              dest: spec.arguments.parameters.0.value
            - src:
                dependencyName: tenant-provisioning-dep
                dataKey: body.tenant_tier
              dest: spec.arguments.parameters.1.value
            - src:
                dependencyName: tenant-provisioning-dep
                dataKey: body.release_version
              dest: spec.arguments.parameters.2.value

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-onboarding-workflow-template.yaml 
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: tenant-onboarding-template
  namespace: argo-workflows
spec:
  serviceAccountName: argoworkflows-sa # SA with IRSA permissions
  templates:
    - name: validate-if-tenant-exists
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./00-validate-tenant.sh {{workflow.parameters.TENANT_ID}}']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
    - name: clone-repository
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./01-tenant-clone-repo.sh {{workflow.parameters.REPO_URL}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_TOKEN}} && cp -r eks-saas-gitops /mnt/vol/eks-saas-gitops']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
    - name: create-tenant-helm-release
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./02-tenant-onboarding.sh {{workflow.parameters.TENANT_ID}} {{workflow.parameters.RELEASE_VERSION}} {{workflow.parameters.TENANT_TIER}} {{workflow.parameters.GIT_USER_EMAIL}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_TOKEN}}']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
  volumeClaimTemplates:
  - metadata:
      name: workdir
    spec:
      storageClassName: gp2
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

1) EventSource + Sensor 흐름

SQS (argoworkflows-onboarding-queue)
        │  longpolling (waitTimeSeconds: 20)
        ▼
EventSource: aws-sqs-onboarding
        │  body.tenant_id / tenant_tier / release_version 추출
        ▼
Sensor: aws-sqs-onboarding
        │  Workflow 오브젝트 동적 생성 (generateName: tenant-onboarding-)
        ▼
Workflow 실행 (argo-workflows 네임스페이스)

SQS 메시지에서 파라미터 3개를 추출해서 Workflow arguments에 주입하는 게 핵심입니다.

parameters:
  - src:
      dataKey: body.tenant_id       → TENANT_ID
  - src:
      dataKey: body.tenant_tier     → TENANT_TIER  
  - src:
      dataKey: body.release_version → RELEASE_VERSION

나머지 REPO_URL, GIT_USERNAME, GIT_TOKEN 등은 Sensor에 하드코딩된 기본값.

 

2) WorkflowTemplate 3단계

Step 1: clone-repository
        └── 01-tenant-clone-repo.sh
            └── Git clone → /mnt/vol/eks-saas-gitops 복사

Step 2: validate-if-tenant-exists
        └── 00-validate-tenant.sh
            └── 이미 존재하는 테넌트면 중단

Step 3: create-tenant-helm-release
        └── 02-tenant-onboarding.sh
            └── HelmRelease 파일 생성 → Git commit & push

Point 1. 공유 볼륨 (workdir)으로 step 간 데이터 전달

각 step은 별도 컨테이너에서 실행되는데, /mnt/vol을 PVC로 공유해서 clone한 repo를 다음 step에서 그대로 사용

volumeClaimTemplates:
  - metadata:
      name: workdir
    spec:
      storageClassName: gp2
      accessModes: [ "ReadWriteOnce" ]
      storage: 1Gi

Point 2. synchronization.mutex로 동시 실행 방지를 합니다.

synchronization:
  mutex:
    name: workflow

여러 SQS 메시지가 동시에 들어와도 Workflow가 순차 실행되도록 뮤텍스로 직렬화 합니다.

 

Check deployment template content.

온보딩은 tenant_id + tenant_tier + release_version 3개였는데, 디플로이먼트는

{
  "tenant_tier": "basic",
  "release_version": "1.0"
}

tenant_id가 없습니다. 특정 테넌트 하나가 아니라 tier 전체를 한 번에 업데이트하는 구조에요.

더보기

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-deployment-sensor.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: aws-sqs-deployment
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  sqs:
    tenant-deployment:
      jsonBody: true
      region: "${aws_region}"
      queue: "argoworkflows-deployment-queue" # Static value defined in TF module
      waitTimeSeconds: 20
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: aws-sqs-deployment
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  dependencies:
    - name: tenant-deployment-dep
      eventSourceName: aws-sqs-deployment
      eventName: tenant-deployment
  triggers:
    - template:
        name: tenant-deployment-template
        k8s:
          operation: create
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: tenant-deployment-
                namespace: argo-workflows
              spec:
                serviceAccountName: argoworkflows-sa
                entrypoint: tenant-deployment
                synchronization:
                  mutex:
                    name: workflow
                arguments:
                  parameters:
                    - name: TENANT_TIER
                      value: "" # Valid values are: silo, pool, hybrid
                    - name: RELEASE_VERSION
                      value: "" # Valid values are: silo, pool, hybrid
                    - name: REPO_URL
                      value: "http://${gitea_url}:3000/admin/eks-saas-gitops.git"
                    - name: GIT_USER_EMAIL
                      value: "admin@example.com"
                    - name: GIT_USERNAME
                      value: "admin"
                    - name: GIT_TOKEN
                      value: "${gitea_token}"
                    - name: GIT_BRANCH
                      value: "main" # Can change based on your configs
                templates:
                  - name: tenant-deployment
                    steps:
                      - - name: clone-repository
                          templateRef:
                            name: tenant-deployment-template
                            template: clone-repository
                      - - name: update-tenant-helm-release
                          templateRef:
                            name: tenant-deployment-template
                            template: update-tenant-helm-release
                volumeClaimTemplates:
                  - metadata:
                      name: workdir
                    spec:
                      storageClassName: gp2
                      accessModes: [ "ReadWriteOnce" ]
                      resources:
                        requests:
                          storage: 1Gi
          parameters:
            - src:
                dependencyName: tenant-deployment-dep
                dataKey: body.tenant_tier
              dest: spec.arguments.parameters.0.value
            - src:
                dependencyName: tenant-deployment-dep
                dataKey: body.release_version
              dest: spec.arguments.parameters.1.value

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-deployment-workflow-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: tenant-deployment-template
  namespace: argo-workflows
spec:
  serviceAccountName: argoworkflows-sa # SA with IRSA permissions
  templates:
    - name: clone-repository
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./01-tenant-clone-repo.sh {{workflow.parameters.REPO_URL}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_TOKEN}} && cp -r eks-saas-gitops /mnt/vol/eks-saas-gitops'] 
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
    - name: update-tenant-helm-release
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./03-tenant-deployment.sh {{workflow.parameters.RELEASE_VERSION}} {{workflow.parameters.TENANT_TIER}} {{workflow.parameters.GIT_USER_EMAIL}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_TOKEN}}'] 
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
  volumeClaimTemplates:
  - metadata:
      name: workdir
    spec:
      storageClassName: gp2
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

1) Workflow 2단계

Step 1: clone-repository
        └── 01-tenant-clone-repo.sh (온보딩이랑 동일한 스크립트 재사용)
            └── Git clone → /mnt/vol 복사

Step 2: update-tenant-helm-release
        └── 03-tenant-deployment.sh
            └── 해당 tier의 모든 HelmRelease 버전 업데이트 → Git commit & push

온보딩의 02-tenant-onboarding.sh는 새 파일 생성이었다면,

03-tenant-deployment.sh는 기존 파일의 버전 값 수정이 목적입니다.

 

2) validate step이 없는 이유

온보딩은 중복 생성 방지가 필요해서 00-validate-tenant.sh로 이미 존재하는지 체크했는데,

디플로이먼트는 이미 존재하는 테넌트를 업데이트하는 거니까 체크 자체가 불필요 합니다.

 

Check offloading templates content.

SQS 구조를 보면 아래와 같아요.

{
  "tenant_id": "tenant-1",
  "tenant_tier": "basic"
}

release_version 없네요. 삭제할 때 버전은 무관하니까요 ! (아래 전체 yaml 코드)

더보기

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-offboarding-sensor.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: aws-sqs-offboarding
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  sqs:
    tenant-provisioning:
      jsonBody: true
      region: "${aws_region}"
      queue: "argoworkflows-offboarding-queue"
      waitTimeSeconds: 20
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: aws-sqs-offboarding
  namespace: argo-events
spec:
  template:
    serviceAccountName: argo-events-sa
  dependencies:
    - name: tenant-provisioning-dep
      eventSourceName: aws-sqs-offboarding
      eventName: tenant-provisioning
  triggers:
    - template:
        name: tenant-offboarding-template
        k8s:
          operation: create
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: tenant-offboarding-
                namespace: argo-workflows
              spec:
                serviceAccountName: argoworkflows-sa
                entrypoint: tenant-provisioning
                synchronization:
                  mutex:
                    name: workflow
                arguments:
                  parameters:
                    - name: TENANT_ID
                      value: "" # ID of your tenant, use this patter eg. tenant-xx (tenant-10, tenant-11)
                    - name: TENANT_TIER
                      value: "" # Valid values are: premium, advanced, basic
                    - name: REPO_URL
                      value: "http://${gitea_url}:3000/admin/eks-saas-gitops.git"
                    - name: GIT_USER_EMAIL
                      value: "admin@example.com"
                    - name: GIT_USERNAME
                      value: "admin"
                    - name: GIT_TOKEN
                      value: "${gitea_token}"
                    - name: GIT_BRANCH
                      value: "main" # Can change based on your configs
                templates:
                  - name: tenant-provisioning
                    steps:
                      - - name: clone-repository
                          templateRef:
                            name: tenant-offboarding-template
                            template: clone-repository
                      - - name: validate-if-tenant-exists
                          templateRef:
                            name: tenant-offboarding-template
                            template: validate-if-tenant-exists
                      - - name: remove-tenant-helm-release
                          templateRef:
                            name: tenant-offboarding-template
                            template: remove-tenant-helm-release
                volumeClaimTemplates:
                  - metadata:
                      name: workdir
                    spec:
                      storageClassName: gp2
                      accessModes: [ "ReadWriteOnce" ]
                      resources:
                        requests:
                          storage: 1Gi
          parameters:
            - src:
                dependencyName: tenant-provisioning-dep
                dataKey: body.tenant_id
              dest: spec.arguments.parameters.0.value
            - src:
                dependencyName: tenant-provisioning-dep
                dataKey: body.tenant_tier
              dest: spec.arguments.parameters.1.value

[ec2-user@ip-10-0-1-207 workflows]$ cat tenant-offboarding-workflow-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: tenant-offboarding-template
  namespace: argo-workflows
spec:
  serviceAccountName: argoworkflows-sa # SA with IRSA permissions
  templates:
    - name: validate-if-tenant-exists
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./00-validate-tenant.sh {{workflow.parameters.TENANT_ID}}']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
    - name: clone-repository
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./01-tenant-clone-repo.sh {{workflow.parameters.REPO_URL}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_TOKEN}} && cp -r eks-saas-gitops /mnt/vol/eks-saas-gitops']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
    - name: remove-tenant-helm-release
      container:
        image: "${ecr_argoworkflow_container}:0.1"
        command: ["/bin/sh","-c"]
        args: ['./04-tenant-offboarding.sh {{workflow.parameters.TENANT_ID}} {{workflow.parameters.TENANT_TIER}} {{workflow.parameters.GIT_USER_EMAIL}} {{workflow.parameters.GIT_USERNAME}} {{workflow.parameters.GIT_BRANCH}} {{workflow.parameters.GIT_TOKEN}}']
        volumeMounts:
        - name: workdir
          mountPath: /mnt/vol
        env:
          - name: GIT_USERNAME
            value: "{{workflow.parameters.GIT_USERNAME}}"
          - name: GIT_TOKEN
            value: "{{workflow.parameters.GIT_TOKEN}}"
  volumeClaimTemplates:
  - metadata:
      name: workdir
    spec:
      storageClassName: gp2
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

1) Workflow 3단계

Step 1: clone-repository
        └── 01-tenant-clone-repo.sh (3개 워크플로우 모두 동일하게 재사용)

Step 2: validate-if-tenant-exists
        └── 00-validate-tenant.sh (온보딩이랑 동일한 스크립트)
            └── 존재하지 않는 테넌트면 중단

Step 3: remove-tenant-helm-release
        └── 04-tenant-offboarding.sh
            └── HelmRelease 파일 삭제 → Git commit & push

흥미로운 포인트는 00-validate-tenant.sh 입니다 (온보딩이랑 동일한 스크립트임)

오프보딩의 validate 스크립트는 온보딩과 목적이 반대인데요,

 

온보딩 validate script : 이미 존재하면 → 중단 (중복 방지)

오프보딩 validate script : 존재하지 않으면 → 중단 (없는 테넌트 삭제 방지)

 

세 워크플로우를 최종적으로 비교해보도록 하겠습니다.

  온보딩 디플로이먼트 오프보딩
SQS 파라미터 tenant_id + tenant_tier +
release_version
tenant_tier + release_version tenant_id + tenant_tier
validate step 중복 방지 존재 확인
대상 범위 테넌트 단위 tier 전체 테넌트 단위
Git 조작 파일 생성 파일 수정 파일 삭제
스크립트 02-onboarding.sh 03-deployment.sh 04-offboarding.sh
공통 스크립트 01-clone-repo.sh 01-clone-repo.sh 01-clone-repo.sh

 


 

02. The entire flow of the onboarding workflow

위에서 설명드린 온보딩 워크플로우 템플릿을 참고하시면 되고, 전체 흐름을 설명드리겠습니다.

Step1. SQS 메시지 발행

{
  "tenant_id": "tenant-1",
  "tenant_tier": "basic",
  "release_version": "0.0"
}

누군가(관리자/SaaS 컨트롤 플레인)가 이 메시지를 argoworkflows-onboarding-queue에 던지는 것에서 시작.

 

Step2. Argo Events가 수신 (EventSource → Sensor)

EventSource (aws-sqs-onboarding)
    └── SQS를 20초 간격으로 longpolling
    └── 메시지 수신 시 Sensor에 전달

Sensor (aws-sqs-onboarding)
    └── body.tenant_id     → TENANT_ID 파라미터로 주입
    └── body.tenant_tier   → TENANT_TIER 파라미터로 주입
    └── body.release_version → RELEASE_VERSION 파라미터로 주입
    └── Workflow 오브젝트 동적 생성 (generateName: tenant-onboarding-xxxx)

 

Step3. Argo Workflows 실행 (3단계)

Step 1: clone-repository
    └── eks-saas-gitops 레포 클론
    └── /mnt/vol (PVC) 에 복사
    └── 다음 step과 공유하기 위해 PVC에 저장

        ↓

Step 2: validate-if-tenant-exists
    └── /mnt/vol 안에서 tenant-1 관련 파일 존재 여부 확인
    └── 이미 존재하면 → 워크플로우 중단 (중복 방지)
    └── 없으면 → 다음 step 진행

        ↓

Step 3: create-tenant-helm-release
    └── TENANT_TIER (basic) 에 맞는 템플릿 선택
    └── TENANT_ID, RELEASE_VERSION 변수 치환하여 HelmRelease 파일 생성
    └── Git commit & push → eks-saas-gitops 레포 반영

 

Step4. Flux v2가 변경 감지

eks-saas-gitops 레포 변경 감지
    └── 새로 생긴 HelmRelease 파일 확인
    └── EKS 클러스터에 테넌트 리소스 배포

 

📌 핵심 설계 포인트

 

1) 이벤트 드리븐: SQS 메시지 하나로 전체 파이프라인이 자동으로 시작됨. 사람이 개입할 지점이 없음.

2) GitOps 경계: Argo Workflows의 역할은 Git 조작까지만. EKS 배포는 Flux가 담당. 둘의 역할이 명확히 분리

3) PVC 공유: 각 step은 독립된 컨테이너지만 /mnt/vol PVC를 통해 clone한 레포를 공유.

4) mutex: 동시에 여러 SQS 메시지가 와도 워크플로우가 순차 실행됨. Git 충돌 방지.

 


 

03. Premium Tier Tenant Onboarding

이제, Premium 테넌트 프로비저닝을 해봅시다.

 

새 테넌트를 온보딩하려면 필요한 메타데이터를 담은 메시지를 SQS 큐로 전송을 해야겠죠 ?

(여기서 필요한 메타데이터는 tenant_id, tenant_tier, release_version)

tenant_tier: premium 은 테넌트 전용 Kubernetes 네임스페이스에 Producer, Consumer 마이크로서비스와
인프라 리소스(SQS, DynamoDB)가 모두 전용으로 프로비저닝됨을 의미함.
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"
    }'

 

SQS 큐로 전송하였다면, 온보딩 워크플로우 생성 여부를 확인해봅시다.

 

이때 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로 접근하시어, 확인하시면 되겠습니다.

온보딩 템플릿을 실행하여 Argo Workflows 실행 (3단계) 과정을 거친 것을 확인 할 수 있습니다.

 

Verify GitOps changes

워크플로우가 완료되면 Gitea 저장소에 새 Premium 테넌트의 HelmRealse 파일이 자동으로

커밋되어 있음을 확인할 수 있습니다.

 

Gitea Web UI 접속 정보

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 Web Interface Access ==="
echo "Public URL (for browser access): $GITEA_PUBLIC_IP"
echo "Username: admin"
echo "Password: $GITEA_ADMIN_PASSWORD"
echo "=================================="

이 Gitea 확인 과정이 실제 Argo Workflows가 Git Push를 잘 했는지 검증하는 과정입니다.

Helm Release 파일이 자동으로 커밋이 되었음을 확인하였습니다.

이 시점부터 Flux v2가 변경 사항을 감지하여, 테넌트를 플랫폼에 추가할 것입니다.

 

위와 같이 premium 테넌트를 플랫폼에 추가하였고, 실제로 producer, consumer 파드가 동작합니다.

 


 

04. Basic Tier Tenant Onboarding

Basic 티어는 Lab 실습 두번째에서 살펴봤듯이 공유 인프라를 사용합니다.

basic_env_template.yaml 기반의 Pool 환경이 여러 Basic 테넌트를 수용하며,

리소스 공유를 통해 비용을 최적화 하는 구조입니다.

 

tenant_tier: basic으로 변경하여 SQS 메시지 전송

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"
    }'

메시지를 전송하면 아래와 같이 응답을 반환받습니다.

  • MD5OfMessageBody: 메시지 본문 무결성 체크값. 전송 중 데이터가 변조되지 않았는지 확인용.
  • MessageId: SQS가 이 메시지에 부여한 고유 ID.

둘 다 SQS가 "메시지 잘 받았어"라고 돌려주는 확인 응답입니다.

이게 나오면 전송 성공한 거고, 이제 Argo Events Sensor가 polling해서 가져갈 차례.

 

Verify GitOps changes

tenant-onboarding-n46wb 워크플로우가 SQS 메시지 수신 후 자동 트리거되었고

3단계(clone → validate → create-helm-release) 모두 Succeeded 입니다.

 

여기서 흥미로운 점은 DURATION 전체 약 13초인데요, premium 테넌트 보다 훨씬 빠릅니다

왜? basic 테넌트가 더 단순하기 때문 (공유 구조)

워크플로우 자체는 tier를 모릅니다. TENANT_TIER 파라미터를 스크립트에 넘길 뿐이고,
어떤 리소스가 만들어지는지는 티어 템플릿이 결정합니다.

이게 이번 실습의 핵심 설계 포인트 입니다.

Argo Workflows는 Git 조작 자동화 엔진일 뿐이고, 비즈니스 로직(티어별 리소스 구성)은 템플릿에 캡슐화됨

 

premium 템플릿과 비교해보면 kind: 부분이 premium은 namespace 였는데,

basic은 HelmRelease인 것을 확인하실 수 있습니다. 추가적으로 targetNamespace도 pool-1 이네요.

 

즉, 이미 존재하는 공유 네임스페이스(pool-1)에 배포가 되었다는 것을 입증하는 것입니다.

 


05. Advanced Tier Tenant Onboarding

Advanced 티어는 Lab 실습 두번째에서 정의한 혼합 모델(Producer 공유 + Consumer 전용)을 의미.

컴플라이언스 요구나 Noisy Neighbor 문제 완화가 필요한 고객군에 적합한 구조입니다.

 

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"
    }'

 

Verify GitOps changes

워크플로우 완료 후 Gitea 저장소에서 tenant-3에 대해 생성된 파일을 확인해봅시다.

 

이 변경된 yaml 템플릿을 보면 Advanced 티어의 특성을 확인할 수 있습니다.

producer:
  enabled: false  # Pool deployment -- advanced tier shares resources with other tenants
  envId: pool-1   # producer는 pool-1 공유

consumer:
  enabled: true   # Silo deployment -- advanced tier has a dedicated deployment for each tenant
  # consumer는 tenant-3 전용 네임스페이스에 배포

 

이제 flux v2가 배포한 플랫폼을 확인해볼까요?

tenant-3 네임스페이스의 파드들은 consumer만 전용으로 생성된 것을 확인할 수 있고,

producer는 공유 pool-1을 사용하는 것까지 확인하였습니다.

 


 

06. Check resources

Step1. Git 저장소 상태 확인

Argo Workflows가 자동 커밋한 내용을 로컬로 pull해서 테넌트 파일 구조 확인.

우리가 Gitea UI에서 봤던 커밋들이 로컬에도 반영되는지 확인 해보겠습니다.

Argo Workflows가 Gitea에 자동 커밋한 내용 3개를 로컬로 내려받았습니다.

tenant-1.yaml  +37줄  ← premium 온보딩
tenant-2.yaml  +30줄  ← basic 온보딩  
tenant-3.yaml  +35줄  ← advanced 온보딩

각 tier의 kustomization.yaml도 같이 업데이트됐고, pool-1.yaml도 변경됐네요.

(advanced producer가 pool-1 공유하면서 생긴 변경).

요약하면 우리가 SQS로 트리거한 3번의 온보딩이 전부 Git에 반영된 걸 로컬에서 확인한 것입니다.

 

각 tier 디렉토리마다 kustomization.yaml이 있어서 Flux가 해당 디렉토리 안의 리소스를

자동으로 감지하는 구조입니다. 새 테넌트가 추가될 때마다 kustomization.yaml에 항목이 추가되고

yaml 파일이 생기는 패턴이에요.

 

Step2. Flux HelmRelease 확인

flux get helmreleases

NAME                            REVISION        SUSPENDED       READY   MESSAGE                                                                                                                     
argo-events                     2.4.3           False           True    Helm install succeeded for release argo-events/argo-events.v1 with chart argo-events@2.4.3                                 
argo-workflows                  0.40.11         False           True    Helm install succeeded for release argo-workflows/argo-workflows.v1 with chart argo-workflows@0.40.11                      
aws-load-balancer-controller    1.6.2           False           True    Helm install succeeded for release aws-system/aws-load-balancer-controller.v1 with chart aws-load-balancer-controller@1.6.2
karpenter                       1.4.0           False           True    Helm install succeeded for release karpenter/karpenter.v1 with chart karpenter@1.4.0                                       
kubecost                        2.1.0           False           True    Helm install succeeded for release kubecost/kubecost.v1 with chart cost-analyzer@2.1.0                                     
metrics-server                  3.11.0          False           True    Helm install succeeded for release kube-system/metrics-server.v1 with chart metrics-server@3.11.0                          
onboarding-service              0.0.1           False           True    Helm install succeeded for release onboarding-service/onboarding-service.v1 with chart application-chart@0.0.1             
pool-1                          0.0.1           False           True    Helm install succeeded for release pool-1/pool-1 with chart helm-tenant-chart@0.0.1             
tenant-1-premium                0.0.1           False           True    Helm upgrade succeeded for release tenant-1/tenant-1-premium.v2 with chart helm-tenant-chart@0.0.1                         
tenant-2-basic                  0.0.1           False           True    Helm install succeeded for release pool-1/tenant-2-basic.v1 with chart helm-tenant-chart@0.0.1                             
tenant-3-advanced               0.0.1           False           True    Helm upgrade succeeded for release tenant-3/tenant-3-advanced.v2 with chart helm-tenant-chart@0.0.1                        
tf-controller                   0.16.0-rc.4     False           True    Helm install succeeded for release flux-system/tf-controller.v1 with chart tf-controller@0.16.0-rc.4

3개 테넌트가 모두 READY=True 상태인지 확인하였습니다.

추가적으로 모든 테넌트가 동일한 helm-tenant-chart를 사용하며 버전 0.0.1로 배포되었음을 확인.

 

💡 핵심 : 모든 티어가 동일한 차트(helm-tenant-chart)를 사용하면서도, values 설정만으로

완전히 다른 배포 구조를 갖게 되었습니다.

 

Step3. 테넌트 배포 검증

모든 테넌트에 동일한 ALB를 사용하며, tenantID 헤더로 테넌트별 환경으로 라우팅됩니다.

export APP_LB=http://$(kubectl get ingress -n tenant-1 -o json | jq -r .items[0].status.loadBalancer.ingress[0].hostname)

curl -s -H "tenantID: tenant-1" $APP_LB/producer | jq
{
  "environment": "tenant-1",
  "microservice": "producer",
  "tenant_id": "tenant-1",
  "version": "0.0.1"
}

curl -s -H "tenantID: tenant-1" $APP_LB/consumer | jq
{
  "environment": "tenant-1",
  "microservice": "consumer",
  "tenant_id": "tenant-1",
  "version": "0.0.1"
}

curl -s -H "tenantID: tenant-2" $APP_LB/producer | jq
{
  "environment": "pool-1",
  "microservice": "producer",
  "tenant_id": "tenant-2",
  "version": "0.0.1"
}

curl -s -H "tenantID: tenant-2" $APP_LB/consumer | jq
{
  "environment": "pool-1",
  "microservice": "consumer",
  "tenant_id": "tenant-2",
  "version": "0.0.1"
}

curl -s -H "tenantID: tenant-3" $APP_LB/producer | jq
{
  "environment": "pool-1",
  "microservice": "producer",
  "tenant_id": "tenant-3",
  "version": "0.0.1"
}

curl -s -H "tenantID: tenant-3" $APP_LB/consumer | jq
{
  "environment": "tenant-3",
  "microservice": "consumer",
  "tenant_id": "tenant-3",
  "version": "0.0.1"
}
  • tenant-1 (premium): producer/consumer 모두 tenant-1 → 전용
  • tenant-2 (basic): producer/consumer 모두 pool-1 → 공유
  • tenant-3 (advanced): producer는 pool-1 (공유), consumer는 tenant-3 (전용) → hybrid

의도된 대로 마이크로서비스 엔드포인트들이 정상 동작 하는것을 확인하였습니다.

 

Step4. 인프라 End-to-End 검증

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

 

consumer, producer environment와 message_id, tenant_id, timestamp 값이

정상적으로 반환되었는지 확인하시면 되겠습니다.

 


 

07. Tenant Offboarding

온보딩과 동일한 Event-Driven 방식으로 오프로딩도 처리됩니다.

오늘 Lab 실습으로 진행한 premium 테넌트를 오프보딩 해보겠습니다.

 

오프로딩 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-1",
        "tenant_tier": "premium"
    }'

 

오프로딩 워크플로우 생성 여부를 확인하였고, 현재 Running 상태로 프로세스가 돌고 있습니다.

kubectl -n argo-workflows get workflow

NAME                       STATUS      AGE     MESSAGE
tenant-offboarding-67zwc   Running     9s      
tenant-onboarding-7mztv    Succeeded   49m     
tenant-onboarding-n46wb    Succeeded   63m     
tenant-onboarding-tnnnp    Succeeded   86m

 

Argo Workflows에서 오프로딩 진행 상황을 모니터링이 가능합니다.

 

Gitea 저장소에서 테넌트 리소스가 제거된 커밋을 확인하실 수 있습니다.

destroyResourcesOnDeletion: true 설정에 의해 Terraform CRD와 함께 AWS 리소스도 자동 정리됨.

 


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

 

이번 실습에서는 Event-Driven 자동화를 통해 멀티테넌트 SaaS 환경의 온보딩/오프보딩이

완전 자동화되는 과정을 확인해보았습니다.