CI/CD¶
EKS 환경에서 Flux v2와 Terraform 컨트롤러를 활용한 GitOps 기반 CI/CD 파이프라인 구성을 정리한다.
개요¶
Amazon EKS 환경에서 GitOps 기반 CI/CD 파이프라인을 구축하고, Platform Engineering 개념을 활용하여 Multi-Tenant SaaS 플랫폼을 구현하는 방법을 학습한다.
이번 실습에서는 다음 도구들을 사용한다:
| 도구 | 역할 |
|---|---|
| Flux v2 | Git 저장소 변경 사항을 Kubernetes 클러스터에 자동 동기화 |
| Argo Workflows | 테넌트 온보딩/오프보딩 자동화 |
| Tofu 컨트롤러 | Terraform을 GitOps 방식으로 실행 |
| Helm 차트 | Kubernetes 애플리케이션 패키징 및 배포 |
GitOps란?¶
GitOps 4대 원칙¶
graph TD
A[GitOps Principles] --> B[1. Declarative 선언적]
A --> C[2. Versioned and Immutable 버전 관리 및 불변성]
A --> D[3. Pulled Automatically 자동 Pull]
A --> E[4. Continuously Reconciled 지속적 조정]
B --> B1["Git = Single Source of Truth<br/>YAML로 원하는 상태 선언"]
C --> C1["Git 커밋 = 버전 히스토리<br/>Rollback 가능"]
D --> D1["Software Agent가 Git 변경 감지<br/>자동으로 클러스터에 적용"]
E --> E1["Actual State → Desired State<br/>Drift 발생 시 자동 복구"]
| 원칙 | 설명 |
|---|---|
| Declarative | 원하는 상태(Desired State)를 선언적으로 정의 (YAML) |
| Versioned & Immutable | Git 커밋으로 버전 히스토리 유지, Rollback 가능 |
| Pulled Automatically | Software Agent가 Git 변경 감지, 자동 반영 |
| Continuously Reconciled | Actual State와 Desired State 차이(Drift) 자동 조정 |
Platform Engineering과 DevOps 진화¶
graph LR
A[DevOps<br/>2009~] --> B[GitOps<br/>2017~]
B --> C[Platform Engineering<br/>2020~]
A --> A1["개발팀과 운영팀 협업<br/>자동화 + 문화 변화"]
B --> B1["Git = 진실의 단일 소스<br/>선언적 인프라 관리"]
C --> C1["IDP 구축<br/>셀프서비스 플랫폼"]
C --> D[3대 가치]
D --> D1[Velocity<br/>속도]
D --> D2[Governance<br/>거버넌스]
D --> D3[Efficiency<br/>효율성]
Platform Engineering 3대 가치:
| 가치 | 설명 |
|---|---|
| Velocity (속도) | 빠른 서비스 배포 기능 제공 |
| Governance (거버넌스) | 정의, 인정, 확정 등의 요구 사항을 플랫폼 차원에서 자동화 |
| Efficiency (효율성) | 반복 대신 구성을 통해 인프라 비용을 절감하고, 인적 자원의 전문성을 더 섬세하게 활용 |
EKS GitOps 전체 아키텍처¶
graph TB
subgraph "Git Repository"
GR[Gitea<br/>GitOps Repository]
HR[Helm Charts<br/>Application Templates]
end
subgraph "Amazon EKS Cluster"
subgraph "GitOps Controllers"
FC[Flux v2<br/>GitRepository Reconciler]
TC[Tofu Controller<br/>Terraform CRD]
HC[Helm Controller<br/>HelmRelease]
end
subgraph "Argo Workflows"
AW[Argo Workflows<br/>Tenant Onboarding/Offboarding]
end
subgraph "Tenants"
T1[Tenant-1<br/>Premium Tier]
T2[Tenant-2<br/>Basic Tier]
T3[Tenant-3<br/>Advanced Tier]
end
end
subgraph "AWS Resources"
ECR[Amazon ECR<br/>Helm Chart Images]
SQS[Amazon SQS]
DDB[Amazon DynamoDB]
S3[Amazon S3]
end
GR -->|Watch| FC
HR -->|Pull| HC
FC -->|Create| TC
FC -->|Create| HC
TC -->|Provision| SQS
TC -->|Provision| DDB
HC -->|Deploy| T1
HC -->|Deploy| T2
HC -->|Deploy| T3
ECR -.->|Pull Images| HC
AW -->|Trigger| GR
핵심 구성 요소:
- Flux v2: Git 저장소를 Watch하여 Kubernetes 리소스 자동 동기화
- Tofu 컨트롤러: Terraform 코드를 GitOps 방식으로 실행 (AWS 리소스 프로비저닝)
- Helm 컨트롤러: Helm 차트 기반 애플리케이션 배포
- Argo Workflows: 테넌트 온보딩/오프보딩 워크플로우 자동화
실습 환경 구성¶
실습 환경¶
| 리소스 | 사양 | 용도 |
|---|---|---|
| EKS Cluster | myeks | Kubernetes 클러스터 (v1.31) |
| Bastion EC2 | t3.medium | 관리 호스트 (kubectl, Flux CLI) |
| Amazon ECR | - | 애플리케이션 컨테이너 이미지 및 Helm 차트 저장소 |
| Gitea | GitOps Repository | Git 저장소 (Terraform, Helm Charts, Application 매니페스트) |
| AWS Resources | SQS, DynamoDB, IAM | 테넌트별 AWS 리소스 |
GitOps 컨트롤러¶
| 컨트롤러 | 버전 | 역할 |
|---|---|---|
| Flux v2 | v2.x | GitRepository Watch 및 Kubernetes 리소스 자동 동기화 |
| Tofu 컨트롤러 | v0.x | Terraform 코드를 GitOps 방식으로 실행 |
| Helm 컨트롤러 | v1.x | Helm 차트 배포 및 HelmRelease 관리 |
| Argo Workflows | v3.x | 테넌트 온보딩/오프보딩 워크플로우 |
실습 1: GitOps로 구현하는 SaaS 플랫폼 엔지니어링¶
1.1 시작하기¶
EKS 클러스터 준비¶
먼저 EKS 클러스터가 정상적으로 구성되었는지 확인한다:
# EKS 클러스터 정보 확인
> EKS GitOps CI/CD (Flux v2, Tofu Controller, Argo Workflows)를 학습한다.
$ kubectl cluster-info
Kubernetes control plane is running at https://XXXXXXXXX.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://XXXXXXXXX.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Namespace 확인¶
GitOps 컨트롤러와 테넌트가 사용할 Namespace를 확인한다:
# Namespace 목록 확인
$ kubectl get ns
NAME STATUS AGE
default Active 42m
flux-system Active 38m
kube-node-lease Active 42m
kube-public Active 42m
kube-system Active 42m
gitea Active 35m
argo-workflows Active 30m
argo-events Active 30m
Flux v2 리소스 확인¶
Flux v2는 다음과 같은 CRD(Custom Resource Definition)를 제공한다:
| Flux v2 리소스 | 역할 |
|---|---|
| gitrepository | Git 저장소 Watch (ECR 로그인 포함) |
| helmrepository | Helm 차트가 저장된 저장소 (ECR 포함) |
| helmchart | 각 소스에서 가져올 Helm 차트 |
| helmrelease | 실제 배포 단위, Helm 차트를 어떤 네임스페이스에 배포 가능 |
| kustomization | GitRepository를 기반 Kubernetes 구성 관리 |
| imagerepository / imagepolicy | 새 컨테이너 이미지 자동 감지 및 정책 적용 |
| imageupdateautomation | 새 이미지 감지 시 Git에 자동 커밋 (Image Automation) |
# GitRepository 리소스 확인
$ kubectl get gitrepository -n flux-system
NAME URL AGE READY STATUS
terraform-v0-0-1 http://admin:***@gitea:3000/admin/... 29m True stored artifact
# HelmRepository 리소스 확인
$ kubectl get helmrepository -n flux-system
NAME URL AGE READY STATUS
helm-tenant-chart oci://ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com 25m True Helm repository is ready
# Kustomization 리소스 확인
$ kubectl get kustomization -n flux-system
NAME AGE READY STATUS
flux-system 38m True Applied revision: main@sha1:abc123
terraform-v0-0-1 29m True Applied revision: v0.0.1@sha1:def456
Gitea Git 저장소 구성¶
GitOps 방식으로 배포하려면 Git 저장소가 필요한다. 이번 실습에서는 Gitea(Self-hosted Git)를 사용한다:
# Gitea Pod 확인
$ kubectl get pod -n gitea
NAME READY STATUS RESTARTS AGE
gitea-0 1/1 Running 0 35m
# Gitea Service 확인
$ kubectl get svc -n gitea
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
gitea ClusterIP 10.100.50.100 <none> 3000/TCP 35m
gitea-ssh ClusterIP 10.100.50.101 <none> 22/TCP 35m
gitops-gitea-repo/
├── terraform/ # Terraform 모듈 (AWS 리소스)
│ └── tenant-apps/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── helm-releases/ # HelmRelease YAML 파일 (테넌트별)
│ ├── tier-basic/
│ ├── tier-advanced/
│ └── tier-premium/
└── helm-charts/ # Helm 차트 템플릿
├── helm-tenant-chart/
└── application-chart/
1.2 Terraform 및 OpenTofu 컨트롤러¶
Tofu 컨트롤러 동작 원리¶
graph LR
subgraph "Git Repository"
TF[Terraform 코드<br/>tenant-apps 모듈]
end
subgraph "EKS Cluster"
FC[Flux v2<br/>GitRepository Watch]
TC[Tofu Controller<br/>Terraform CRD]
TR[TF-Runner<br/>Pod]
end
subgraph "AWS"
SQS[Amazon SQS<br/>Queue]
DDB[Amazon DynamoDB<br/>Table]
IAM[IAM Role<br/>IRSA]
end
TF -->|Git Push| FC
FC -->|Create| TC
TC -->|Spawn| TR
TR -->|Provision| SQS
TR -->|Provision| DDB
TR -->|Provision| IAM
TC -.->|tfstate Secret| K8S[(Kubernetes Secret<br/>tfstate)]
TR -.->|tfplan Secret| K8S
동작 흐름:
1. Git 저장소에 Terraform 코드 Push (Git 커밋)
2. Flux가 변경 감지 → Tofu 컨트롤러가 Terraform CRD 생성
3. tf-runner Pod가 실행되어 Terraform 모듈 실행 (terraform init, terraform plan, terraform apply)
4. AWS 리소스(SQS, DynamoDB, IAM) 생성
5. Terraform State를 Kubernetes Secret에 저장 (tfstate, tfplan)
Terraform 모듈 구조¶
$ tree terraform/tenant-apps/
terraform/tenant-apps/
├── main.tf # 메인 Terraform 코드
├── variables.tf # 입력 변수 정의
├── outputs.tf # 출력 값 정의
└── versions.tf # Provider 버전 관리
# SQS Queue (Producer → Consumer 메시지 전달)
resource "aws_sqs_queue" "tenant_queue" {
count = var.enable_producer ? 1 : 0
name = "${var.tenant_id}-queue"
delay_seconds = 0
max_message_size = 262144
message_retention_seconds = 345600
receive_wait_time_seconds = 0
tags = {
TenantId = var.tenant_id
Tier = var.tenant_tier
}
}
# DynamoDB Table (Producer가 메시지 메타데이터 저장)
resource "aws_dynamodb_table" "tenant_table" {
count = var.enable_producer ? 1 : 0
name = "${var.tenant_id}-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
tags = {
TenantId = var.tenant_id
Tier = var.tenant_tier
}
}
# IAM Role for IRSA (Pod → AWS 리소스 접근)
resource "aws_iam_role" "tenant_role" {
count = var.enable_producer || var.enable_consumer ? 1 : 0
name = "${var.tenant_id}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = var.oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${var.oidc_provider}:sub" = "system:serviceaccount:${var.tenant_id}:${var.tenant_id}-sa"
"${var.oidc_provider}:aud" = "sts.amazonaws.com"
}
}
}]
})
}
variable "tenant_id" {
description = "Tenant ID"
type = string
}
variable "tenant_tier" {
description = "Tenant Tier (basic, advanced, premium)"
type = string
}
variable "enable_producer" {
description = "Enable Producer resources (SQS, DynamoDB)"
type = bool
default = true
}
variable "enable_consumer" {
description = "Enable Consumer resources"
type = bool
default = true
}
variable "oidc_provider_arn" {
description = "EKS OIDC Provider ARN for IRSA"
type = string
}
variable "oidc_provider" {
description = "EKS OIDC Provider URL (without https://)"
type = string
}
Terraform CRD 생성¶
Flux가 Git 저장소를 감지하면 자동으로 Terraform CRD를 생성한다:
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
name: tenant-example
namespace: flux-system
spec:
interval: 1m
path: ./terraform/tenant-apps
sourceRef:
kind: GitRepository
name: terraform-v0-0-1
vars:
- name: tenant_id
value: "tenant-example"
- name: tenant_tier
value: "premium"
- name: enable_producer
value: "true"
- name: enable_consumer
value: "true"
- name: oidc_provider_arn
value: "arn:aws:iam::ACCOUNT_ID:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXX"
- name: oidc_provider
value: "oidc.eks.ap-northeast-2.amazonaws.com/id/XXXXX"
writeOutputsToSecret:
name: tenant-example-outputs
storeReadablePlan: human
approvePlan: auto
sourceRef: GitRepository 이름 (terraform-v0-0-1)
- path: Terraform 모듈 경로 (./terraform/tenant-apps)
- vars: Terraform 변수 전달
- writeOutputsToSecret: Terraform Outputs를 Kubernetes Secret에 저장
- approvePlan: auto: terraform plan 후 자동으로 terraform apply 실행
tf-runner Pod 실행¶
Terraform CRD가 생성되면 tf-runner Pod가 실행된다:
# tf-runner Pod 확인
$ kubectl get pod -n flux-system | grep tf-runner
tf-runner-tenant-example-xxxxxxx 0/1 Completed 0 2m
$ kubectl logs -n flux-system tf-runner-tenant-example-xxxxxxx
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.x.x
Terraform has been successfully initialized!
Terraform used the selected providers to generate the following execution plan:
# aws_sqs_queue.tenant_queue[0] will be created
+ resource "aws_sqs_queue" "tenant_queue" {
+ name = "tenant-example-queue"
...
}
# aws_dynamodb_table.tenant_table[0] will be created
+ resource "aws_dynamodb_table" "tenant_table" {
+ name = "tenant-example-table"
...
}
Plan: 3 to add, 0 to change, 0 to destroy.
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Terraform State 저장¶
Terraform State는 Kubernetes Secret에 저장된다:
# Terraform State Secret 확인
$ kubectl get secret -n flux-system | grep tfstate
tfstate-default-tenant-example Opaque 1 2m
# Secret 내용 확인
$ kubectl get secret -n flux-system tfstate-default-tenant-example -o jsonpath='{.data.tfstate}' | base64 -d | jq '.resources[] | {type, name}'
{
"type": "aws_sqs_queue",
"name": "tenant_queue"
}
{
"type": "aws_dynamodb_table",
"name": "tenant_table"
}
{
"type": "aws_iam_role",
"name": "tenant_role"
}
enable_producer와 enable_consumer 옵션
- enable_producer = false: Producer 리소스(SQS, DynamoDB, IAM) 생성하지 않음
- enable_consumer = false: Consumer 리소스 생성하지 않음
- Basic Tier: enable_producer = false (공유 pool-1 사용)
- Advanced Tier: enable_producer = false, enable_consumer = true (Consumer만 전용)
- Premium Tier: enable_producer = true, enable_consumer = true (모두 전용)
1.3 Helm 차트¶
Helm 차트 디렉터리 구조¶
$ tree helm-charts/
helm-charts/
├── helm-tenant-chart/ # 테넌트별 애플리케이션 배포 (Producer + Consumer 통합)
│ ├── Chart.yaml
│ ├── templates/
│ │ ├── deployment.yaml # Producer/Consumer Deployment
│ │ ├── service.yaml # ClusterIP Service
│ │ ├── ingress.yaml # ALB Ingress
│ │ ├── hpa.yaml # Horizontal Pod Autoscaler
│ │ └── serviceaccount.yaml # IRSA ServiceAccount
│ ├── values.yaml # 기본값
│ └── values.yaml.template # 템플릿 (티어별 Override)
└── application-chart/ # Onboarding Service 동작
├── Chart.yaml
└── templates/
helm-tenant-chart 상세¶
Chart.yaml:
apiVersion: v2
name: helm-tenant-chart
description: Multi-Tenant SaaS Application Chart
type: application
version: 0.0.1
appVersion: "1.0.0"
# Tenant 정보
tenantId: "default-tenant"
tier: "basic"
# Producer 설정
apps:
producer:
enabled: true
image:
repository: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/producer
tag: "latest"
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
env:
- name: TENANT_ID
value: "{{ .Values.tenantId }}"
- name: SQS_QUEUE_URL
value: "https://sqs.ap-northeast-2.amazonaws.com/ACCOUNT_ID/{{ .Values.tenantId }}-queue"
- name: DYNAMODB_TABLE_NAME
value: "{{ .Values.tenantId }}-table"
consumer:
enabled: true
image:
repository: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/consumer
tag: "latest"
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
env:
- name: TENANT_ID
value: "{{ .Values.tenantId }}"
- name: SQS_QUEUE_URL
value: "https://sqs.ap-northeast-2.amazonaws.com/ACCOUNT_ID/{{ .Values.tenantId }}-queue"
# Ingress 설정
ingress:
enabled: true
className: alb
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
host: "{{ .Values.tenantId }}.example.com"
# IRSA ServiceAccount
serviceAccount:
create: true
name: "{{ .Values.tenantId }}-sa"
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::ACCOUNT_ID:role/{{ .Values.tenantId }}-role"
# HPA 설정
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
ECR에 Helm 차트 업로드¶
Helm 차트를 Amazon ECR에 OCI 형식으로 업로드한다:
# ECR 로그인
$ aws ecr get-login-password --region ap-northeast-2 | \
helm registry login \
--username AWS \
--password-stdin ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com
Login Succeeded
# Helm 차트 패키징
$ cd helm-charts
$ helm package helm-tenant-chart
Successfully packaged chart and saved it to: /home/ec2-user/environment/helm-charts/helm-tenant-chart-0.0.1.tgz
# ECR에 Push
$ helm push helm-tenant-chart-0.0.1.tgz oci://ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com
Pushed: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/helm-tenant-chart:0.0.1
Digest: sha256:abc123def456...
$ aws ecr describe-images \
--repository-name helm-tenant-chart \
--region ap-northeast-2
{
"imageDetails": [
{
"imageDigest": "sha256:abc123def456...",
"imageTags": ["0.0.1"],
"imagePushedAt": "2026-04-15T10:30:00+09:00",
"artifactMediaType": "application/vnd.cncf.helm.chart.config.v1+json"
}
]
}
values.yaml 파일 내 values.yaml.template의 필요 기본 값을 Override하여 설정
- 테스트 값은 test-values.yaml 파일을 만들고 Override 설정
1.4 Helm 차트의 Flux 통합¶
Flux v2 Reconciliation 흐름¶
graph TB
subgraph "Flux v2 Reconciliation Loop"
FC[Flux Controller<br/>Watch GitRepository]
HR[HelmRepository<br/>ECR Helm Chart URL]
HC[HelmRelease CRD]
end
subgraph "Git Repository"
GR[Gitea<br/>HelmRelease YAML]
end
subgraph "Amazon ECR"
ECR[Helm Chart<br/>OCI Image]
end
subgraph "EKS Cluster"
DEPLOY[Deployment<br/>Producer Service]
SVC[Service]
ING[Ingress]
end
GR -->|Git Push| FC
FC -->|Create| HR
HR -->|Pull| ECR
FC -->|Create| HC
HC -->|Install/Upgrade| DEPLOY
HC -->|Install/Upgrade| SVC
HC -->|Install/Upgrade| ING
동작 흐름:
1. Flux v2가 GitRepository를 Watch
2. Git에서 HelmRelease YAML 파일 감지 (.spec.chart.spec.sourceRef 참조)
3. HelmRepository에서 ECR Helm Chart 이미지 Pull
4. Helm Install 또는 Helm Upgrade 실행
5. Kubernetes 리소스(Deployment, Service, Ingress) 생성
HelmRepository 생성¶
먼저 Amazon ECR Helm 차트 저장소를 HelmRepository로 등록한다:
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: helm-tenant-chart
namespace: flux-system
spec:
interval: 1m
type: oci
url: oci://ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com
# HelmRepository 확인
$ kubectl get helmrepository -n flux-system
NAME URL AGE READY STATUS
helm-tenant-chart oci://ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com 10m True Helm repository is ready
HelmRelease 생성 (Premium Tier)¶
example-tenant-premium.yaml:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: example-tenant-premium
namespace: flux-system
spec:
releaseName: example-tenant-premium
targetNamespace: example-tenant
interval: 1m0s
chart:
spec:
chart: helm-tenant-chart
version: "0.0.1"
sourceRef:
kind: HelmRepository
name: helm-tenant-chart
values:
tenantId: example-tenant
tier: premium
apps:
producer:
enabled: true
image:
repository: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/producer
tag: "latest"
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: TENANT_ID
value: "example-tenant"
- name: SQS_QUEUE_URL
value: "https://sqs.ap-northeast-2.amazonaws.com/ACCOUNT_ID/example-tenant-queue"
- name: DYNAMODB_TABLE_NAME
value: "example-tenant-table"
- name: ENVIRONMENT
value: "premium"
consumer:
enabled: true
image:
repository: ACCOUNT_ID.dkr.ecr.ap-northeast-2.amazonaws.com/consumer
tag: "latest"
replicas: 2
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
env:
- name: TENANT_ID
value: "example-tenant"
- name: SQS_QUEUE_URL
value: "https://sqs.ap-northeast-2.amazonaws.com/ACCOUNT_ID/example-tenant-queue"
- name: ENVIRONMENT
value: "premium"
ingress:
enabled: true
className: alb
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
host: "example-tenant.example.com"
serviceAccount:
create: true
name: "example-tenant-sa"
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::ACCOUNT_ID:role/example-tenant-role"
hpa:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
Git에 Push 및 Flux Reconciliation¶
# Git 커밋
$ cd /home/ec2-user/environment/gitops-gitea-repo
$ git add helm-releases/tier-premium/example-tenant-premium.yaml
$ git commit -m "Add HelmRelease for example-tenant-premium"
$ git push origin main
# Flux가 Git 변경 감지 (1분 이내)
$ kubectl get gitrepository -n flux-system -w
NAME URL READY STATUS
terraform-v0-0-1 http://admin:***@gitea:3000/admin/... True stored artifact: revision 'main@sha1:abc123'
# HelmRelease 생성 확인
$ kubectl get helmrelease -n flux-system
NAME AGE READY STATUS
example-tenant-premium 30s True Helm install succeeded
# Helm Release 확인
$ helm list -n example-tenant
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
example-tenant-premium example-tenant 1 2026-04-15 10:45:00.123456789 +0900 KST deployed helm-tenant-chart-0.0.1 1.0.0
배포된 리소스 확인¶
# Namespace 생성 확인
$ kubectl get ns example-tenant
NAME STATUS AGE
example-tenant Active 1m
# Deployment 확인
$ kubectl get deployment -n example-tenant
NAME READY UP-TO-DATE AVAILABLE AGE
example-tenant-premium-producer 2/2 2 2 1m
example-tenant-premium-consumer 2/2 2 2 1m
# Pod 확인
$ kubectl get pod -n example-tenant
NAME READY STATUS RESTARTS AGE
example-tenant-premium-producer-xxxxxxxxx-xxxxx 1/1 Running 0 1m
example-tenant-premium-producer-xxxxxxxxx-xxxxx 1/1 Running 0 1m
example-tenant-premium-consumer-xxxxxxxxx-xxxxx 1/1 Running 0 1m
example-tenant-premium-consumer-xxxxxxxxx-xxxxx 1/1 Running 0 1m
# Service 확인
$ kubectl get svc -n example-tenant
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
example-tenant-premium-producer ClusterIP 10.100.10.100 <none> 8080/TCP 1m
example-tenant-premium-consumer ClusterIP 10.100.10.101 <none> 8080/TCP 1m
# Ingress 확인
$ kubectl get ingress -n example-tenant
NAME CLASS HOSTS ADDRESS PORTS AGE
example-tenant-premium alb example-tenant.example.com k8s-examplet-xxxxxxxx-xxxxxxxxxx.elb... 80 1m
# ServiceAccount 확인 (IRSA)
$ kubectl get sa -n example-tenant example-tenant-sa -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/example-tenant-role
name: example-tenant-sa
namespace: example-tenant
kustomization.yaml을 통한 배포¶
여러 HelmRelease를 한 번에 관리하려면 kustomization.yaml을 사용한다:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: tenant-releases
namespace: flux-system
spec:
interval: 1m
path: ./helm-releases
prune: true
sourceRef:
kind: GitRepository
name: terraform-v0-0-1
# Kustomization 확인
$ kubectl get kustomization -n flux-system
NAME AGE READY STATUS
tenant-releases 5m True Applied revision: main@sha1:abc123
실습 2: SaaS 티어 전략¶
SaaS 티어 모델 (Silo, Hybrid, Pool)¶
graph TB
subgraph "SaaS Deployment Models"
S[Silo Model<br/>Tenant별 독립 환경]
H[Hybrid Model<br/>혼합 환경]
P[Pool Model<br/>공유 환경]
end
subgraph "Silo (Tenant-1, Tenant-2)"
S1[Web App 1]
S2[Microservice 1-A]
S3[Microservice 1-B]
S4[DB 1]
S5[Web App 2]
S6[Microservice 2-A]
S7[Microservice 2-B]
S8[DB 2]
end
subgraph "Hybrid (Tenant-3, Tenant-4)"
H1[Shared Web App]
H2[Microservice 3]
H3[Microservice 4]
H4[DB 3]
H5[DB 4]
end
subgraph "Pool (Tenant-5~N)"
P1[Shared Web App]
P2[Shared Microservice A]
P3[Shared Microservice B]
P4[Shared DB]
end
S --> S1
H --> H1
P --> P1
SaaS 티어별 특징:
- Silo (Premium Tier): 테넌트별 독립 환경, 전용 리소스, 높은 격리성, 높은 비용
- Hybrid (Advanced Tier): 일부 공유 + 일부 전용, 중간 격리성, 중간 비용
- Pool (Basic Tier): 완전 공유 환경, 낮은 격리성, 낮은 비용
티어별 설정¶
| 티어 | Producer | Consumer | 인프라 | 비용 | 격리 수준 |
|---|---|---|---|---|---|
| Basic | 공유 (pool-1) | 공유 (pool-1) | 공유 | 낮음 | 낮음 |
| Advanced | 공유 (pool-1) | 전용 | Hybrid | 중간 | 중간 |
| Premium | 전용 | 전용 | 전용 | 높음 | 높음 |
핵심: values 설정만으로 배포 방식이 결정되며, 각 티어별 Kubernetes 리소스 수준도 조절 가능
티어별 HelmRelease 생성¶
Basic Tier 예시:
spec:
values:
tenantId: tenant-basic
tier: basic
apps:
producer:
enabled: false # pool-1 공유 사용
env:
- name: SQS_QUEUE_URL
value: "https://sqs.../pool-1-queue" # 공유 Queue
consumer:
enabled: false # pool-1 공유 사용
spec:
values:
tenantId: tenant-advanced
tier: advanced
apps:
producer:
enabled: false # pool-1 공유 사용
env:
- name: SQS_QUEUE_URL
value: "https://sqs.../pool-1-queue"
- name: ENVIRONMENT
value: "pool-1"
consumer:
enabled: true # 전용 Consumer
env:
- name: ENVIRONMENT
value: "tenant-3"
spec:
values:
tenantId: tenant-premium
tier: premium
apps:
producer:
enabled: true # 전용 Producer
env:
- name: SQS_QUEUE_URL
value: "https://sqs.../tenant-premium-queue"
- name: ENVIRONMENT
value: "tenant-premium"
consumer:
enabled: true # 전용 Consumer
env:
- name: ENVIRONMENT
value: "tenant-premium"
티어별 배포 확인¶
# Premium Tier 확인
$ kubectl get pod -n tenant-premium
NAME READY STATUS RESTARTS AGE
tenant-premium-producer-xxx 1/1 Running 0 2m
tenant-premium-consumer-xxx 1/1 Running 0 2m
# Advanced Tier 확인 (Consumer만 존재)
$ kubectl get pod -n tenant-advanced
NAME READY STATUS RESTARTS AGE
tenant-advanced-consumer-xxx 1/1 Running 0 2m
# Basic Tier 확인 (Producer/Consumer 모두 pool-1 환경 사용, 별도 Pod 없음)
$ kubectl get pod -n tenant-basic
No resources found in tenant-basic namespace.
실습 3: 자동화된 테넌트 온보딩/오프보딩¶
Argo Workflows 온보딩 워크플로우¶
graph LR
subgraph "Trigger"
SQS[Amazon SQS<br/>Onboarding Queue]
end
subgraph "Argo Workflows"
SENSOR[Argo Events Sensor<br/>SQS 메시지 감지]
WF[Argo Workflow<br/>tenant-onboarding]
end
subgraph "Workflow Steps"
S1[1. Clone Repository]
S2[2. Create Tenant<br/>HelmRelease YAML]
S3[3. Git Push to Repo]
end
subgraph "Git Repository"
GR[Gitea<br/>eks-saas-gitops]
end
subgraph "Flux v2"
FC[Flux Controller<br/>Watch]
HR[HelmRelease CRD]
end
subgraph "EKS Cluster"
DEPLOY[Deployment<br/>Producer + Consumer]
end
SQS -->|Message| SENSOR
SENSOR -->|Trigger| WF
WF --> S1
S1 --> S2
S2 --> S3
S3 --> GR
GR -->|Watch| FC
FC -->|Create| HR
HR -->|Deploy| DEPLOY
온보딩 워크플로우 동작:
1. SQS 메시지 전송: {"tenant_id": "tenant-3", "tenant_tier": "advanced", "release_version": "0.0.1"}
2. Argo Events Sensor가 SQS 메시지 감지 → Argo Workflow 실행
3. Workflow Steps: Git Clone → HelmRelease YAML 생성 → Git Push
4. Flux v2가 Git 변경 감지 → HelmRelease CRD 생성
5. Helm Install 실행 → Kubernetes 리소스(Deployment, Service) 배포
온보딩 실행¶
# SQS 메시지 전송 (테넌트 온보딩 요청)
$ export ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL=$(kubectl get configmap -n flux-system ...)
$ aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_ONBOARDING_QUEUE_SQS_URL \
--message-body '{"tenant_id": "tenant-tldbc", "tenant_tier": "advanced", "release_version": "0.0.1"}'
# Argo Workflow 확인
$ kubectl -n argo-workflows get workflow
NAME STATUS AGE
tenant-onboarding-gzt45 Running 9s
# Argo Workflows Web UI 확인
$ ARGO_WORKFLOW_URL=$(kubectl -n argo-workflows get svc/argo-workflows-server -o json | jq -r '.status.loadBalancer.ingress[0].hostname')
$ echo http://$ARGO_WORKFLOW_URL:2746/workflows
# Gitea에서 HelmRelease 커밋 확인
# - 브랜치: tier-advanced
# - 파일: helm-releases/tier-advanced/tenant-tldbc.yaml
오프보딩 실행¶
# SQS 메시지 전송 (테넌트 오프보딩 요청)
$ export ARGO_WORKFLOWS_OFFBOARDING_QUEUE_SQS_URL=$(kubectl get configmap -n flux-system ...)
$ aws sqs send-message \
--queue-url $ARGO_WORKFLOWS_OFFBOARDING_QUEUE_SQS_URL \
--message-body '{"tenant_id": "tenant-tldbc", "tenant_tier": "advanced"}'
# Argo Workflow 확인
$ kubectl -n argo-workflows get workflow
NAME STATUS AGE
tenant-offboarding-gptkz Running 9s
# Gitea에서 HelmRelease 삭제 커밋 확인 (destroyResourcesOnDeletion: true)
# - 브랜치: tier-advanced
# - 커밋 메시지: "Removing tenant: tenant-tldbc in tier: advanced"
전체 자동화 흐름 요약¶
- Amazon SQS: 온보딩/오프보딩 트리거 메시지 수신
- Argo Events: SQS 메시지 감지 및 워크플로우 트리거
- Argo Workflows: 템플릿 기반 Git 커밋 생성 및 자동 커밋
- Flux v2: Git 변경 감지 → EKS 리소스 배포
- Tofu 컨트롤러: Terraform CRD 기반 AWS 인프라 프로비저닝
핵심 흐름:
SQS 메시지 1개 전송
→ Argo Events → Argo Workflows → Git 커밋
→ Flux → EKS 배포
→ Tofu Controller → AWS 리소스 생성
실습 4: 리소스 확인 및 테스트¶
배포 검증¶
# Premium Tier Deployment 확인
$ kubectl get deployment -n tenant-premium
NAME READY UP-TO-DATE AVAILABLE AGE
tenant-premium-producer 2/2 2 2 5m
tenant-premium-consumer 2/2 2 2 5m
# Basic Tier Deployment 확인
$ kubectl get deployment -n tenant-basic
# (결과 없음 - pool-1 환경 공유 사용)
# Advanced Tier Deployment 확인
$ kubectl get deployment -n tenant-advanced
NAME READY UP-TO-DATE AVAILABLE AGE
tenant-advanced-consumer 2/2 2 2 5m
환경 변수 확인 (티어별 차이)¶
# Premium Tier (전용 환경)
$ kubectl exec -n tenant-premium deployment/tenant-premium-producer -- env | grep ENVIRONMENT
ENVIRONMENT=tenant-premium
# Advanced Tier (Hybrid 환경)
$ kubectl exec -n tenant-advanced deployment/tenant-advanced-consumer -- env | grep ENVIRONMENT
ENVIRONMENT=tenant-3 # Consumer만 전용, Producer는 pool-1 사용
# DynamoDB 데이터 확인
$ aws dynamodb scan --table-name tenant-premium-table --region ap-northeast-2 | jq '.Items'
# SQS 메시지 확인
$ aws sqs receive-message --queue-url https://sqs.../tenant-premium-queue --region ap-northeast-2
Argo Image Updater¶
ArgoCD Image Updater 패턴: Git 저장소를 감시하여 새 컨테이너 이미지가 Push되면 자동으로 매니페스트를 업데이트하는 패턴이다.
동작 흐름: 1. Build → CI Push → Container Registry 2. ArgoCD Image Updater가 Registry Watch 3. 새 이미지 감지 시 Git Repository에 Image Tag 자동 업데이트 4. ArgoCD가 Git 변경 감지 → Kubernetes 배포
참고 링크: - argo CD Image Updater - Blog - ArgoCD 배포에 레벨링 하기* - Blog - [CD] ArgoCD Image Updater를 활용한 Continuous Delivery w/AWS ECR - Blog
Argo CD App-of-apps¶
App-of-Apps 패턴: Root Application이 Child Applications을 관리하는 구조로, 여러 애플리케이션을 계층적으로 배포한다.
구조:
apps (Root Application)
├── helm-guestbook (Child Application)
├── helm-hooks (Child Application)
├── kustomize-guestbook (Child Application)
└── sync-waves (Child Application)
참고 링크: - ArgoCD Docs - Cluster Boostrapping: app of apps pattern - ArgoCD Example Apps - Github
핵심 개념 정리¶
GitOps vs Traditional CI/CD¶
| 항목 | Traditional CI/CD | GitOps |
|---|---|---|
| 배포 방식 | Push (CI 도구가 클러스터에 직접 배포) | Pull (클러스터가 Git 저장소를 감시하며 변경 사항 Pull) |
| 진실의 소스 | CI 도구 설정 | Git 저장소 |
| 상태 관리 | 수동 관리 | 자동 Drift 감지 및 복구 |
| 보안 | CI 도구에 클러스터 접근 권한 필요 | 클러스터가 Git 저장소만 접근 |
| 롤백 | 수동 롤백 | Git Revert로 자동 롤백 |
Flux v2 vs Argo CD¶
| 항목 | Flux v2 | Argo CD |
|---|---|---|
| 핵심 기능 | CLI 중심, Tofu/Helm 컨트롤러 통합 | GUI 중심, 웹 UI 강력 |
| 아키텍처 | Kubernetes CRD 및 컨트롤러 | 멀티클러스터 지원 강력 |
| Helm 지원 | (HelmRelease CRD) | (네이티브 RBAC 통합) |
| 커스터마이징 | ||
| 통합 GUI | (CLI 위주) | (강력한 웹 UI) |
| 커뮤니티 | CNCF 졸업 프로젝트 | CNCF 졸업 프로젝트 |
실무 선택 기준: - Flux v2: Terraform과의 통합이 필요하거나, CLI 기반 GitOps를 선호할 때 - Argo CD: 웹 UI를 통한 시각적 관리가 중요하거나, App-of-Apps 패턴을 사용할 때
Argo Workflows vs Jenkins X¶
| 항목 | Argo Workflows | Jenkins X |
|---|---|---|
| 실행 환경 | Kubernetes Native (Pod 단위 Step) | Kubernetes 기반 Jenkins 통합 |
| 워크플로우 정의 | YAML (Kubernetes CRD) | Jenkinsfile (Groovy) |
| GitOps 통합 | (Argo Events + Git 커밋) | (Lighthouse, Tekton) |
| 확장성 | 높음 (Kubernetes 스케일링) | 높음 (Jenkins 에이전트 확장) |
| 사용 사례 | 이벤트 드리븐 자동화, 배치 작업 | 기존 Jenkins 워크플로우 이관 |
이번 실습에서는 GitOps 기반 CI/CD 파이프라인을 구축하고, Platform Engineering 개념을 활용하여 Multi-Tenant SaaS 플랫폼을 구현했습니다.
핵심 학습 내용¶
- GitOps 4대 원칙: Declarative, Versioned and Immutable, Pulled Automatically, Continuously Reconciled
- Platform Engineering: 속도(Velocity), 거버넌스(Governance), 효율성(Efficiency)을 제공하는 Internal Developer Platform (IDP)
- Flux v2: GitRepository Watch → HelmRepository Pull → HelmRelease 생성 → Kubernetes 리소스 자동 동기화
- Tofu 컨트롤러: Terraform CRD를 통해 tf-runner Pod 실행 → AWS 리소스 프로비저닝 (SQS, DynamoDB, IAM)
- Helm 차트: ECR에 OCI 형식으로 저장, values 파일로 환경별 설정 Override
- HelmRelease: Flux v2가 제공하는 CRD, Helm 릴리스를 선언적으로 관리
- SaaS 티어 전략: Basic (Pool, 공유 환경), Advanced (Hybrid, Consumer만 전용), Premium (Silo, 모두 전용)
- Argo Workflows: SQS 메시지 → Argo Events → Workflow 실행 → Git 커밋 → Flux 배포 (완전 자동화)
전체 자동화 흐름 요약¶
SQS 메시지 1개 전송
↓
Argo Events → Argo Workflows → Git 커밋
↓
Flux → EKS 배포 (HelmRelease)
↓
Tofu Controller → AWS 리소스 생성 (SQS, DynamoDB, IAM)
GitOps 방식으로 Git = Single Source of Truth를 유지하며, 변경 사항은 모두 Git 커밋으로 추적 가능하고 Rollback도 간단한다 (Declarative + Versioned).