0. Contents
1. Overview
이전 편과 이번 편
ArgoCD 가 하는 일
- 이미지를 굽지 않는다
- Git 에 적힌 쿠버네티스 YAML 을 읽어서 클러스터의 상태와 비교하고, 다르면 맞춘다 — 그게 전부다
- ALB 를 만들거나 Secrets Manager 에서 값을 꺼내는 일은 다른 컨트롤러가 한다
- ArgoCD 가 넣은 객체를 읽은 컨트롤러다
진행 상황(2026-10-15 기준)
- 끝난 단계: 설치, 저장소 자격증명, 플랫폼 GitOps
- 앱 저장소 연결: 코드만 병합된 채 저장소 등록을 기다린다(9장 그림)
2. Source of Truth — ArgoCD가 읽는 두 저장소
ArgoCD 가 읽는 지시서 두 권
argocd/dev/platform공통 컨트롤러, namespace, 앱 연결 정의
kukie-dev-platform (root)kukie-platformenvs/devweb, member, agent, redis, Ingress
kukie-dev-appskukie-appsroot Application 과 kukie-dev-apps
- root Application 은 kukie-infra 를 읽고 그 안의 Application 들을 만든다(App of Apps, 03편 3.2)
- 이번에 그 목록에
kukie-dev-apps가 하나 더 들어갔다 - kukie-manifest 를 보는 것은 이 Application 뿐이다
앱용 Application 의 위치: kukie-infra
- 이유는 쓰기 권한이다
- Application 을 쓸 수 있는 사람은 어느 프로젝트에 넣을지도 고를 수 있다(위키 5.4)
- kukie-manifest 는 앱 CI 봇이 쓰는 저장소다
- 거기에 Application 을 두면 봇이 클러스터 전체 권한의
kukie-platform을 고를 수 있다
- 거기에 Application 을 두면 봇이 클러스터 전체 권한의
- 그래서 앱 쪽 Application 과 그 울타리(
kukie-apps)는 사람이 리뷰하는 kukie-infra 에만 있다
두 프로젝트의 차이
kukie-platformkukie-infra, 컨트롤러 차트 저장소 3곳
argocd, kube-system, external-secrets, kukie-dev
전부 (CRD, ClusterRole 등)
제한 없음
kukie-appskukie-manifest 하나
kukie-dev 하나
없음
Deployment, Service, ConfigMap, Ingress, ExternalSecret, SecretStore
3. Repository Access — 비공개 저장소 읽기 자격증명
읽기 자격증명의 필요
- 두 저장소 모두 비공개다
- ArgoCD 가 3분마다 Git 을 확인하려면 읽기 자격증명이 필요하다
3.1 Token Choice — PAT에서 GitHub App으로
처음 절차: 개인 fine-grained token
- 처음 README 절차(2026-10-14, #35): kukie-infra 하나에 Contents 읽기만 준 개인 fine-grained token
- 그보다 앞선 문서(2026-09-22, #5): “토큰 또는 SSH key” 로 열어 두었다
- 왜 PAT 로 좁혔는지는 커밋에도 PR 에도 남아 있지 않았다
만든 사람 계정
최대 1년
끊김
토큰 하나
HTTPS
저장소
없음
상관없음
키 2개
git@ 로 바꿔야 함조직
없음 (ArgoCD 가 1시간 토큰을 스스로 받음)
상관없음
앱 하나
HTTPS
#46: 조직 소유 GitHub App 으로 전환
- 2026-10-15 의 #46 이 조직 소유 GitHub App 으로 바꿨다
- 이유
- 개인 계정에 묶이지 않는다
- 만료 갱신이 없다
- 주소가 HTTPS 그대로라 root Application 과
sourceRepos를 고칠 필요가 없다
- 처음으로 이유가 적힌 결정이다
자격증명 보관 위치
- ArgoCD 화면에서 등록했다
- 결과는
argocdnamespace 의 repository Secret 하나다 - Terraform 변수나 Git 에는 넣지 않는다 → 넣으면 state 에 private key 가 남는다
3.2 Two Apps — 쓰기 앱과 읽기 앱의 분리
기존 앱 kukie-manifest-bot 과의 분리
- 조직에는 앱이 이미 하나 있었다 — 2026-09-26 에 만든
kukie-manifest-bot - 둘을 합치지 않은 이유: 권한과 열쇠가 놓이는 자리
kukie-manifest-bot앱 레포 3곳의 CI
Contents·Pull requests 쓰기
kukie-manifest
각 레포 Actions Secret, 잡이 돌 때만 사용
durokukie-argocd-devArgoCD
Contents·Metadata 읽기
kukie-infra, kukie-manifest
클러스터 Secret, 항상 존재
클러스터에 두는 열쇠: 읽기 전용
- ArgoCD 가 쓰기 앱의 열쇠를 들면
- 클러스터에서 그 열쇠가 샐 때 kukie-manifest 를 고쳐 아무 이미지나 배포할 수 있다
- 그래서 항상 클러스터에 놓여 있는 열쇠는 읽기만 하게 했다
앱과 설치의 구분
- 앱은 조직에 속하고, 설치는 저장소별 허락이다
- kukie-manifest 를 읽게 하는 일은 새 앱을 만드는 게 아니었다
durokukie-argocd-dev의 설치 범위에 저장소 하나를 더하는 일이었다
4. Sync Loop — 감지와 비교와 적용
4.1 Interval — 3분 주기의 확인
확인 주기와 실제 반영 시간
- 읽기 · 렌더링 · 비교 · 적용의 반복과 확인 주기(최대 3분)는 위키 1절과 9.1에 있다
- 이 클러스터는 주기를 기본값 그대로 두었다
- 플랫폼 쪽 렌더링은 Helm 차트다
- #40 을 병합하고 root Application 이 새 커밋을 읽기까지 실제로 약 1분 반이 걸렸다
GitOps 켜기: root Application 하나
- 이 클러스터에서 “GitOps 를 켰다” 는 건
enable_gitops = true로 root Application 하나를 만든 일이다 - 켜진 시점을 알려 주는 기록(2026-10-15 14:59 KST)
- Helm 릴리스
argocd-apps의 revision 이 1에서 2로 바뀐 기록 - 같은 시각에 생긴 Application 4개
- Helm 릴리스
4.2 Auto and Manual — 플랫폼과 앱의 동기화 방식
OutOfSync 이후: 플랫폼과 앱의 차이
자동 (
prune, selfHeal)바로 Git 대로 맞춘다. 클러스터에서 손으로 바꾼 것도 되돌린다
kukie-dev-apps수동
차이만 보여 준다
앱 쪽 수동 동기화의 이유: 순서
- member 이미지는 아직 ECR 에 없고, Secret 값도 비어 있다
- 자동으로 두면 연결하는 순간 이미지 받기에 실패하는 Pod 가 생긴다
- 수동이면 ArgoCD 가 kukie-manifest 를 읽고 렌더링까지는 한다
- 저장소 연결과 YAML 이 맞는지를 배포 없이 먼저 볼 수 있다
- kukie-manifest 는 비공개라 kukie-infra 의 CI 가 렌더링해 볼 수 없는 저장소이기도 하다
# argocd/dev/platform/templates/kukie-apps.yaml
spec:
project: kukie-apps
source:
repoURL: https://github.com/durokukie/kukie-manifest.git
targetRevision: main
path: envs/dev
syncPolicy: # automated 없음 — 읽고 비교만
syncOptions: [RespectIgnoreDifferences=true]
병합 직후 상태: ComparisonError
- 병합 직후
kukie-dev-apps는ComparisonError였다 - 원인: ArgoCD 에 kukie-manifest 저장소가 아직 등록되지 않아
authentication required를 받았다 - 등록하면 OutOfSync 로 바뀌고 만들 객체 목록이 보인다
- 자동 동기화는 member digest, Secret 값, DB 허용이 끝난 뒤 별도 PR 로 켠다
4.3 Sync Wave — 생성 순서
같은 커밋 안의 생성 순서
- 같은 커밋 안에서도 순서가 있다
argocd.argoproj.io/sync-wave주석이 작은 것부터 만든다
Namespace
kukie-dev, admission policy 2개와 연결앱 Pod 와 Ingress 가 들어오기 전에 검사가 서 있어야 한다
AppProject
kukie-appsApplication 이 고를 울타리가 먼저 있어야 한다
Application 4개
클러스터의 실제 생성 순서
- #40 병합 뒤 클러스터에도 이 순서로 생겼다
- 검사는 7장에서 다룬다
5. Downstream Controllers — ArgoCD 이후의 컨트롤러
객체를 넣은 뒤: 컨트롤러 셋
- ArgoCD 는 객체를 넣는 데까지 한다
- 실제 일은 그 객체를 읽는 컨트롤러가 한다(위키 8절)
- 이 클러스터에서 그 일을 맡는 건 03편 3.3 에서 세운 컨트롤러 셋이다
AWS Load Balancer Controller
AWS 의 ALB
External Secrets Operator
Secrets Manager 값이 담긴 쿠버네티스 Secret
Reloader
그 값을 쓰는 Deployment 재시작
Ingress 담당: LB Controller
- 이 클러스터에서 Ingress(
ingressClassName: alb)를 읽는 건 LB Controller 다 - Ingress 의 기본 개념은 K8sPatterns.13 5.3 에 있다
ALB 의 자리와 수명
- ALB 는 Pod 위에서 돌지 않는다
- AWS 가 운영하는 장비가 private subnet 에 자리를 잡는다
target-type: ip라서 노드를 거치지 않고 Pod IP 로 바로 요청을 보낸다
- Pod 가 바뀌면 LB Controller 가 대상 목록을 고친다
- ArgoCD 는 ALB 가 있는지도 모른다
- 다만 ArgoCD 가 Ingress 를 지우면 LB Controller 가 ALB 를 지운다 → 결과적으로 ALB 의 수명은 Git 을 따라간다
6. Ownership Boundary — 관리 범위의 세 층
Ingress 를 따라 나뉘는 관리 주체 셋
subnet, 보안 그룹, IAM 역할
Terraform
PR, plan,
/applyALB
LB Controller
Ingress 를 따라 자동
Ingress
ArgoCD
Git 병합
subnet: 고르지 않고 가리키기
- subnet 은 ArgoCD 가 관리하지 않는다
- 주문서는 땅을 고르지 않고 가리킬 뿐이다
- 가리키는 방법은 ID 가 아니라 이름과 태그다
이름
kukie-dev-internal-albName 태그가 그 이름인 보안 그룹없음
kubernetes.io/role/internal-elb 태그가 붙은 subnetTerraform 출력 자동 전달 없음
- Terraform 출력을 manifest 로 자동으로 넘기는 장치는 없다
- 보안 그룹 ID 는 다시 만들면 바뀐다
- 값을 넘기려면 Terraform 쪽에 kukie-manifest 쓰기 권한이 생긴다
- 이름과 태그가 약속 역할을 한다
- 대신 이름을 바꾸면 세 곳을 같이 바꿔야 한다 — Terraform, manifest, admission policy
ingress_annotations 출력 정리
- Terraform 의
ingress_annotations출력은 원래 보안 그룹 ID 와 subnet ID 목록을 안내했다- Ingress 를 처음 쓰는 사람이 보라고 만든 메모였다
- 실제 manifest 는 처음부터 이름과 자동 탐색을 썼다
- 7장의 정책이 들어오면서 이 메모대로 쓴 Ingress 는 거부되게 됐다 → 출력을 이름과 자동 탐색으로 맞췄다
- 리소스는 바뀌지 않아 plan 은 생성 0 · 변경 0 · 삭제 0 이었다
7. Admission Guard — 클러스터 입구의 검사
허용 목록의 빈칸: 종류만 본다
kukie-apps의 허용 목록은 종류만 본다 — Deployment 는 허용, ClusterRole 은 금지- 허용된 Deployment 안에 무엇을 적었는지는 보지 않는다
- #40 의 리뷰가 이 빈칸을 두 번 짚었다
검사 위치: API 서버 입구
- ArgoCD 가 보낸 객체는 쿠버네티스 API 서버를 지나 저장된다 → 검사는 그 입구에 둔다
- 규칙이 붙는 곳은 namespace 레이블과 클러스터 범위 객체다
kukie-apps로는 건드릴 수 없다- 바꾸려면 kukie-infra 에 PR 을 내야 한다
7.1 Pod Security — 위험 설정과 baseline
위험 설정: 격리에 낸 구멍
- 컨테이너는 노드의 디스크, 네트워크, 다른 프로세스와 격리돼 있다
- 위험 설정은 그 격리에 스스로 구멍을 내는 설정이다
- 노드 하나를 뚫으면 그 노드의 모든 Pod 와 노드 IAM 권한까지 넘어간다
Pod Security Admission 켜기
- 쿠버네티스에 들어 있는 검사다
- namespace 레이블 한 줄로 켠다
# argocd/dev/platform/templates/namespace.yaml
metadata:
name: kukie-dev
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
baseline 의 정해진 거부 항목
privilegedtrue노드 관리자 권한
hostNetwork, hostPID, hostIPCtrue노드의 네트워크, 프로세스, 공유 메모리
volumes[].hostPath사용
노드 디스크
hostPort0 이 아닌 값
노드 포트
capabilities.add기본 목록 밖 (
SYS_ADMIN 등)사실상 관리자 권한
Unconfined커널 보안 필터
procMount, sysctls기본값 밖
커널 설정
baseline 이 보지 않는 것
- 이 값들을 적지 않으면 기본값이 안전한 쪽이라 그냥 통과한다
- 이미지, 환경변수, 메모리 같은 설정은 보지 않는다
- 지금 manifest 의 앱 넷은 이 항목을 하나도 쓰지 않는다
수준 결정: 실제 클러스터에 dry-run
- 수준은 실제 클러스터에 물어보고 정했다
kubectl label --dry-run=server- 레이블을 바꾸지 않는다
- 바꾸면 기존 Pod 중 무엇이 걸리는지만 돌려준다
- 결과
- baseline: 경고가 없었다
- restricted:
kukie-dev의 Reloader 가allowPrivilegeEscalation != false, unrestricted capabilities로 걸렸다
- 그래서 강제는 baseline, restricted 는 경고와 감사 기록만 남긴다
거부 시점: Pod 를 만드는 순간
- 검사는 Pod 에 걸린다
- Deployment 는 경고만 받고 들어간다
- 그 Deployment 가 Pod 를 만들려는 순간 거부되어
FailedCreate이벤트가 남는다 - 노드 권한이 정말 필요한 도구(노드 지표 수집기 같은 것)는
kukie-dev가 아니라 kukie-infra 가 관리하는 별도 namespace 에 둔다
7.2 Internal ALB — Ingress와 Service 제한
두 번째 빈칸: 로드밸런서
kukie-apps가 허용한 Ingress 와 Service 는 LB Controller 가 AWS 로드밸런서로 만든다- “CloudFront 만 들어오는 internal ALB” 라는 경계는 manifest 의 주석 한 줄이었다
- 쓰기 권한이 새면 그 줄을
internet-facing으로 바꿔 인터넷에 바로 열린 ALB 를 만들 수 있었다
차단 수단: ValidatingAdmissionPolicy
- 쿠버네티스 1.30 부터 기본 기능이다
- 웹훅 서버 없이 CEL 식으로 규칙을 적는다
- Ingress 와 Service 는 필드가 달라 정책을 둘로 나눴다
- 한 정책에 섞으면 서로 없는 필드를 참조한다는 타입 경고가 남는다
# argocd/dev/platform/templates/kukie-dev-admission.yaml (요약)
validations: # kukie-dev-ingress-internal-alb
- expression: "has(object.spec.ingressClassName) && object.spec.ingressClassName == 'alb'"
- expression: "variables.annotations[?'alb.ingress.kubernetes.io/scheme'].orValue('') == 'internal'"
- expression: "variables.annotations[?'alb.ingress.kubernetes.io/security-groups'].orValue('') == 'kukie-dev-internal-alb'"
- expression: "!['alb.ingress.kubernetes.io/subnets', 'alb.ingress.kubernetes.io/customer-owned-ipv4-pool',
'kubernetes.io/ingress.class'].exists(key, key in variables.annotations)"
validations: # kukie-dev-no-load-balancer-service
- expression: "!has(object.spec.type) || object.spec.type != 'LoadBalancer'"
정책 연결과 CI 검사
- 두 정책 모두
failurePolicy: Fail, 연결은validationActions: [Deny]다 kukie-devnamespace 에만 붙는다- 정책 두 개와 Deny 연결, namespace 레이블이 빠지면 kukie-infra 의
check-gitops.rb가 PR 을 실패시킨다
7.3 Verification — kind 클러스터 검증
kind 클러스터 시험: 아홉 가지 요청
- 실제 클러스터에 넣기 전에 로컬 kind 클러스터(Kubernetes 1.36.1)에 같은 정책을 넣었다
- server dry-run 으로 아홉 가지를 보냈다
- 통과: 지금 manifest 의 Ingress, ClusterIP Service, 다른 namespace 의 internet-facing Ingress
- 거부:
internet-facing, 다른 보안 그룹, 다른 ingress class, 옛kubernetes.io/ingress.class주석, subnet 직접 지정,LoadBalancerService
- 처음 한 정책에 Ingress 와 Service 를 섞었을 때 남던 타입 경고는 둘로 나눈 뒤 사라졌다
- 시험이 끝난 뒤 kind 클러스터는 지웠다
실제 클러스터 반영
- #40 병합 뒤 실제 클러스터에도 레이블과 정책 두 개, 연결 두 개가 생겼다
kukie-dev의 Reloader 는 그대로 Running 이었다
8. Operator View — 관리 화면 접속 경로
화면과 API 담당: argocd-server
- ArgoCD Pod 넷의 역할은 위키 2절에 있다
- 그중 화면과 API 를 내는 건
argocd-server다
접속 방법: kubectl port-forward
argocd-server는 ClusterIP 라 클러스터 밖에서 들어올 주소가 없다- 화면은
kubectl port-forward로 본다
kubectl -n argocd port-forward svc/argocd-server 8080:443
port-forward 통로의 경로
- 이 명령은 내 PC 의 8080 포트와 Pod 사이에 통로를 만든다
- 통로는 이미 열려 있는 EKS API 연결 안을 지난다
- 그래서 그 연결에 들어갈 수 있는 사람만 화면을 볼 수 있다
AWS IAM
kubectl 이 AWS 프로필로 받은 토큰 — "이 요청은 IAM 사용자 누구"
EKS API 서버
접속 IP 가 허용 목록에 있는지, 그 IAM 사용자가 access entry 관리자 명단에 있는지
ArgoCD
ArgoCD 자체 계정
adminEKS 의 신원: AWS 신원 그대로
- EKS 는 클러스터 계정을 따로 만들지 않고 AWS 신원을 쓴다
- ①② 는 건물 출입증이고, ③ 은 그 건물 안 방 하나의 열쇠다
- 관리자 명단과 IP 허용 목록은 02편 5장의 access entry 와 같은 것이다
Pod 쪽 포트가 8080 인 이유
- 출력
Forwarding from 127.0.0.1:8080 -> 8080에서 Pod 쪽은 443 이 아니라 8080 이다 - 서비스의 443 이 Pod 의 8080 으로 이어진다
- kubectl 은 서비스를 거치지 않고 Pod 포트에 바로 붙는다
9. Discussion
작업 대부분: ArgoCD 바깥의 일
- ArgoCD 를 붙이는 일은 대부분 ArgoCD 바깥의 일이었다
- 저장소를 읽을 자격증명(3장)
- 무엇을 어느 울타리에 넣을지(2장)
- 넣은 뒤 누가 이어받는지(5, 6장)
- 넣기 전에 누가 거르는지(7장)
- ArgoCD 자체의 설정은 Application 하나에 동기화 방식 한 줄이었다
리뷰가 찾은 세 가지와 닫은 방법
- 모두 “종류는 막았는데 내용은 열려 있다” 는 같은 모양이었다
- 허용된 Deployment 안의 privileged
- 허용된 Ingress 안의 internet-facing
- 새 검사와 어긋난 Terraform 출력
- 앞의 둘: 앱 저장소가 바꿀 수 없는 자리에 규칙을 두는 것으로 닫았다
- 마지막: 메모를 실제 방식에 맞추는 것으로 닫았다
남은 일: 앱 쪽
- ArgoCD 에 kukie-manifest 저장소 등록
- member 이미지와 digest PR 자동화
kukie-dev/memberSecret 값- DB 5432 허용
- member 의 SES 발송용 IRSA 역할
- 다 끝나면
kukie-dev-apps에 자동 동기화 한 줄을 더한다
핵심 메시지
ArgoCD 는 Git 과 클러스터를 맞추는 일만 한다. 굽기는 앞에서, ALB 와 Secret 은 뒤에서 다른 것이 한다. 앱이 쓸 수 있는 자리와 규칙이 놓이는 자리를 나눴다. 규칙은 리뷰를 거치는 저장소에만 있다. 다음 편은 앱 쪽 — 자동 동기화를 켜기 전에 채워야 할 이미지, Secret, DB 다.
10. References
"By default, Argo CD checks (polls) Git repositories every 3 minutes to detect changes."
"restrict what kinds of objects may or may not be deployed (e.g. RBAC, CRDs, DaemonSets, NetworkPolicy etc...)"
kukie-apps 의 허용 목록이 종류까지만 막는 근거다. 그 안의 내용은 7장에서 따로 거른다."Minimally restrictive policy which prevents known privilege escalations. Allows the default (minimally specified) Pod configuration."
"Validating admission policies offer a declarative, in-process alternative to validating admission webhooks."
"For internal load balancers, the controller looks for private subnets with following tags:"
kubernetes.io/role/internal-elb 태그에 맡긴 근거다."Fundamentally, an EKS access entry associates a set of Kubernetes permissions with an IAM identity, such as an IAM role."
