[CloudNeta] EKS 워크샵 스터디 (6) - GitOps와 SaaS 플랫폼 엔지니어링 Part 1 - 개념과 인프라 설계

이번 게시글에서는 EKS 워크샵 스터디 제 6주차 내용을 작성합니다.

이 글은 2부로 나누어집니다.

  1. 6주차 - GitOps와 SaaS 플랫폼 엔지니어링 Part 1 - 개념과 인프라 설계 (현재 보고 계신 글)
  2. 6주차 - GitOps와 SaaS 플랫폼 엔지니어링 Part 2 - SaaS 티어 전략과 테넌트 자동화

들어가며

이번 게시글에서는 아래 내용을 설명합니다:

이번 실습 환경은 아래와 같은 구성을 가집니다.

001-overview

GitOps와 SaaS 플랫폼 엔지니어링에 대해

GitOps와 플랫폼 엔지니어링을 설명하기에 앞서 발전 흐름을 한번 살펴보고 넘어갑시다.

DevOps 의 대두

먼저 과거에 회사에서 서버를 운영하던 모습을 살펴봅시다. 서버를 구성하기 위해 기본적으로 배선작업을 하고, 랙(Server Rack)이나 캐비넷에 설치하던 시절이 있었습니다[1]. 이러한 시대에는 개발팀(Dev)과 운영팀(Ops)이 분리되어 하드웨어, 소프트웨어 파트를 각각 맡았습니다. 회사의 성장에 따라 이런 조직도 자연스레 커지고 복잡해지며, 개발/운영에 드는 비용이 증대되었죠. 2010년 전후로 클라우드 컴퓨팅의 시대가 도래하며 하드웨어 제품을 대여할 수 있게 되고, Chef, Puppet, Terraform 등과 같은 소프트웨어가 등장하여 이러한 수고를 "모든" 회사가 하지 않아도 되는 시대가 왔습니다. 이러한 시대의 흐름을 타고 개발과 운영이 한데 어우러진, 이른바 "DevOps 운동"이 시작되었습니다.

플랫폼 엔지니어링의 등장

개발과 운영이 점점 더 고도화되고, 다양한 도구들이 등장하니 회사마다의 요구사항을 저마다 풀기 위한 시도가 등장했습니다. 이는 플랫폼 엔지니어링(Platform Engineering)이라는 개념인데요. 이는 클라우드 환경에서 운영부담을 최소화하고 개발에 온전히 집중할 수 있도록 추상화를 제공하는 방법입니다. 회사마다의 접근방식을 설계하고 더 나아가 거버넌스를 세워 마치 회사 내의 제품처럼 관리합니다. 이러면 개발자가 고도화된 인프라, 보안, 그리고 CI/CD까지 관여하는 것이 아니라 온전히 개발에 집중할 수 있는 환경을 갖출 수 있도록 서비스를 구성하는 또 하나의 일이 되는 것이지요. 결국 플랫폼 팀에게는 내부 개발자를 위한 또 하나의 제품을 만드는 일이 됩니다.

많은 논의들이 있지만 이 글에선 당근의 접근 방식, 그리고 무신사의 접근 방식을 통해 배운 점을 간략히 요약해보려 합니다[2].

다음은 당근의 접근 방식입니다. '설정보다 관습(Convention over Configuration)', '의견이 반영된(Opinionated) 단순한 시스템', '문서 없이도 사용 가능한 편의성'이라는 세 가지 핵심 가치를 내세웠습니다. 배포 프로세스의 파편화와 복잡한 YAML 관리, SRE 팀에 반복되는 배포 설정 요청으로 인한 병목 현상이 이들의 문제였으며, 셀프 서비스, 설정 추상화, 소유권 관리, 비용 가시성을 주요 방향성으로 삼아, 기존 ArgoCD UI를 직접 사용하는 방식 대신 쿠버네티스를 몰라도 배포할 수 있도록 설정을 추상화한 새로운 배포 시스템을 구축하고, 개발자가 테스트부터 배포까지 전 과정을 직접 수행하는 셀프 서비스 환경과 프로젝트 소유권 관리 체계를 함께 구현했습니다.

다음은 무신사의 접근 방식입니다. '단순 반복적인 토일(Toil) 업무 제거', '인지 부하 감소를 위한 추상화', '데이터 기반의 비용 가시성 확보'라는 세 가지 핵심 가치를 기준점으로 잡습니다. 인프라 형상의 공존으로 인한 복잡성 증가와, 신규 서비스 구축 시 SRE 팀에 집중되는 병목 현상, 그리고 빈번한 마케팅 이벤트에 따른 수동 증설 부담이 이들의 문제였으며 **셀프 서비스 지향 아키텍처, 소프트웨어 카탈로그(Backstage 기반), 가드레일, 핀옵스(FinOps)**의 4대 방향성을 주요 거버넌스로 두고, '런치패드' 서비스를 통해 최소 정보만으로 인프라를 자동 프로비저닝, 이벤트 달력과 연동해 서버를 자동 증설하는 등의 자체 구현을 시도했습니다. 현재는 Amazon Bedrock을 연동해 장애 영향도 파악 등에 생성형 AI를 실험 중에 있습니다.

이렇듯 회사의 요구사항을 충족하는 것이기 때문에 정답이 없으며, 회사와 각 팀 간의 주요 내용을 잘 조율하기 위한 하나의 방향성과 그 해결방식이라 할 수 있지요.

GitOps란?

GitOps는 인프라 및 애플리케이션 설정을 선언적으로 정의하고, 이를 Git을 통해 관리하는 방식입니다. opengitops.dev에서는 이를 네 가지 원칙으로 정리하고 있습니다.

첫째, **선언적 정의(Declarative)**입니다. 시스템의 상태를 '어떻게(How)' 도달할지가 아니라, '어떤 상태(What)'여야 하는지를 YAML 파일 등으로 선언적으로 기술합니다. 둘째, **버전 관리와 불변성(Versioned and Immutable)**입니다. 모든 선언적 설정은 Git에 저장되어 변경 이력이 남으며, 문제 발생 시 특정 시점으로의 롤백이 용이해집니다. 셋째, **자동 반영(Pulled Automatically)**입니다. Git에 저장된 Desired State는 사람이 직접 명령어를 입력하지 않아도 시스템이 변경 사항을 감지해 스스로 클러스터에 반영합니다. 넷째, **지속적인 조정(Continuously Reconciled)**입니다. 소프트웨어 에이전트가 실제 시스템의 Actual State와 Git에 정의된 Desired State를 지속적으로 비교하며, 둘 사이에 Drift가 발생하면 자동으로 원래 상태로 되돌립니다.

이 네 가지 원칙의 핵심을 한 마디로 표현하면 Auto Reconciliation이라 할 수 있습니다. 누군가 kubectl 명령어로 클러스터 설정을 수동으로 변경하더라도, GitOps 에이전트가 이를 감지하고 Git에 정의된 상태로 자동 복구함으로써 시스템의 일관성을 유지합니다.

SaaS DevOps에 대해

이렇듯 복잡해지는 비즈니스 요구사항에 대응하기 위해서는 SaaS DevOps 환경에서도 GitOps 활용이 더욱 중요해집니다. 빠른 릴리즈와 효율적인 운영이 가능해지고, 새로운 테넌트가 추가될 때마다 온보딩 과정도 한층 수월해집니다.

한편 SaaS 앱은 사일로(Silo), 하이브리드(Hybrid), 풀(Pool) 등 다양한 배포 모델을 사용하는데, 이 때 테넌트 온보딩 과정에서 리소스를 프로비저닝하는데 필요한 요구 사항이 다릅니다.

예시 - GitOps를 활용한 EKS 기반 SaaS 앱의 레퍼런스 아키텍처

아래 아키텍처를 보고 필요한 부분을 설명드리겠습니다.

실습 1. 앱 및 인프라 설계

들어가며

현대 조직에서 팀은 일반적으로 애플리케이션 개발팀과 인프라/운영팀, 두 가지로 나뉩니다. 각 팀의 소유권과 책임은 명확히 구분되어 있으며, 대부분의 애플리케이션은 IAM 역할, SQS 큐, DynamoDB 테이블 같은 특정 인프라 구성 요소를 필요로 합니다. 이 때문에 개발자가 인프라팀에 리소스 생성을 요청하는 상황이 반복적으로 발생합니다. 앞서 당근, 무신사의 예시를 본 것처럼요. 조직 규모가 커지면 이 요청이 걷잡을 수 없이 커지고, 개선할 엄두도 안 나고 문제가 기하급수적으로 커집니다.

이를 해결하기 위해 개발자 주도형 인프라(Developer-driven Infrastructure)라는 패러다임이 나옵니다. 사전에 보안/거버넌스 구성 속에 만들어진 인프라를 개발자가 직접 프로비저닝하고 관리하며, IaC와 자동화 도구를 통해 셀프서비스 구성까지 마치면 주요 테넌트마다 신속하게 사용할 수 있죠.

세션 소개

위와 같은 인프라 구성을 위한 쿠버네티스 인프라 자동화 툴체인을 살펴봅시다.

이번 세션에서는 멀티테넌트 환경에서 팀이 반드시 다뤄야 하는 고유한 복잡성을 해소하는, 완전히 동작하는 EKS 기반 SaaS DevOps 자동화 솔루션을 살펴봅니다. 목표는 Flux v2, Argo Workflows, Helm 및 기타 쿠버네티스 도구를 활용하여 테넌트별 Amazon Elastic Kubernetes Service(EKS) 환경을 프로비저닝하고 구성하는 다양한 티어와 배포 모델을 지원하는 엔드투엔드 솔루션을 구축하는 것이 목표입니다.

개발자 주도 환경

앞서 말한 상황에 대해 인프라 청사진을 구성하고, IaC와 자동화 도구를 통한 셀프 서비스 기능으로 구동되도록 하는 것이 목표입니다.

1-1. 시작하기

이번 워크샵으로 SaaS 멀티테넌트 환경에서의 DevOps 프로세스를 단순화할 수 있는 플랫폼 경험을 제공하는 것을 목표로 합니다. 실습 1의 환경구성은 아래와 같습니다:

먼저, 아래 요소들이 프로비저닝되어 있으니 확인할 예정입니다.

구성 요소 설명
Amazon EKS 클러스터 Flux, Argo Workflows 등 애드온 포함
Amazon ECR 애플리케이션 컨테이너 이미지 및 Helm 차트 저장소
Gitea 저장소 GitOps 릴리스, 앱 템플릿, 마이크로서비스 코드
AWS 리소스 정상 작동을 위한 네트워킹, IAM 등

리소스 살펴보기

$ kubectl get ns
NAME                 STATUS   AGE
argo-events          Active   1d12h
argo-workflows       Active   1d12h
aws-system           Active   1d12h
default              Active   1d12h
flux-system          Active   1d12h
karpenter            Active   1d12h
kube-node-lease      Active   1d12h
kube-public          Active   1d12h
kube-system          Active   1d12h
kubecost             Active   1d12h
onboarding-service   Active   1d12h
pool-1               Active   1d12h

flux 리소스 살펴보기

Flux는 이 솔루션의 핵심 컴포넌트입니다. Flux는 Git과 ECR에 정의된 환경 설정을 감시하며 클러스터에 변경 사항을 배포하는데, 이를 통해 클러스터의 배포 상태가 Git에 선언된 Desired State와 일치하도록 유지하고, 올바른 버전의 Helm 차트가 클러스터에 배포되도록 보장합니다. 본 실습에서는 Flux를 통해 아래 리소스가 배포되어 있음을 확인하실 수 있습니다.

$ flux get all

NAME                    REVISION                SUSPENDED       READY   MESSAGE                                             
ocirepository/capacitor v0.4.8@sha256:1efcb443  False           True    stored artifact for digest 'v0.4.8@sha256:1efcb443'

NAME                            REVISION                        SUSPENDED       READY   MESSAGE                                                      
gitrepository/flux-system       refs/heads/main@sha1:06abc42a   False           True    stored artifact for revision 'refs/heads/main@sha1:06abc42a'
gitrepository/terraform-v0-0-1  v0.0.1@sha1:2d19a84a            False           True    stored artifact for revision 'v0.0.1@sha1:2d19a84a'         

NAME                                    REVISION        SUSPENDED       READY   MESSAGE                                     
helmrepository/argo                     sha256:77d58f2f False           True    stored artifact: revision 'sha256:77d58f2f'
helmrepository/eks-charts               sha256:d5d7cd31 False           True    stored artifact: revision 'sha256:d5d7cd31'
helmrepository/helm-application-chart                   False           True    Helm repository is Ready                   
helmrepository/helm-tenant-chart                        False           True    Helm repository is Ready                   
helmrepository/karpenter                                False           True    Helm repository is Ready                   
helmrepository/kubecost                                 False           True    Helm repository is Ready                   
helmrepository/metrics-server           sha256:ba69c5bb False           True    stored artifact: revision 'sha256:ba69c5bb'
helmrepository/tf-controller            sha256:1fcad0f6 False           True    stored artifact: revision 'sha256:1fcad0f6'

NAME                                                    REVISION        SUSPENDED       READY   MESSAGE                                                          
helmchart/flux-system-argo-events                       2.4.3           False           True    pulled 'argo-events' chart with version '2.4.3'                 
helmchart/flux-system-argo-workflows                    0.40.11         False           True    pulled 'argo-workflows' chart with version '0.40.11'            
helmchart/flux-system-aws-load-balancer-controller      1.6.2           False           True    pulled 'aws-load-balancer-controller' chart with version '1.6.2'
helmchart/flux-system-karpenter                         1.4.0           False           True    pulled 'karpenter' chart with version '1.4.0'                   
helmchart/flux-system-kubecost                          2.1.0           False           True    pulled 'cost-analyzer' chart with version '2.1.0'               
helmchart/flux-system-metrics-server                    3.11.0          False           True    pulled 'metrics-server' chart with version '3.11.0'             
helmchart/flux-system-onboarding-service                0.0.1           False           True    pulled 'application-chart' chart with version '0.0.1'           
helmchart/flux-system-pool-1                            0.0.1           False           True    pulled 'helm-tenant-chart' chart with version '0.0.1'           
helmchart/flux-system-tf-controller                     0.16.0-rc.4     False           True    pulled 'tf-controller' chart with version '0.16.0-rc.4'         

NAME                                            LAST SCAN               SUSPENDED       READY   MESSAGE                                                
imagerepository/consumer-image-repository       2026-04-26T18:21:22Z    False           True    successful scan: found 2 tags with checksum 1935083242
imagerepository/payments-image-repository       2026-04-26T18:21:22Z    False           True    successful scan: found 2 tags with checksum 1935607531
imagerepository/producer-image-repository       2026-04-26T18:21:22Z    False           True    successful scan: found 2 tags with checksum 1935869675

NAME                                    IMAGE                                                   TAG                     READY    MESSAGE                                                                                                     
imagepolicy/consumer-image-policy       002437807755.dkr.ecr.us-west-2.amazonaws.com/consumer   prd-20260424T054925Z    True     Latest image tag for 002437807755.dkr.ecr.us-west-2.amazonaws.com/consumer resolved to prd-20260424T054925Z
imagepolicy/payments-image-policy       002437807755.dkr.ecr.us-west-2.amazonaws.com/payments   prd-20260424T054908Z    True     Latest image tag for 002437807755.dkr.ecr.us-west-2.amazonaws.com/payments resolved to prd-20260424T054908Z
imagepolicy/producer-image-policy       002437807755.dkr.ecr.us-west-2.amazonaws.com/producer   prd-20260424T054944Z    True     Latest image tag for 002437807755.dkr.ecr.us-west-2.amazonaws.com/producer resolved to prd-20260424T054944Z

NAME                                                            LAST RUN                SUSPENDED       READY   MESSAGE               
imageupdateautomation/consumer-update-automation-pooled-envs    2026-04-26T18:19:12Z    False           True    repository up-to-date
imageupdateautomation/consumer-update-automation-tenants        2026-04-26T18:19:12Z    False           True    repository up-to-date
imageupdateautomation/payments-update-automation-pooled-envs    2026-04-26T18:17:24Z    False           True    repository up-to-date
imageupdateautomation/payments-update-automation-tenants        2026-04-26T18:19:12Z    False           True    repository up-to-date
imageupdateautomation/producer-update-automation-pooled-envs    2026-04-26T18:17:24Z    False           True    repository up-to-date
imageupdateautomation/producer-update-automation-tenants        2026-04-26T18:19:12Z    False           True    repository up-to-date

NAME                                            REVISION        SUSPENDED       READY   MESSAGE                                                                                                                     
helmrelease/argo-events                         2.4.3           False           True    Helm install succeeded for release argo-events/argo-events.v1 with chart argo-events@2.4.3                                 
helmrelease/argo-workflows                      0.40.11         False           True    Helm install succeeded for release argo-workflows/argo-workflows.v1 with chart argo-workflows@0.40.11                      
helmrelease/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
helmrelease/karpenter                           1.4.0           False           True    Helm install succeeded for release karpenter/karpenter.v1 with chart karpenter@1.4.0                                       
helmrelease/kubecost                            2.1.0           False           True    Helm install succeeded for release kubecost/kubecost.v1 with chart cost-analyzer@2.1.0                                     
helmrelease/metrics-server                      3.11.0          False           True    Helm install succeeded for release kube-system/metrics-server.v1 with chart metrics-server@3.11.0                          
helmrelease/onboarding-service                  0.0.1           False           True    Helm install succeeded for release onboarding-service/onboarding-service.v1 with chart application-chart@0.0.1             
helmrelease/pool-1                              0.0.1           False           True    Helm upgrade succeeded for release pool-1/pool-1.v2 with chart helm-tenant-chart@0.0.1                                     
helmrelease/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                       

NAME                                    REVISION                        SUSPENDED       READY   MESSAGE                                         
kustomization/capacitor                 v0.4.8@sha256:1efcb443          False           True    Applied revision: v0.4.8@sha256:1efcb443       
kustomization/controlplane              refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/dataplane-pooled-envs     refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/dataplane-tenants         refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/dependencies              refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/flux-system               refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/infrastructure            refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a
kustomization/sources                   refs/heads/main@sha1:06abc42a   False           True    Applied revision: refs/heads/main@sha1:06abc42a

Gitea 저장소 접근 설정

아키텍처에서 살펴보았듯, gitea 서버에 있는 다양한 저장소에 접근할 수 있어야 합니다. 먼저 아래 명령을 통해 gitea 서버의 접근정보를 확인합니다.

[ec2-user@ip-10-0-1-245 environment]$ 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"
[ec2-user@ip-10-0-1-245 environment]$ export GITEA_ADMIN_PASSWORD=$(aws ssm get-parameter --name "/eks-saas-gitops/gitea-admin-password" --with-decryption --query 'Parameter.Value' --output text)
[ec2-user@ip-10-0-1-245 environment]$ 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://<REDACTED>:3000
Username: admin
Password: <REDACTED>
==================================

Use the PUBLIC URL above to access Gitea from your web browser.

이후 ConfigMap에서 필요한 값을 가지고 와 클론할 준비를 합니다.

# 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
[ec2-user@ip-10-0-1-245 environment]$ cd $REPO_PATH
# Clone the microservice repositories
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

# Verify the repositories were cloned successfully
echo "Microservice repositories:"
ls -la $REPO_PATH
echo ""
echo "GitOps repository:"
ls -la $GITOPS_REPO_PATH
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.
Microservice repositories:
total 0
drwxrwxr-x. 5 ec2-user ec2-user 54 Apr 26 18:30 .
drwxr-xr-x. 4 ec2-user ec2-user 86 Apr 26 18:30 ..
drwxrwxr-x. 4 ec2-user ec2-user 93 Apr 26 18:30 consumer
drwxrwxr-x. 4 ec2-user ec2-user 93 Apr 26 18:30 payments
drwxrwxr-x. 4 ec2-user ec2-user 93 Apr 26 18:30 producer

GitOps repository:
total 4
drwxr-xr-x. 9 ec2-user ec2-user  156 Apr 24 05:50 .
drwxr-xr-x. 4 ec2-user ec2-user   86 Apr 26 18:30 ..
drwxr-xr-x. 7 ec2-user ec2-user  147 Apr 26 15:18 .git
-rw-r--r--. 1 ec2-user ec2-user 1132 Apr 24 05:50 .gitignore
drwxr-xr-x. 3 ec2-user ec2-user   24 Apr 24 05:50 application-plane
drwxr-xr-x. 3 ec2-user ec2-user   24 Apr 24 05:50 clusters
drwxr-xr-x. 3 ec2-user ec2-user   24 Apr 24 05:50 control-plane
drwxr-xr-x. 4 ec2-user ec2-user   56 Apr 24 05:50 helm-charts
drwxr-xr-x. 4 ec2-user ec2-user   36 Apr 24 05:50 infrastructure
drwxr-xr-x. 3 ec2-user ec2-user   21 Apr 24 05:50 terraform

마지막으로 flux가 gitea 저장소 서버를 잘 모니터링하고 있는지 살펴봅니다. flux-system 과 terraform-v0-0-1 두 GitRepository가 READY=True 상태인지 확인하였습니다.

[ec2-user@ip-10-0-1-245 microservice-repos]$ kubectl -n flux-system get gitrepository 
NAME               URL                                                 AGE     READY   STATUS
flux-system        http://10.35.48.28:3000/admin/eks-saas-gitops.git   1d12h   True    stored artifact for revision 'refs/heads/main@sha1:06abc42a8f9343dc24ebdc92e6e14bdee9eb6cac'
terraform-v0-0-1   http://10.35.48.28:3000/admin/eks-saas-gitops.git   1d12h   True    stored artifact for revision 'v0.0.1@sha1:2d19a84a88a6ded7c9aa8ac76508452e3f1d48b2'

1-2. Terraform & OpenTofu Controller

이번 애플리케이션 인프라는 Terraform 모듈을 사용하여 정의/관리됩니다. 저장소 내 모듈 구조는 아래와 같습니다:

tree /home/ec2-user/environment/gitops-gitea-repo/terraform/modules/ -L 2

gitops-gitea-repo/terraform/modules/
├── flux_cd/           # EKS에 Flux를 설치하는 데 필요한 리소스
├── gitea/             # Gitea 저장소에 필요한 리소스
├── gitops-saas-infra/ # 워크숍 전체 인프라 구성 리소스
└── tenant-apps/       # 테넌트 애플리케이션 전용 인프라 구성 요소
    ├── data.tf
    ├── main.tf
    ├── outputs.tf
    ├── variables.tf
    └── versions.tf

아키텍처를 살펴보시면 아래와 같습니다:

이 실습에서 핵심은 tenant-apps 모듈로, 이 모듈 하나로 새 테넌트 온보딩 시 필요한 SQS 큐, DynamoDB 테이블, IRSA(IAM Role for Service Account) 등 모든 인프라를 프로비저닝할 수 있다는 것이 특징입니다.

테라폼 모듈을 확인해보기

경로에 접근해서 init/plan 을 해봅시다.

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
enable_producer 옵션의 true/false 차이는?

irsa_role 의 유무입니다. 단순히 변수 하나를 컨트롤하는 것 만으로 내부 주요 설정을 크게 제어할 수 있다는 것이 장점입니다.

Tofu Controller 연동

Tofu Controller는 이 아키텍처에서 핵심적인 역할을 담당합니다. Terraform CRD라는 커스텀 리소스 정의를 활용하여 Terraform 모듈을 원활하게 연동할 수 있게 해줍니다.

Terraform Controller 동작 흐름

flowchart TD
    Git[("Git 저장소
(Terraform CRD & 모듈)")] Flux["Flux
(Source/Kustomize Controller)"] CRD["Terraform CRD
(클러스터에 생성)"] TFC["TF Controller
(flux-system 네임스페이스 감시)"] Runner["tf-runner Pod"] AWS["AWS 리소스 프로비저닝
(SQS, DynamoDB 등)"] Secret[("Kubernetes Secret
tfstate / tfplan")] Git -->|① 변경 감시| Flux Flux -->|② Reconciliation| CRD CRD -->|③ 생성/변경 감지| TFC TFC -->|④ 파드 실행| Runner Runner -->|⑤ Terraform 모듈 실행| AWS Runner -->|⑥ state / plan 저장| Secret
  1. Flux의 Git 저장소 감시: Flux는 Terraform CRD와 모듈이 포함된 Git 저장소의 변경 사항을 지속적으로 감시합니다.
  2. Terraform CRD 생성: Git 저장소에 정의된 Terraform CRD가 클러스터에 생성되면, Flux가 이를 감지하고 Reconciliation 프로세스를 시작합니다.
  3. TF Controller의 Terraform CRD 감시: TF Controller는 flux-system 네임스페이스 내의 Terraform CRD를 모니터링하다가, 새로운 CRD가 생성되거나 기존 CRD가 업데이트되면 필요한 작업을 시작합니다.
  4. tf-runner 파드 실행: TF Controller가 tf-runner 파드를 띄웁니다. 이 파드는 Git 저장소에서 지정된 Terraform 모듈을 가져와 실행하며, CRD에 정의된 대로 인프라를 관리합니다.
  5. 관리 리소스 프로비저닝: tf-runner 파드가 Terraform 모듈의 정의에 따라 Amazon SQS 큐, Amazon DynamoDB 테이블 등 필요한 리소스를 프로비저닝합니다.
  6. 상태를 시크릿에 저장: Terraform 실행의 상태(state)와 플랜(plan)은 쿠버네티스 시크릿(tfstate, tfplan)으로 저장되어 이후 Terraform 작업에서도 접근할 수 있습니다.

Tofu Controller 파드의 상태를 확인합니다.

kubectl get po -n flux-system
NAME                                       READY   STATUS    RESTARTS       AGE
capacitor-76b57ffc6f-2w92w                 1/1     Running   1 (131m ago)   159m
helm-controller-6fbc6bbbc9-7pfpz           1/1     Running   0              160m
kustomize-controller-9db49689-jdlt6        1/1     Running   0              160m
notification-controller-86d4b4cd45-m72lt   1/1     Running   0              160m
source-controller-cfb87bd97-57mhs          1/1     Running   0              160m
tf-controller-cf8b957d7-dvqk7              1/1     Running   0              159m

tf-controller 파드는 Terraform CRD 매니페스트를 감시합니다. Terraform CRD가 클러스터에 추가되면 별도의 파드에서 tf-runner를 실행하고, 지정된 모듈을 가져와 실행합니다. 동작을 직접 확인하기 위해 Terraform CRD를 생성해봅니다.

cat << 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
EOF

생성된 파일에서 sourceRef는 Flux가 사용하는 또 다른 커스텀 리소스 정의인 terraform-v0-0-1 GitRepository를 가리킵니다. 이 리소스를 살펴봅니다.

kubectl get GitRepository terraform-v0-0-1 -nflux-system -oyaml | grep -i spec -C10
spec:
  interval: 300s
  ref:
    tag: v0.0.1
  secretRef:
    name: flux-system
  timeout: 60s
  url: http://10.35.48.243:3000/admin/eks-saas-gitops.git

GitRepository는 v0.0.1 태그를 가리키는 Gitea 저장소를 참조하며, 해당 태그의 리소스를 감시합니다. 이 태그는 워크샵 환경 구성 시 생성된 것입니다.

git tag

Terraform CRD에 정의된 path는 앞서 테스트한 tenant-apps 모듈을 가리킵니다.

spec:
  path: ./terraform/modules/tenant-apps

GitOps 방식으로 배포하려면, Flux가 이를 Reconcile할 수 있도록 kustomization.yaml에 새 파일에 대한 참조를 추가합니다.

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
EOF

Flux가 감시하는 main 브랜치에 변경 사항을 커밋하고 푸시합니다.

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 Reconciliation을 강제 실행합니다.

flux reconcile source git flux-system

잠시 후 po/example-tenant-tf-runner 파드가 실행되는 것을 확인할 수 있습니다. 컨트롤러가 실행한 이 파드는 tenant-apps Terraform 모듈을 수행합니다. 로그는 다음과 같이 확인합니다.

kubectl get po -nflux-system
kubectl logs po/example-tenant-tf-runner -nflux-system -f

Terraform이 Amazon EKS에 생성한 리소스를 검증합니다. 먼저 example-tenant용 DynamoDB 테이블이 프로비저닝됐는지 확인합니다.

aws dynamodb list-tables

consumer-example-tenant- 접두사를 가진 테이블을 확인할 수 있습니다. 이어서 SQS 큐도 확인합니다.

aws sqs list-queues

마찬가지로 consumer-example-tenant- 접두사를 가진 SQS 큐를 확인할 수 있습니다.

Terraform CRD 삭제

Tofu Controller를 통해 Terraform이 생성한 리소스를 삭제하려면, 저장소에서 CRD를 제거하고 kustomization 파일에서 해당 참조를 삭제하면 됩니다.

Terraform CRD 파일을 삭제합니다.

rm /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/example-tenant-terraform-crd.yaml

kustomization.yaml에서 Terraform CRD 참조를 제거합니다.

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 main

Flux Reconciliation을 강제 실행합니다.

flux reconcile source git flux-system

tf-runner의 terraform destroy 로그는 다음과 같이 확인합니다.

kubectl logs po/example-tenant-tf-runner -nflux-system -f

리소스 삭제 여부를 최종 검증합니다.

aws dynamodb list-tables
aws sqs list-queues

이번 실습에서는 Tofu Controller를 연동하여 Terraform CRD를 통해 Terraform 모듈을 관리하고, 인프라 프로비저닝을 자동화한 뒤 생성된 리소스를 검증했습니다. 다음에는 Helm 차트와, 쿠버네티스 애플리케이션 배포·관리에서 Helm이 담당하는 역할을 살펴보겠습니다.

Helm 차트

Helm 차트는 쿠버네티스 애플리케이션을 정의하고 설치하며 관리하는 강력한 방법입니다. 쿠버네티스 리소스를 패키지로 묶어 일관된 방식으로 관리할 수 있게 해줍니다. Helm 차트의 구조와 이번 워크샵에서의 활용 방식을 살펴봅니다.


Helm 차트 구조

Helm 차트는 연관된 쿠버네티스 리소스들을 기술하는 파일들의 모음으로, 다음과 같은 디렉토리 구조로 구성됩니다.

├── Chart.yaml         # 차트에 대한 정보
├── values.yaml        # 차트의 기본 설정값
├── charts/            # 의존 차트를 담는 디렉토리
├── templates/         # 쿠버네티스 매니페스트 템플릿을 담는 디렉토리
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── terraform.yaml
│   └── ...

워크샵에서 사용하는 Helm 차트

이번 워크샵에서는 두 가지 주요 Helm 차트를 사용합니다.

테넌트 애플리케이션용 Helm 차트의 구조는 다음과 같습니다.

├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── terraform.yaml
│   └── ...

최소 values 파일 예시

Helm 차트를 테스트할 때는 필수 파라미터만 담은 간소화된 values.yaml을 사용할 수 있습니다.

# values.yaml
tenantId: "example-tenant"

apps:
  producer:
    enabled: true

  consumer:
    enabled: true

values.yaml은 템플릿에 사용할 설정값을 제공합니다. 최소한의 값만 정의한다는 것은 일부 파라미터만 재정의하고 나머지는 차트의 기본값에 의존한다는 의미입니다. 모든 설정 옵션을 일일이 재정의하지 않아도 되므로 빠른 테스트와 커스터마이징에 유리합니다.

Helm 차트 테스트

helm template 명령어를 사용하면 실제 배포 없이 쿠버네티스 매니페스트를 렌더링해볼 수 있습니다. 먼저 test-values.yaml 파일을 생성합니다.

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
EOF

프로듀서와 컨슈머 서비스를 모두 활성화한 상태로 차트를 테스트합니다.

cd /home/ec2-user/environment/gitops-gitea-repo
helm template example-tenant ./helm-charts/helm-tenant-chart --values ./helm-charts/helm-tenant-chart/test-values.yaml

터미널에 렌더링된 쿠버네티스 매니페스트가 출력되며, 프로듀서와 컨슈머 서비스 각각의 리소스를 확인할 수 있습니다.

이번엔 프로듀서 서비스를 비활성화하고 다시 테스트해봅니다. values.yaml에서 producer.enabledfalse로 변경합니다.

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
EOF
helm template example-tenant ./helm-charts/helm-tenant-chart --values ./helm-charts/helm-tenant-chart/test-values.yaml

프로듀서가 비활성화되어 디플로이먼트는 생성되지 않지만, 프로듀서에 대한 라우트는 여전히 생성됩니다. 특정 테넌트의 프로듀서가 비활성화된 경우 요청을 공용(pooled) 프로듀서로 포워딩하기 위함입니다. 이는 폴백(fallback) 메커니즘을 통해 서비스 연속성을 보장하는 구조로, 이후 티어 전략 섹션에서 리소스 관리와 테넌트별 서비스 수준 처리 방식을 더 자세히 다룹니다.

테스트가 끝났으면 임시 파일을 정리합니다.

rm -rf /home/ec2-user/environment/gitops-gitea-repo/helm-charts/helm-tenant-chart/test-values.yaml

이번 섹션에서는 Helm 차트의 구조를 살펴보고, helm template 명령어로 생성된 매니페스트를 직접 검증해봤습니다. 다음 섹션에서는 HelmRelease CRD를 활용하여 Helm 차트를 Flux와 연동하고, GitOps 원칙에 따라 Helm 배포를 관리하는 방법을 살펴보겠습니다.

Helm 차트와 Flux 연동

이 섹션에서는 Helm 차트를 Flux와 연동하여 쿠버네티스 애플리케이션을 관리하는 방법을 살펴봅니다. HelmRelease를 사용해 테넌트 애플리케이션용 Helm 차트를 배포합니다. 시작에 앞서, Helm 차트는 이미 패키징되어 ECR에 푸시된 상태입니다. 다음과 같이 확인합니다.

# 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)

# Docker를 ECR에 인증
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY

# ECR 리포지토리의 아티팩트(이미지) 목록 조회
aws ecr list-images --repository-name $ECR_REPOSITORY --region $AWS_REGION

0.0.1 태그로 패키징된 Helm 차트 하나가 이미 푸시되어 있는 것을 확인할 수 있습니다.

{
    "imageIds": [
        {
            "imageDigest": "sha256:15806915991455232e0c7084008389392aa106d62bddd6c553ff062a35a46083",
            "imageTag": "0.0.1"
        }
    ]
}

HelmRelease는 Flux가 제공하는 CRD로, Helm 릴리즈를 선언적으로 관리할 수 있게 해줍니다. YAML 파일로 Helm 릴리즈를 정의하면 Flux가 클러스터에서의 배포와 관리를 자동으로 처리합니다.

다음은 example-tenant에 대한 HelmRelease 예시입니다.

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  # 사일로(Silo) 배포 -- 프리미엄 티어는 테넌트별 전용 배포
      consumer:
        enabled: true  # 사일로(Silo) 배포 -- 프리미엄 티어는 테넌트별 전용 배포

위 예시에서 sourceRefHelmRepository를 가리키고 있습니다. 이 경우 Helm 차트는 Amazon ECR에 저장됩니다. HelmRepository의 상세 정보는 다음과 같이 확인합니다.

$ kubectl get HelmRepository -nflux-system | grep -i helm-tenant-chart

helm-tenant-chart    oci://123456789012.dkr.ecr.us-west-2.amazonaws.com/gitops-saas/

Flux가 Helm 릴리즈를 관리하고 배포하는 데 사용하는 ECR 리포지토리를 확인할 수 있습니다.

HelmRelease 생성

Step 1: HelmRelease YAML 파일 생성

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

Step 2: Kustomization에 HelmRelease 추가

GitOps 방식으로 배포하려면 kustomization.yaml에 새 파일에 대한 참조를 추가합니다.

cat << EOF >> /home/ec2-user/environment/gitops-gitea-repo/application-plane/production/tenants/kustomization.yaml
    - example-tenant-helmrelease.yaml
EOF

Step 3: 변경 사항 커밋 및 푸시

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

Step 4: Flux Reconciliation 강제 실행

flux reconcile source git flux-system

리소스 확인

HelmRelease를 Git에 푸시하고 Flux가 Reconcile을 완료하면, 쿠버네티스와 AWS에 생성된 리소스를 확인합니다.

쿠버네티스 리소스 확인

example-tenant 네임스페이스가 생성됐는지 확인합니다.

kubectl get namespaces

해당 네임스페이스의 리소스 목록을 조회합니다.

kubectl get all -n example-tenant

AWS 리소스 확인

example-tenant용 DynamoDB 테이블과 SQS 큐가 생성됐는지 확인합니다.

aws dynamodb list-tables
aws sqs list-queues

리소스 정리

HelmRelease와 연관된 리소스를 삭제하려면 HelmRelease YAML 파일을 제거하고 kustomization.yaml을 업데이트합니다.

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 Reconciliation을 강제 실행합니다.

flux reconcile source git flux-system

Flux가 Reconcile을 완료하고 Helm 릴리즈와 Terraform 리소스를 정리하는 데 다소 시간이 걸릴 수 있습니다. tf-runner 로그는 다음과 같이 확인합니다.

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

이번 섹션에서는 HelmRelease를 생성하여 Helm 차트와 Flux를 연동하고, 쿠버네티스 및 AWS에 생성된 리소스를 확인한 뒤 정상적으로 정리되는 과정까지 살펴봤습니다. 다음 섹션에서는 티어 전략을 더 깊이 살펴보고, 테넌트별 리소스를 효율적으로 관리하는 방법을 다루겠습니다.


  1. 물론 현재에도 여전히 유효하며, 필요에 따라 자체적으로 꾸리는 경우도 존재하죠! ↩︎

  2. 이 외에도 AWS가 플랫폼 엔지니어링을 소개하고 수많은 고객사와 협업한 방식 도 소개시켜 드리고 싶지만, 최소 4개의 회사에 대해 첨예한 논의방안이 있으므로 여러분들도 살펴보시기를 강권합니다. ↩︎