0. Contents
1. Overview
이번 편은 가운데 두 층 — EKS 와 ArgoCD 다
앞 두 편과 이번 편
- 01편과 02편은 권한 이야기였다
- 레포를 쓰기 권한으로 나눴다
- Terraform 을 PR 댓글로 승인했다
- 배포 로봇이 넘을 수 없는 경계를 그었다
- 이번 편은 그 권한 위에 실제로 무언가를 세우는 이야기다
장별 내용
- 2장: EKS 는 PR 하나로 등록했지만
/apply는 세 번 했다- 출구 비용이 워커보다 비싸서 NAT 를 바꿨다
- 미리 바꿔 둔 입력값이 엉뚱한 plan 에 섞일 뻔했다
- 3장: ArgoCD 는 처음 한 번만 밖에서 넣고, 나머지는 지시서까지 Git 에 두었다
2. EKS — 세 번 만에 선 클러스터
2.1 NAT — 워커보다 비싼 출구
처음 설계: NAT Gateway
- 노드는 공인 IP 없이 private subnet 에 둔다
- 이미지를 받으려면 밖으로 나가는 출구가 필요하다
- 처음 설계는 NAT Gateway 였다
- 서울 리전 가격으로 계산해 보니 이상한 숫자가 나왔다
출구 하나가 워커 한 대보다 비쌌다 — 점선을 넘는 막대가 NAT Gateway 다
fck-nat 로 교체
- 워커 노드(t3a.medium 1대)가 월 $34 정도인데, 출구가 워커보다 비쌌다
- EC2 한 대를 NAT 로 쓰는 fck-nat 로 바꿨다
- 대가는 가용성이다
- 한 대뿐이라 인스턴스가 교체되는 동안 밖으로 나가는 통신이 끊긴다
- Auto Scaling 그룹이 같은 ENI 와 EIP 를 다시 붙이므로 라우트는 바뀌지 않는다
노드는 공인 IP 없이 NAT 인스턴스를 거쳐 나간다 — 한 대라 교체 중에는 바깥 통신이 끊긴다
NAT 이미지: 직접 구운 AMI
- NAT 인스턴스의 OS 이미지는 공개 AMI 대신 이 계정에서 Packer 로 직접 구웠다
- 목적: 인스턴스가 교체될 때 검토하지 않은 이미지로 부팅하지 않게 한다
- 굽는 김에 두 가지를 더했다
- EIP 연결이 실패하면 약 2분간 재시도한다
- NAT Gateway 처럼 TCP MSS 를 1460 으로 낮춘다
ECR 수명 규칙
- 같은 이유로 ECR 에도 수명 규칙을 걸었다
prod-태그: 6개만 남긴다dev-태그: 4개만 남긴다- 태그 없는 이미지: 하루만 남긴다
- 규칙이 없으면 이미지는 지우지 않는 한 계속 쌓인다 → 저장 비용도 같이 쌓인다
2.2 Three Applies — 실패는 되돌려지지 않고 이어진다
PR 하나, apply 세 번
- EKS 는 PR 하나로 등록했지만
/apply는 세 번 했다
PR 하나, /apply 세 번 — Terraform 은 실패해도 되돌리지 않고, 다음 견적서가 남은 칸을 채운다
두 실패 비교
- 두 실패를 한 장에 놓으면 이렇다
1차는 너무 빨라서, 2차는 너무 꽉 묶어서 — 둘 다 PR 댓글엔 '명령이 실패했습니다' 한 줄뿐이었다
1차 실패: IAM 최종 일관성
- NAT 모듈이 instance profile 을 만들자마자 그걸 쓰는 인스턴스를 띄웠다
- 권한이 아직 퍼지지 않아 인스턴스 시작이
Authentication Failure로 실패했다 - Terraform 은 그 Auto Scaling 그룹을 tainted 로 표시했다 → 다음 plan 이 교체로 잡아 해결했다
2차 실패: 너무 좁게 잡은 권한
- EKS 는 노드 그룹을 만들 때 노드 그룹용 서비스 연결 역할이 있는지 호출자 권한으로 먼저 조회한다
- 그 역할이 계정에 아직 없었다
- apply 역할의
iam:GetRole은 클러스터 역할 6개로만 묶여 있었다 - 오류는 이랬다
Failed to validate if SLR: AWSServiceRoleForAmazonEKSNodegroup already exists
due to missing permissions for 'iam:GetRole'
조치: 역할 하나에만 iam:GetRole 추가
- 그 역할 하나에 대한
iam:GetRole만 foundation 에 더했다 - 역할이 아직 없을 때 IAM 이 어떤 ARN 으로 평가하는지는 확인하지 못했다 → 경로가 있는 ARN 과 없는 ARN 을 둘 다 적었다
- CloudTrail 의 거부 기록에는 경로 없이 이름만 남아 있었다
결과 댓글: 리소스별 표시
- 두 실패 모두 PR 댓글만 보고는 원인을 알 수 없었다
- 실패 댓글에는 “Terraform/AWS 명령이 실패했습니다” 한 줄뿐이었다
- 이후 결과 댓글을 리소스별로 바꿨다. 보여 주는 것은 다음뿐이다
- 성공·실패한 리소스 주소
- AWS 오류 코드
- 고정된 분류(권한 부족, 전파 지연 가능 등)
- ARN, IP, 오류 원문은 여전히 내보내지 않는다
2.3 Shared Input — 미리 바꾼 Secret 은 다음 plan 에 섞인다
미리 넣어 둔 관리자 IP
- 두 번째 관리자의 IP 를
KUKIE_ADMIN_CIDRS에 미리 넣어 두었다 - 출입증 PR(02편 4장 ②) 때 같이 반영될 거라 생각했다
- 리뷰가 짚은 함정은 이랬다
Secret 갱신 (코드 diff 없음)
└─▶ 다음 eks-platform plan 전부에 aws_eks_cluster.this 변경 1 이 붙는다
├─ 출입증 PR 이 먼저면 의도대로
└─ 다른 PR(예: 버킷)이 먼저면 그 PR diff 에 없는 IP 변경이 몰래 적용된다
원인: diff 밖의 입력값
- 코드 리뷰는 diff 를 본다
- 입력값은 diff 에 나오지 않는다
- 조치: README 에 “출입증 PR 보다 먼저 다른 eks-platform PR 을
/apply하지 않는다” 를 적었다- 출입증 PR 을 적용한 뒤 그 문장을 지우는 것까지 할 일로 남겼다
- 한계: 코드로 막는 장치는 없고 문장으로 막은 것이다
- 다음에는 입력값을 바꾸는 시점을 그 값을 쓰는 PR 직전으로 붙이는 편이 낫다
3. ArgoCD — 처음 한 번만 밖에서 넣는다
3.1 Bootstrap — 관리인은 누가 들이나
ArgoCD 의 동작 방식
- ArgoCD 는 클러스터 안에서 Git 을 읽어 클러스터를 Git 대로 맞춘다
- 안에서 밖으로 가져오는 방식이다 → 한번 들어가면 밖에서 클러스터로 들어올 일이 없다
- 그래서 02편 5장의 IP 제한과 출입 명부에 걸리지 않는다
문제: ArgoCD 자신의 설치
- 빈 클러스터에는 Git 을 읽어 줄 것이 없다
- 누군가는 밖에서 들어가 ArgoCD 를 설치해야 한다 — 그건 출입 명부에 있는 사람이다
- 그래서 ArgoCD 설치 root 는 댓글 배포 대상에서 빼고 관리자가 로컬에서 적용한다
관리인은 한 번만 밖에서 들이고, 그다음부터는 안에서 Git 을 읽어 클러스터를 맞춘다
# terraform/argocd-bootstrap — Terraform 이 helm install 을 대신 한다
resource "helm_release" "argocd" {
namespace = "argocd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
values = [file("argocd-values.yaml")] # 노드 1대용: 복제 1개씩, UI 비공개
}
resource "helm_release" "argocd_apps" {
chart = "argocd-apps"
values = [yamlencode({
projects = local.projects
applications = var.enable_gitops ? local.root : {} # ★ 스위치
})]
depends_on = [helm_release.argocd]
}
두 번에 나눈 설치
- 1차: ArgoCD 와 프로젝트만 만든다
- 관리자가 ArgoCD 화면에서 레포 읽기 전용 토큰을 등록한다
- 2차:
enable_gitops = true로 상위 지시서를 넣는다
- 비공개 레포를 읽을 수 없는 상태로 지시서부터 주면 ArgoCD 는 오류만 낸다
- 토큰은 Terraform 변수에 넣지 않는다 — state 에 남기 때문이다
함정: 한 release 안의 CRD
- 처음에는 ArgoCD 차트와 AppProject 를 같은 Helm release 에 넣었다
- argo-cd 차트는 CRD 를 일반 템플릿으로 설치한다
- 첫 설치 때 Helm 은 아직 존재하지 않는
AppProject종류를 매핑하지 못해 실패한다atomic이라 통째로 롤백된다
- 해결:
argocd-apps를 별도 release 로 떼고depends_on으로 순서를 잡았다
3.2 App of Apps — 지시서도 Git 에 둔다
지시서(Application)의 네 항목
- ArgoCD 는 지시서(Application)가 없으면 아무 일도 하지 않는다
- 지시서에는 네 가지를 적는다
kind: Application
spec:
project: kukie-apps # 어떤 울타리 안에서
source: # 무엇을 보고
repoURL: https://github.com/durokukie/kukie-manifest.git
targetRevision: main
path: envs/dev
destination: # 어디에
server: https://kubernetes.default.svc
namespace: kukie-dev
syncPolicy: # 어떻게 맞출지
automated: {prune: true, selfHeal: true}
지시서도 Git 에
- 지시서도 쿠버네티스 리소스다
- 그래서 지시서를 Git 에 두면 ArgoCD 가 다른 리소스처럼 읽어서 만든다
- 사람이 넣는 것은 맨 위 지시서 하나뿐이다
사람이 넣는 건 맨 위 하나 — 나머지 층은 Git 에서 생기고, 지워져도 되살아난다
손으로 넣을 때와 Git 에 둘 때
- 손으로
kubectl apply한 지시서- 누가 언제 넣었는지 남지 않는다
- 지워지면 다시 생기지 않는다
- 클러스터를 새로 만들면 무엇을 넣었었는지 기억해야 한다
- Git 에 둔 지시서
- 커밋이 곧 기록이다
- root 하나만 다시 넣으면 나머지가 줄줄이 복구된다
대가: 병합이 곧 반영
- 이 구조의 대가는 01편 3.1의 댓글 승인과 정반대의 성질이다
- GitOps 를 켠 뒤에는
argocd/dev/platform을 바꾼 PR 이 main 에 병합되는 순간 반영된다 —/apply가 없다 - 그래서 이 폴더의 PR 은 병합 전에 검사한다
- 렌더링 결과를 검사한다(
check-gitops.rb) - 지시서 목록이 정확히 맞는지까지 확인한다
- 렌더링 결과를 검사한다(
- 이유: root 가 prune 을 켜 두었다 → 차트에서 지시서 하나가 빠지면 그 설비가 클러스터에서 지워진다
3.3 Platform Controllers — 문, 열쇠, 새로고침
공통 설비 세 가지
- root 가 깔아 주는 공통 설비는 셋이다
- 셋 다 앱이 클러스터 밖의 AWS 와 이어지게 도와준다
문(LB Controller), 열쇠(External Secrets), 새로고침(Reloader)
External Secrets 의 범위
kukie-dev/*이름의 Secret 만 읽을 수 있는 IRSA 역할을 쓴다kukie-dev네임스페이스에서만 일하도록 범위를 묶었다- Git 에는 “금고의 몇 번 칸을 가져와라” 는 위치만 적힌다
- 쿠버네티스 Secret 은 base64 일 뿐이다 → 이 구조에서 실제로 지켜야 하는 것은 누가 클러스터에서 Secret 을 읽을 수 있느냐다
- 02편 4장에서 관리자를 늘릴 때 두 단계를 거친 이유이기도 하다
ALB 의 함정: 보안 그룹 Name 태그
- kukie-manifest 의 Ingress 는 보안 그룹을 이름(
kukie-dev-internal-alb)으로 지정한다 - LB Controller 는 이름을
Name태그로 찾는다 - Terraform 의
name인자만으로는 태그가 붙지 않는다 - 앱을 붙이기 전에 태그 한 줄을 더해 두었다
3.4 kukie-apps — 앱 쪽 울타리
설비 울타리와 앱 울타리
- 설비용 프로젝트
kukie-platform은 CRD 와 클러스터 RBAC 까지 설치해야 한다 → 클러스터 전체 권한이다 - 앱을 여기에 올리면 앱 YAML 을 쓰는 사람이 클러스터 전체를 건드릴 수 있다
- 그래서 앱에는 별도의 좁은 울타리를 둔다
kind: AppProject
metadata:
name: kukie-apps
spec:
sourceRepos: [https://github.com/durokukie/kukie-manifest.git] # 이 레포만
destinations: [{namespace: kukie-dev}] # 이 네임스페이스만
clusterResourceWhitelist: [] # 클러스터 범위 리소스 금지
namespaceResourceWhitelist: # 여섯 종류만
- {group: apps, kind: Deployment}
- {group: "", kind: Service}
- {group: "", kind: ConfigMap}
- {group: networking.k8s.io, kind: Ingress}
- {group: external-secrets.io, kind: ExternalSecret}
- {group: external-secrets.io, kind: SecretStore}
허용 목록에 없는 Secret
- 허용 목록에 Secret 이 없다는 점을 눈여겨볼 만하다
- kukie-manifest 로는 비밀 값을 직접 만들 수 없다
- ExternalSecret 으로 금고 위치를 가리키는 것만 된다
앱 쪽에는 좁은 울타리를 따로 둔다 — 울타리 정의는 관리자 레포에, 울타리 안의 내용만 앱 레포에
지시서의 위치: kukie-infra
- 이 지시서도 kukie-infra 에 둔다(01편 2장)
- kukie-manifest 는 앱 CI 봇이 쓰는 레포고, 무료 플랜이라 브랜치 보호가 없다
- 지시서까지 거기 있으면 봇이 project 필드를
kukie-platform으로 바꾸는 것만으로 울타리를 넘는다 - 나누는 기준
- 울타리와 지시서: 관리자 레포
- 울타리 안의 내용: 앱 레포
Draft 로 묶어 둔 연결 PR
- 이 연결은 PR 로 만들어 두었지만 Draft 로 묶어 두었다
- 병합하는 순간 ArgoCD 가 배포를 시작하는데, 아직 준비가 덜 됐다
- 이미지 digest 가
pending이다 - 회원 서버의 Secret 값과 DB 접근 허용이 없다
- 이미지 digest 가
- 빈 가게를 한꺼번에 열면 빨간불만 켜진다
- 순서: 회원 서버와 Redis 를 먼저 켜고, 에이전트와 화면은 이미지가 준비되는 대로 붙인다
4. Discussion
EKS: 실패가 남긴 것
- EKS 는 한 번에 서지 않았다
- 다만 실패할 때마다 만든 것은 남았고, 다음 견적서가 남은 칸만 채웠다
- 실패를 리소스별로 보여 주도록 결과 댓글을 바꾼 것이 가장 오래 남을 수확이다
ArgoCD: 반대 방향의 교훈
- 한번 들이면 병합이 곧 적용이 된다 → 지시서 목록과 울타리를 병합 전에 검사해야 한다
남은 일
- ArgoCD 를 실제로 설치한다
- 회원 서버부터 앱을 올린다
- CloudFront 로 HTTPS 입구를 연다
노드 용량: Pod 17개 한도
- 노드 1대는 VPC CNI 기본값으로 Pod 17개가 한도다
- 시스템 Pod 4개 + ArgoCD 4개 + 설비 5개에 앱 4개를 올리면 꽉 찬다
- LGTM 을 올리기 전에 노드부터 키워야 한다
핵심 메시지
NAT
출구가 워커보다 비쌌다. EC2 한 대로 바꾸고 교체 중 끊김을 받아들였다
세 번의 apply
실패는 되돌려지지 않고 이어진다. 오류를 리소스별로 보여야 고친다
입력값
diff 에 안 나오는 Secret 은 다음 plan 에 섞인다
ArgoCD
처음 한 번만 밖에서 넣고, 지시서까지 Git 에 둔다
울타리
앱에는 좁은 프로젝트. 정의는 관리자 레포에
클러스터를 세우는 일은 한 번에 끝나지 않았고, 그래서 실패가 무엇을 남기는지가 중요했다. ArgoCD 를 들인 뒤부터는 반대로, 병합 한 번이 곧 배포다. 다음 편은 이 위에 회원 서버부터 앱을 올리고, 바이브코더의 코드를 받아 줄 호스팅 클러스터를 설계하는 이야기다.
5. References
EKS 출입 항목이 무엇인지는 EKS 사용 설명서의 개요 첫 문단에 있다.
"Fundamentally, an EKS access entry associates a set of Kubernetes permissions with an IAM identity, such as an IAM role."
[해석] "근본적으로 EKS 출입 항목은 쿠버네티스 권한 묶음을 IAM 역할 같은 IAM 자격 증명에 연결한다." — 3.1 에서 ArgoCD 설치가 출입 명부에 있는 사람 몫이 되는 근거다.
App of Apps 의 상위 레포를 누가 써야 하는지는 Argo CD 의 Cluster Bootstrapping 문서에 경고로 적혀 있다.
"The ability to create Applications in arbitrary Projects is an admin-level capability. Only admins should have push access to the parent Application's source repository."
[해석] "임의의 프로젝트에 Application 을 만들 수 있는 능력은 관리자 수준의 권한이다. 상위 Application 의 소스 레포에는 관리자만 push 할 수 있어야 한다." — 3.4 에서 kukie-apps 지시서를 kukie-manifest 가 아니라 kukie-infra 에 둔 근거다.
