본문 바로가기
DevOps/EKS

[AEWS4기] FluxCD로 CI/CD 환경 구성하기

by 서어켜엉 2026. 4. 27.

현재 회사에서 사내 업무에 활용할 AI Agent를 직접 개발하고 있다. 운영 환경을 EKS로 결정했고, 단순히 컨테이너를 굴리는 수준이 아니라 스터디를 통해 배운 CI/CD 지식을 활용해 GitOps를 실제 환경에 적용해보고 싶었다. CI/CD 도구는 앞 글에서 고민한대로 FluxCD를 선택했다.

 

레포지토리 구성

총 3개의 레포지토리로 구성했다. 

레포지토리 관리도구 책임 범위
terraform-infra Terraform AWS 리소스 + FluxCD 부트스트랩
fleet-infra FluxCD 플랫폼 애드온
application FluxCD AI Agent 배포 Manifest
그 외 AI Agent 별 소스코드 레포지토리    

 

왜 3개의 레포지토리로 구성을 했나?

처음엔 단일 레포지토리(monorepo)도 고민했다. 관리 포인트가 적고, 작은 팀에서는 그게 더 효율적이라는 의견도 있다. 하지만 결국 분리하기로 결정했고, 그 이유는 아래와 같다.

 

1. 변경 주기와 위험도가 다르다

세 레이어의 변경 빈도는 완전히 다르다.

  • Terraform 인프라: 한 번 만들고 거의 건드리지 않는다. VPC나 EKS 클러스터 자체를 바꾸는 일은 분기에 한 번 있을까 말까다. 대신 잘못 건드리면 클러스터 전체가 날아간다.
  • 플랫폼 애드온: 운영 중 튜닝이 종종 발생한다. Karpenter NodePool 변경, KEDA 스케일링 정책 조정 등. 주에 한두 번 정도.
  • 애플리케이션: 매일 수십 번 배포된다. AI Agent 로직 개선, 프롬프트 튜닝, 의존성 업데이트.

2. 권한과 책임 경계가 다르다

회사의 부서 R&R과도 연관이 있다. 이건 우리 부서만 사용하는 에이전트 개발 프로젝트이기 때문에 위 코드를 전부 관리할 권한이 있지만, 실제 대고객 서비스의 경우는 각각의 레포지토리를 관리하는 부서가 전부 다르고, 권한도 제한되어 있다. GitOps의 전사 도입을 계획하고 있기 때문에 실제로 회사에서 운영하는 방식과 최대한 유사하게 구성했다.

 

  • terraform-infra는 플랫폼/인프라 엔지니어가 다룬다. AWS 권한이 광범위하게 필요하고, 변경 시 승인 프로세스가 강해야 한다.
  • fleet-infra는 DevOps/플랫폼 엔지니어가 다룬다. 클러스터 내부 권한이 필요하지만, AWS 리소스 자체를 바꾸진 않는다.
  • application은 개발팀이 다룬다. 애드온 설정이나 인프라를 건드릴 일이 없어야 한다.

 

 

3. FluxCD의 다중 소스 모델과 잘 맞는다

# fleet-infra용 GitRepository
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: fleet-infra
  namespace: flux-system
spec:
  interval: 1m
  ref:
    branch: main
  url: https://github.com/myorg/fleet-infra
---
# application용 GitRepository
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: application
  namespace: flux-system
spec:
  interval: 30s
  ref:
    branch: main
  url: https://github.com/myorg/application

 

 

4. 부트스트랩 순서가 자연스럽다

terraform-infra (AWS 리소스 + FluxCD 설치)
        ↓
fleet-infra (FluxCD가 플랫폼 애드온 동기화)
        ↓
application (애드온 위에서 애플리케이션 동작)

이 순서가 깨지면 시스템이 동작하지 않는다. 예를 들어 Karpenter가 배포되기 전에 애플리케이션이 떴다면 파드는 노드를 못 잡고 Pending 상태에 빠진다. 레포 분리가 곧 의존성 명시를 의미한다.

 

전체 아키텍처 개요

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   ┌──────────────────┐                                      │
│   │ terraform-infra  │  ── AWS 리소스 + FluxCD 부트스트랩    │
│   └────────┬─────────┘                                      │
│            │ provisioning                                   │
│            ▼                                                │
│   ┌──────────────────┐      ┌──────────────────┐            │
│   │   EKS Cluster    │ ◄─── │   fleet-infra    │            │
│   │                  │ sync │ (플랫폼 애드온)   │            │
│   │  ┌───────────┐   │      └──────────────────┘            │
│   │  │  FluxCD   │   │                                      │
│   │  └─────┬─────┘   │      ┌──────────────────┐            │
│   │        │         │ ◄─── │   application    │            │
│   │        └─────────┼──────│   (AI Agent)     │            │
│   └──────────────────┘ sync └──────────────────┘            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

 

Terraform으로 인프라 및 FluxCD 부트스트랩을 수행하고, 이후 플랫폼 애드온이나 어플리케이션 배포는 전부 FluxCD를 통해 이루어 지는 아키텍처로 구성했다.

 

 

완벽한 GitOps의 모순 - 왜 일부는 Terraform으로?

GitOps를 한다고 했으면 모든 K8s 리소스를 Git에 두고 FluxCD로 동기화해야 맞다. 그런데 막상 구축해보면 닭과 달걀 문제에 부딪힌다.

FluxCD가 매니페스트를 동기화하려면, 먼저 FluxCD 자체가 클러스터에 설치되어 있어야 한다. 그런데 FluxCD를 설치하는 매니페스트는 누가 적용할 것인가?

 

부트스트랩 영역은 Terraform으로

내 선택은 FluxCD 자체의 설치, 그리고 FluxCD가 첫 번째로 바라봐야 할 GitRepository / Kustomization 리소스까지는 Terraform으로 관리하는 것이었다.

terraform-infra 레포에는 AWS 리소스 외에도 다음과 같은 K8s 리소스가 Terraform 코드로 들어 있다.

# FluxCD 설치 (Helm)
resource "helm_release" "flux" {
  name             = "flux"
  repository       = "https://fluxcd-community.github.io/helm-charts"
  chart            = "flux2"
  namespace        = "flux-system"
  create_namespace = true
  version          = "2.x.x"
}

# FluxCD가 fleet-infra를 바라보도록 GitRepository / Kustomization 등록
resource "kubernetes_manifest" "fleet_gitrepo" {
  manifest = yamldecode(file("${path.module}/manifests/fleet-gitrepo.yaml"))
  depends_on = [helm_release.flux]
}

resource "kubernetes_manifest" "fleet_kustomization" {
  manifest = yamldecode(file("${path.module}/manifests/fleet-kustomization.yaml"))
  depends_on = [kubernetes_manifest.fleet_gitrepo]
}

 

 

Terraform이 책임지는 것은 딱 두 가지다.

 

  • FluxCD 자체를 클러스터에 설치
  • FluxCD에게 "이 레포(fleet-infra)를 보고 일해라" 라고 지시하는 최소한의 진입점 리소스

이후의 모든 K8s 리소스(Karpenter, ALBC, KEDA, AI Agent) 는 FluxCD가 fleet-infra와 application 레포를 바라보면서 알아서 동기화한다.

 

이전 글에서 Terraform으로 K8S Manifest를 관리하지 않는 이유를 소개했는데 왜?

Terraform은 앞 글에서 언급한대로 다양한 이유로 k8s 를 관리하는데 적절하지 않다.

하지만 부트스트랩 리소스는 다르다.

  • 거의 변경되지 않는다. FluxCD의 Helm chart 버전을 올리거나, 진입점 GitRepository URL을 바꾸는 일은 1년에 몇 번이다.
  • drift가 발생할 일이 없다. 클러스터 운영자가 직접 flux-system 네임스페이스의 리소스를 손으로 건드릴 일이 없다.
  • 부트스트랩 시점에만 필요하다. 애플리케이션 배포 흐름과 완전히 분리되어 있다.

즉, 앞 글에서 비판한 단점들이 부트스트랩 영역에서는 거의 문제가 되지 않는다. 오히려 Terraform이 가진 장점, 즉 AWS 리소스부터 K8s 진입점까지 단일 워크플로우로 묶을 수 있다는 이점이 훨씬 크다.

 

규칙은 단순하다. GitOps 도구가 동작할 수 있는 최소한의 환경까지만 Terraform으로 만들고, 그 이후로는 절대 Terraform이 K8s를 건드리지 않는다.

이 경계를 분명히 그어두지 않으면, 편하다는 이유로 Terraform이 K8s 리소스를 점점 잠식하기 시작한다. 그러면 GitOps는 명목상으로만 남고, 실제로는 Terraform 중심의 push 기반 운영으로 회귀할 수도 있다.

 

아직 해결하지 못한 문제 - Pod Identity와 R&R의 경계

현재 조직 상황과 조건들을 반영하면서 최대한 자연스러운 GitOps 워크플로우를 만들었다고 생각했지만 아직 해결되지 않은 영역이 있다. 바로 Pod의 AWS 권한 부여 문제이다.

 

AI Agent는 Bedrock, S3 등 여러 AWS 리소스에 접근해야 하고, 이를 위해 EKS Pod Identity를 사용한다. 그런데 Pod Identity 구성에 필요한 IAM Role, Policy, 그리고 Pod Identity Association은 여전히 terraform-infra 레포에서 Terraform으로 관리하고 있다. 즉, 앞서 그어둔 경계를 살짝 넘은 상태다.

tf-controller 같은 도구를 쓰면 이걸 K8s manifest처럼 FluxCD로 동기화할 수 있다. 기술적으로는 어렵지 않다. 그런데 여기서 멈칫하게 되는 지점이 있다.

Pod의 권한을 인프라 담당자가 아닌 사람이 멋대로 수정할 수 있다는 게 맞는가?

 

지금 구조에서는 IAM 코드가 terraform-infra에 있기 때문에, 새 권한이 필요하면 인프라팀의 PR 리뷰를 거쳐야 한다. 권한이라는 민감한 영역에 자연스러운 review gate가 존재하는 셈이다.

만약 IAM을 application 레포로 옮긴다면, 개발자가 자기 PR에 IAM Policy 변경을 끼워 넣을 수 있게 된다. FluxCD는 그걸 그대로 reconcile한다. 사고가 났을 때 책임 소재도 애매해진다. GitOps의 완성도를 위해 R&R(권한 분리)이라는 보안의 기본 원칙을 무너뜨리는 trade-off가 발생하는 것이다.

 

GitOps를 도입할 때 흔히 "모든 것을 GitOps로"라는 욕심이 생기는데, 그러다 보면 권한 분리나 책임 소재 같은 다른 가치들과 충돌한다. 어디까지 GitOps에 맡기고 어디부터는 다른 방식으로 통제할 것인가 그 경계를 그리는 것이 결국 플랫폼 엔지니어링의 일이라는 걸, 이 프로젝트 환경을 구축하면서 느낄 수 있었다.

 


let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });