시작하며
최근 프로젝트에서 마이크로서비스 아키텍처를 EKS로 이전하면서, GitOps를 도입하기로 결정했습니다. 하지만 막상 ArgoCD를 설치하려고 하니 예상치 못한 문제들이 하나둘 나타나기 시작했어요.
"ArgoCD가 시작하려면 argocd-secret이 필요한데, 이 시크릿은 ExternalSecret이 만들어야 하고, ExternalSecret이 동작하려면 External Secrets Operator가 필요하고, ESO를 ArgoCD로 관리하려면... 어? 순환 의존성이네?"
오늘은 이런 복잡한 의존성 문제를 어떻게 해결했는지, 그리고 Terraform과 ArgoCD의 역할을 어떻게 나누어 Single Sync를 유지했는지 공유해보려고 합니다.
문제 상황: 닭이 먼저냐 달걀이 먼저냐
초기 계획의 함정
처음 계획은 단순했습니다:
- Terraform으로 EKS 클러스터 생성
- ArgoCD 설치
- 모든 애플리케이션을 ArgoCD로 관리
하지만 실제로 구현하려니 순환 의존성 문제가 발생했어요.
graph LR
A[ArgoCD 시작] --> B[argocd-secret 필요]
B --> C[ExternalSecret이 생성]
C --> D[ESO 필요]
D --> E[ArgoCD로 ESO 설치?]
E --> A
ArgoCD는 argocd-secret이 있어야 시작되는데, 이 시크릿을 만들려면 이미 ArgoCD가 동작하고 있어야 한다는 모순이었죠.
추가로 고려해야 할 것들
- 보안: AWS Secrets Manager에 저장된 OAuth 정보를 안전하게 가져오기
- 자동화: Terraform 재배포 후에도 모든 설정이 자동으로 복구되어야 함
- Single Sync: 하나의 진실 공급원(Git)에서 모든 것을 관리
- 순서 제어: 인프라 → 애드온 → 애플리케이션 순으로 배포
해결 방법: 역할 분담과 Bootstrap 전략
1. Terraform vs ArgoCD 역할 분담
문제를 해결하기 위해 최소한의 Bootstrap만 Terraform에서 하고, 나머지는 모두 ArgoCD에 맡기기로 했습니다.
Terraform 관리 범위 (Bootstrap만)
# terraform/prod/compute/helm-releases.tf
# 순환 의존성 해결을 위한 최소한의 설치
1. EKS 클러스터
2. Gateway API CRDs
3. Istio (Base + Istiod)
4. External Secrets Operator ← 핵심!
5. ClusterSecretStore
6. argocd-secret (ExternalSecret으로)
7. ArgoCD Helm 설치
8. 초기 AppProject + Root Application
ArgoCD 관리 범위 (모든 애플리케이션)
# k8s/argocd/apps/prod/ 디렉토리
- 클러스터 애드온들 (ALB Controller, External DNS 등)
- ArgoCD 자체 설정 (GitOps로!)
- 마이크로서비스들
- Istio 설정
- 모니터링 스택
2. 순환 의존성 해결: ESO 먼저 설치
핵심은 External Secrets Operator를 ArgoCD보다 먼저 Terraform에서 설치하는 것이었습니다.
# terraform/prod/compute/helm-releases.tf
# =============================================================================
# 3. External Secrets Operator (ArgoCD보다 먼저 설치 - Bootstrap 필수)
# =============================================================================
resource "helm_release" "external_secrets" {
name = "external-secrets"
repository = "https://charts.external-secrets.io"
chart = "external-secrets"
version = "1.2.0"
namespace = "external-secrets"
create_namespace = true
# CRDs 설치
set {
name = "installCRDs"
value = "true"
}
depends_on = [
helm_release.istiod,
module.pod_identity_external_secrets # Pod Identity 연결
]
}
3. argocd-secret 자동 생성
ESO가 설치되면, ArgoCD 시작 전에 필요한 시크릿을 자동으로 생성할 수 있습니다.
# terraform에서 kubectl로 적용
resource "kubectl_manifest" "argocd_external_secret" {
yaml_body = <<-YAML
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: argocd-oauth-secret
namespace: argocd
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: argocd-secret # ArgoCD가 찾는 시크릿 이름
creationPolicy: Owner
template:
data:
server.secretkey: "{{ .server_secretkey }}"
dex.google.clientID: "{{ .dex_google_clientID }}"
dex.google.clientSecret: "{{ .dex_google_clientSecret }}"
data:
- secretKey: server_secretkey
remoteRef:
key: "wealist/prod/argocd/server"
property: secretkey
- secretKey: dex_google_clientID
remoteRef:
key: "wealist/prod/oauth/argocd"
property: client_id
- secretKey: dex_google_clientSecret
remoteRef:
key: "wealist/prod/oauth/argocd"
property: client_secret
YAML
depends_on = [kubernetes_namespace.argocd]
}
4. ArgoCD 설치 시 중요한 설정
# ArgoCD Helm 설치 시 핵심 설정
resource "helm_release" "argocd" {
name = "argocd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
version = "5.55.0"
namespace = "argocd"
create_namespace = false # 이미 생성됨
# ⚠️ 중요: ArgoCD가 자체 시크릿을 만들지 않도록 설정
set {
name = "configs.secret.createSecret"
value = "false" # ExternalSecret이 관리하므로
}
# argocd-secret이 먼저 생성되어야 ArgoCD가 시작 가능
depends_on = [time_sleep.wait_for_argocd_secret]
}
App of Apps 패턴으로 모든 것을 GitOps로
Root Application 구조
ArgoCD가 설치되면, App of Apps 패턴으로 모든 애플리케이션을 관리합니다.
# k8s/argocd/root-apps/prod.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: wealist-apps-prod
namespace: argocd
spec:
project: wealist-prod
source:
repoURL: https://github.com/OrangesCloud/wealist-project-advanced-k8s.git
targetRevision: k8s-deploy-prod
path: k8s/argocd/apps/prod # 이 디렉토리의 모든 Application을 관리
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Sync Wave로 배포 순서 제어
복잡한 의존성을 sync-wave 어노테이션으로 해결했습니다.
# k8s/argocd/apps/prod/argocd-config.yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-10" # 가장 먼저
# k8s/argocd/apps/prod/cluster-addons.yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-6" # 애드온들
# k8s/argocd/apps/prod/external-secrets.yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "1" # ExternalSecret 리소스들
# k8s/argocd/apps/prod/infrastructure.yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "2" # ConfigMap, PVC
# k8s/argocd/apps/prod/auth-service.yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "5" # 마이크로서비스들
핵심 인사이트: ArgoCD 자체도 GitOps로 관리하기
가장 중요한 깨달음
ArgoCD 자체 설정도 Git으로 관리해야 한다는 것이었습니다. 그렇지 않으면 Terraform을 재배포할 때마다 수동으로 설정을 다시 해야 하거든요.
# k8s/argocd/apps/prod/argocd-config.yaml
# ArgoCD가 자기 자신의 설정을 관리!
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: argocd-config
namespace: argocd
spec:
project: wealist-prod
sources:
# ArgoCD ConfigMap 설정 (health check, SSO 등)
- repoURL: https://github.com/OrangesCloud/wealist-project-advanced-k8s.git
targetRevision: k8s-deploy-prod
path: k8s/argocd/config
# Discord 알림 설정
- repoURL: https://github.com/OrangesCloud/wealist-project-advanced-k8s.git
targetRevision: k8s-deploy-prod
path: k8s/argocd/notifications
# 정책 설정
- repoURL: https://github.com/OrangesCloud/wealist-project-advanced-k8s.git
targetRevision: k8s-deploy-prod
path: k8s/argocd/kyverno/prod
실제 ArgoCD 설정 예시
# k8s/argocd/config/argocd-cm-patch.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
# Google OAuth SSO 설정
dex.config: |
connectors:
- type: google
id: google
name: Google
config:
clientID: $dex.google.clientID # argocd-secret에서 주입
clientSecret: $dex.google.clientSecret
redirectURI: https://argocd.wealist.co.kr/api/dex/callback
# 커스텀 Health Check
resource.customizations.health.external-secrets.io_ExternalSecret: |
hs = {}
if obj.status ~= nil then
if obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "False" then
hs.status = "Degraded"
hs.message = condition.message
return hs
end
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"
hs.message = "ExternalSecret is ready"
return hs
end
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for ExternalSecret"
return hs
실제 적용 결과
배포 프로세스
이제 전체 배포 프로세스는 다음과 같습니다:
# 1. Terraform으로 EKS + ArgoCD Bootstrap
cd terraform/prod/compute
terraform apply
# 2. ArgoCD가 자동으로 모든 애플리케이션 배포
# (수동 작업 없음!)
# 3. 애플리케이션 업데이트는 Git Push만으로
git add .
git commit -m "Update user-service to v1.2.3"
git push origin k8s-deploy-prod
# ArgoCD가 자동으로 감지하고 배포
장점들
1. 완전한 GitOps
- 모든 설정이 Git에 있음
- 변경 이력 추적 가능
- 롤백 간단
2. 자동 복구
- Terraform 재배포 후에도 ArgoCD가 모든 설정 자동 복구
- 수동 작업 최소화
3. 명확한 의존성 관리
- sync-wave로 배포 순서 보장
- 각 애플리케이션의 상태를 ArgoCD UI에서 확인 가능
배운 점과 주의사항
핵심 교훈
1. Bootstrap은 최소한으로
- Terraform에서는 정말 필요한 것만 설치
- 나머지는 모두 ArgoCD에 맡기기
- (기업별로 어디에 중점을 더 두냐에 따라서 엔지니어의 역할이 달라질수 있다고 생각합니다.)
2. 순환 의존성은 미리 파악하기
- 의존성 그래프를 그려보고 시작
- 어떤 것을 먼저 설치해야 하는지 명확히 하기
3. ArgoCD 자체도 GitOps 대상
- ArgoCD 설정도 Git으로 관리해야 진정한 GitOps
- 수동 설정은 언젠가 문제가 됨
주의사항
1. CRD 설치 타이밍
# CRD가 완전히 등록될 때까지 대기 필요
resource "time_sleep" "wait_for_eso_crds" {
depends_on = [helm_release.external_secrets]
create_duration = "30s" # 충분한 대기 시간
}
2. kubectl_manifest vs kubernetes_manifest
# kubernetes_manifest는 plan 시 CRD 검증을 하므로
# CRD가 아직 없으면 실패함
# kubectl_manifest는 apply 시에만 검증하므로 안전
resource "kubectl_manifest" "cluster_secret_store" {
yaml_body = <<-YAML
apiVersion: external-secrets.io/v1 # CRD 필요
kind: ClusterSecretStore
YAML
}
3. ArgoCD Health Check 설정
- ExternalSecret, ClusterSecretStore 등의 커스텀 리소스는 기본 Health Check가 없음
- 직접 정의해야 sync-wave가 제대로 동작함
마치며
EKS에서 ArgoCD를 구축하면서 가장 어려웠던 부분은 **"어디까지 Terraform에서 하고, 어디서부터 ArgoCD에 맡길 것인가"**를 결정하는 것이었습니다.
결국 답은 최소한의 Bootstrap만 Terraform에서 하고, 나머지는 모두 GitOps로 관리하는 것이었어요. 특히 순환 의존성 문제는 External Secrets Operator를 먼저 설치하는 것으로 깔끔하게 해결할 수 있었습니다.
이제는 새로운 서비스를 추가하거나 설정을 변경할 때 Git에 커밋만 하면 되니까, 정말 편해졌습니다. 무엇보다 **"Infrastructure as Code"**가 아니라 **"Infrastructure as Git"**이 된 느낌이에요.
여러분도 비슷한 고민을 하고 계신다면, 이 글이 조금이나마 도움이 되길 바랍니다. 궁금한 점이나 다른 접근 방법이 있으시면 댓글로 공유해주세요!
참고 자료
'Network' 카테고리의 다른 글
| Kind 클러스터에서 SandboxChanged 에러 해결하기: systemd와 containerd의 갈등 해소 (1) | 2026.01.20 |
|---|---|
| Istio로 대규모 트래픽 관리하기: 실전 적용 사례와 확장 전략 (2) | 2026.01.10 |
| 실전 Kubernetes 보안: 4C 모델을 프로덕션 클러스터에 적용하며 배운 것들 (0) | 2026.01.04 |
| GitHub Actions Docker CI 파이프라인 구조 이해하기: 멀티 아키텍처 빌드부터 캐시 전략 (1) | 2026.01.03 |
| SealSecret vs AWS Secret Manager (0) | 2026.01.02 |