0%
bit·habit 인프라 튜토리얼
GITOPS bit-habit · 실습

ArgoCD 설치하기 — 이 클러스터에, 한 걸음씩

지금 이 인프라는 전부 밀어넣기(push)입니다. 사람이 kubectl apply를 칩니다. GitOps는 그걸 뒤집습니다 — 클러스터 안에 사는 컨트롤러가 git을 영원히 당겨(pull) 읽으며 클러스터를 저장소와 같게 유지합니다. ArgoCD가 그 컨트롤러이고, 이 페이지는 그걸 이 클러스터에 올리는 과정입니다.

배우는 것

push vs pull · Application CRD · Sync vs Health · 드리프트 감지 · selfHeal / prune · Traefik 뒤에서 --insecure로 도는 이유 · 네임스페이스별 TLS secret

완료 기준

https://argocd.bit-habit.com에 로그인되고, 보드에 앱 타일이 뜨고, 손으로 만든 드리프트를 ArgoCD가 잡아내는 걸 내 눈으로 본다.

선행 조건 — 아직 미완

저장소와 클러스터의 드리프트가 0이어야 합니다. 11개 중 8개는 정리됐고, 3개가 남았습니다bithabit-api · oauth2-proxy · base/ingress.yaml. 앞으로 할 일의 2단계를 마저 끝내세요.

이 페이지는 “빨리 설치”가 목표가 아닙니다

명령어만 쭉 복사하면 15분이면 끝납니다. 그런데 그렇게 설치한 ArgoCD는 언젠가 사이트를 통째로 내립니다prune이 뭔지 모른 채 켰기 때문에. 그래서 매 단계마다 “이 줄이 왜 여기 있나”를 묻습니다. 특히 수동 동기화로 시작하고 base/ingress.yaml맨 마지막에 건드립니다.

진행 바 · 체크리스트

맨 위 진행 바가 이 페이지 전체 체크를 추적합니다(이 브라우저에 저장됩니다). 각 단계 끝의 체크박스를 눌러 진도를 남기세요. 퀴즈는 맞히는 게 목적이 아니라 “왜 그런가”를 한 번 멈춰서 생각하게 하려는 장치입니다.

⚙️ 이 클러스터의 특수사정 — 아래 모든 명령에 반영돼 있습니다

🛑 먼저 — 오늘은 아직 설치하면 안 됩니다

2026-07-26에 이 클러스터를 실제로 측정했습니다. 지금 상태에서 이 페이지의 7번(전체 확대)까지 가면 사이트 여러 개가 내려갑니다. 저장소가 클러스터보다 낡았기 때문에, ArgoCD가 “고쳐주는” 게 아니라 낡은 상태로 되돌립니다. 아래 에서 왜 그런지 보고, 앞으로 할 일의 준비 단계를 끝낸 다음 0번부터 시작하세요.

숲 — 지금 설정은 몇 군데에 살고 있나

ArgoCD가 헷갈리는 이유는 명령어가 어려워서가 아닙니다. “설정”이 한 군데가 아니라 네 군데에 산다는 걸 아무도 명시적으로 말해주지 않아서예요. 그 네 개를 먼저 그리고 시작합니다. 이 그림이 이 페이지 전체의 지도입니다.

flowchart LR
  L["💻 내 노트북
git clone"] G["☁️ GitHub
origin/main"] S["🖥️ 서버 디스크
~/workspace/bit-habit-infra"] K["☸️ 클러스터 etcd
실제로 도는 것"] L -->|"git push"| G G -->|"git pull"| S S -.->|"kubectl apply
사람이 칠 때만"| K K -.->|"돌아오는 길이 없다"| S linkStyle 3 stroke:#e0413a,stroke-width:2px
화살표 4개 중 위의 2개는 git이 자동으로 맞춰줍니다. 세 번째 점선만 사람의 손이고, 네 번째는 아예 존재하지 않습니다 — 클러스터에서 뭘 바꿔도 파일로 돌아오지 않습니다.
여기가 핵심입니다

kubectl apply -f x.yaml은 “파일을 연결”하는 게 아니라 복사입니다. apply를 치는 순간 내용이 etcd로 복사되고, 그 다음부터 파일과 etcd는 남남입니다. 파일을 고쳐도 클러스터는 모르고, 클러스터를 고쳐도 파일은 모릅니다. YAML 파일은 “현재 상태”가 아니라 “마지막으로 apply를 쳤을 때의 기억”이에요.

드리프트는 이렇게 생깁니다 (이 클러스터에서 실제로 일어난 일)

sequenceDiagram
  autonumber
  participant F as 저장소의 YAML
  participant K as 클러스터 etcd
  Note over F,K: 116일 전 — 둘이 같음
  F->>K: kubectl apply
  Note over K: 어느 날, 급하게
로컬 레지스트리로 이미지 방식을 바꿈 K->>K: kubectl edit / set image Note over F: 파일은 안 고침
(당장 잘 도니까) Note over F,K: 오늘 — 둘이 다름 = 드리프트 Note over F,K: 여기서 ArgoCD를 켜면?
git이 이기고, 클러스터가 116일 전으로 돌아감
아무도 잘못하지 않았습니다. 돌아오는 화살표가 없다는 구조가 만든 결과예요. 그래서 ArgoCD를 켜기 전에 이 간격을 손으로 한 번 메워야 합니다.

실제로 잰 값 — 그리고 그날 안에 8개가 사라졌습니다

추측이 아니라 kubectl diff로 클러스터에 직접 물어본 결과입니다. drift = 저장소를 apply하면 바뀌는 줄 수. 2026-07-26 오전에 잰 값과, 같은 날 오후의 값입니다.

오전오후무엇이 문제였나
bithabit-web · code-server · gatus
headlamp · infra · llm-app-lab · static-web
00 원래 깨끗했음 — 전부 정적 사이트라 이미지가 없습니다
booktoss · daily-seongsu · sentinel
seoul-apt-price · startpage · viz-platform
각 60 저장소가 docker.io/library/<앱>:latest라고 주장했는데 존재할 수 없는 경로였습니다 (library/는 Docker Hub 공식 이미지 전용). 진짜는 localhost:5000/…
ghost · wikijs각 80 이미지는 맞고 imagePullPolicy: Never만 틀림 — 노드 캐시가 살아 있는 동안만 동작하는 시한폭탄
bithabit-api1818 이미지 + 포트 8000 → 8002 + Endpoints가 죽은 IP로 → API 전면 중단
oauth2-proxy110110 클러스터에 없는 앱이 새로 생성됨
base/ingress.yaml22 TLS 호스트 목록에서 www.blog·www.wiki가 빠짐
8개를 지우는 데 kubectl을 한 번도 안 썼습니다

바뀐 건 git뿐입니다 — 저장소의 값을 클러스터의 값으로 고쳐 쓰고, push하고, 서버에서 pull. 클러스터에는 손도 안 댔습니다. “드리프트 해소”가 위험한 작업처럼 들리지만, 방향을 클러스터 → git으로 잡으면 본질적으로 무위험입니다. 위험은 반대 방향에만 있어요. 남은 3개가 어려운 건 줄 수가 많아서가 아니라, 어느 쪽이 정답인지 사람이 판단해야 하기 때문입니다.

가장 위험한 파일 한 개

apps/bithabit-api/service.yaml에는 손으로 쓴 Endpoints가 들어 있고, 10.0.0.61:8002(옛날 호스트 IP)를 가리킵니다. 지금 실제로는 API가 Pod 10.42.0.22:8000에서 돕니다. 이 파일을 apply하면 Service가 아무것도 없는 주소를 가리키게 됩니다.

이건 “옛날에는 맞았던 파일”이에요 — API가 호스트에서 돌던 시절의 화석입니다. 드리프트는 틀린 파일이 아니라, 낡은 파일의 문제입니다.

왜 이렇게 됐나 — 매니페스트는 틀린 게 아니라 화석이었습니다

“누가 실수했나”를 찾으려고 서버의 .bash_history를 뒤졌는데, 실수한 사람이 없었습니다. 대신 사라진 워크플로가 그대로 남아 있었어요.

서버의 ~/.bash_history — 옛날 배포 방식bash
docker build -t sentinel:latest . \
  && docker save sentinel:latest \
  | k3s ctr images import - \
  && kubectl rollout restart deployment/sentinel

레지스트리가 생기기 전에는 이미지를 containerd에 직접 밀어넣었습니다. 그 방식에는 “받아올 곳”이 아예 없습니다 — 그러니 매니페스트가 image: sentinel:latest(레지스트리 호스트 없음) 와 imagePullPolicy: Never라고 적은 건 완벽하게 옳았습니다.

flowchart LR
  subgraph OLD["옛날 — 레지스트리가 없던 시절"]
    O1["docker build
sentinel:latest"] --> O2["docker save"] O2 --> O3["k3s ctr images import"] --> O4["containerd
이미지 저장소"] end subgraph NEW["지금 — 호스트 레지스트리"] N1["docker build
localhost:5000/sentinel"] --> N2["docker push"] N2 --> N3["registry:2
127.0.0.1:5000"] N3 -->|"pull"| N4["containerd
이미지 저장소"] end OLD ~~~ NEW
차이는 딱 하나 — 아래 경로에만 pull 화살표가 있습니다. 위에는 pull이 없으니 Never가 정답이었고, 아래에는 있으니 Never가 곧 사고입니다. 워크플로는 바뀌었는데 매니페스트만 옛 세계에 남았습니다.
여기서 배울 것

드리프트를 “누가 게을러서 생긴 것”으로 보면 대책이 “다음엔 잘하자”밖에 안 나옵니다. 실제로는 인프라가 진화할 때 매니페스트만 따라오지 못한 것이고, 그건 돌아오는 화살표가 없다는 구조가 만든 필연입니다. ArgoCD가 진짜로 고치는 건 사람의 부주의가 아니라 이 구조예요 — 켜는 순간 “매니페스트를 안 고치면 배포가 안 되는” 세계가 되니까요.

⚠️ 조사하다 발견한 살아있는 함정

새 이미지를 push하고 rollout restart를 쳐도, imagePullPolicy: IfNotPresent면 새 이미지를 안 받습니다. containerd에 :latest 태그가 이미 있으니 “있네, 안 받아도 되겠다”가 되고, 재시작은 옛 코드를 조용히 다시 돌립니다.

지금 이 클러스터에서 자체 빌드 앱 9개 중 — 자동 배포되는 2개(mentoring, pr-auth)만 Always로 맞게 돼 있고, 손으로 배포하는 7개는 전부 IfNotPresent입니다. 고친 사람이 그 지식을 커밋이 아니라 클러스터에만 남겨서, 나머지 7개는 그 수정을 못 받았습니다. — 이게 kubectl edit이 위험한 진짜 이유입니다.

오늘 아침에 이미 한 일

사실 사본은 네 개가 아니라 다섯 개였습니다. 서버에 푸시 안 된 커밋 3개가 있었고, 그중 하나가 habit.bit-habit.com 장애 수정이었습니다 — git에는 없고 서버에만 있던 수정이죠.

flowchart LR
  subgraph B["오늘 아침 이전 — 진실이 4개"]
    B1["노트북
681ccbe"]:::old B2["GitHub
81b52bd"]:::mid B3["서버
9b6f0ba"]:::new B4["클러스터"]:::k end subgraph A["push + pull 이후 — 진실이 2개"] A1["노트북 · GitHub · 서버
모두 9b6f0ba"]:::new A2["클러스터"]:::k A1 -.->|"남은 간격
= 드리프트"| A2 end B ==> A classDef old fill:#fdeceb,stroke:#e0413a classDef mid fill:#fff7e6,stroke:#e36209 classDef new fill:#eafaf0,stroke:#28a745 classDef k fill:#eef4ff,stroke:#2d6cdf
이게 0단계였고 이미 끝났습니다. 남은 간격은 하나 — git ↔ 클러스터. 그 간격을 0으로 만드는 게 ArgoCD를 켜기 위한 입장권입니다.
🧠 숲 확인
저장소가 클러스터보다 낡은 상태에서 selfHeal: true로 ArgoCD를 켜면?
핵심: ArgoCD에게 “어느 쪽이 더 최신인가”라는 개념은 없습니다. git이 무조건 정답이라고 가정하고 동작해요. 그래서 GitOps의 진짜 준비 작업은 설치가 아니라 “git을 정답으로 만들어두는 일”입니다. selfHeal이 무서운 건 되돌릴 수 없어서가 아니라, 되돌리려는 손을 계속 이기기 때문입니다.

앞으로 할 일 — 설치 전에 끝내야 하는 것

ArgoCD 설치는 30분입니다. 드리프트 정리가 GitOps의 90%예요. 순서를 지키는 게 전부이고, 각 단계 사이에 게이트가 있습니다 — 게이트를 통과 못 하면 다음으로 안 갑니다.

flowchart TD
  S0["0단계 ✅ 완료
진실 합치기
서버 커밋 push → 노트북 pull"]:::done S1["1단계 · 관측
kubectl diff를 손에 익힌다
클러스터는 안 건드림 · 위험 0"]:::safe S2["2단계 · 흡수
git을 클러스터에 맞춘다
방향이 중요: 클러스터 → git"]:::work G{"게이트
모든 앱 drift = 0?"}:::gate S3["3단계 · 설치
아래 0번 섹션부터
수동 sync → apps → base"]:::goal S0 --> S1 --> S2 --> G G -->|"아니오 — 남은 앱 계속"| S2 G -->|"예"| S3 classDef done fill:#eafaf0,stroke:#28a745 classDef safe fill:#eef4ff,stroke:#2d6cdf classDef work fill:#fff7e6,stroke:#e36209 classDef gate fill:#f6f2fd,stroke:#6f42c1 classDef goal fill:#fdeceb,stroke:#e0413a
관측 먼저, 개입은 나중. 1단계는 클러스터에 아무것도 쓰지 않습니다 — 무서움을 없애는 건 조심하는 마음이 아니라 미리 보는 도구예요.

1단계 — 안전벨트부터: kubectl diff

diff서버사이드 dry-run입니다. 진짜 API 서버에 “이거 적용하면 뭐가 바뀌냐”만 물어보고 아무것도 쓰지 않습니다. 프로덕션에서 공짜로 쓸 수 있는 유일한 안전벨트이고, 이 페이지의 위험 경고들도 전부 이 명령 하나로 알아낸 겁니다. 먼저 앱 하나로 감을 잡으세요.

ssh bit-habitbash
cd ~/workspace/bit-habit-infra
sudo k3s kubectl diff -R -f apps/startpage/

예상: -로 시작하는 줄이 지금 살아있는 값, +apply하면 될 값입니다. - image: localhost:5000/startpage:latest / + image: docker.io/library/startpage:latest가 보이면 제대로 읽은 겁니다.

처음 만나면 무조건 당황하는 지점

kubectl diff차이가 있으면 종료 코드 1을 냅니다. 에러가 아니라 설계예요 (CI에서 if diff; then ...로 쓰라고). echo $?로 확인해보세요. 0 = 완전히 같음, 1 = 차이 있음, 2 = 진짜 에러. 참고로 apps/argocd/는 지금 2가 납니다 — Application이라는 종류를 클러스터가 아직 모르니까요. 그게 바로 “아직 설치 안 됨”의 증거입니다.

감이 왔으면 전체를 한 장으로 봅니다. 아래를 그대로 붙여넣으면 위의 표를 직접 재현합니다 — 숫자가 저와 다르면, 그 사이에 클러스터가 또 바뀐 겁니다.

ssh bit-habit — 앱별 드리프트 한 장으로bash
cd ~/workspace/bit-habit-infra
for d in apps/*/; do
  n=$(basename "$d")
  [ "$n" = "argocd" ] && continue
  cnt=$(sudo k3s kubectl diff -R -f "$d" 2>&1 | grep -cE '^[+-] ')
  printf '%-18s %s\n' "$n" "$cnt"
done

예상: 7개는 0, 나머지는 6~18, oauth2-proxy는 110. 이 명령이 2단계의 진도표이자 완료 판정기입니다 — 전부 0이 되면 끝입니다.

2단계 — 흡수: git을 클러스터에 맞춘다

방향이 전부입니다

본능적으로는 “저장소가 옳으니 클러스터를 고치자”고 하고 싶습니다. 반대로 하세요. 지금 돌아가고 있는 쪽이 정답입니다 — 116일간 트래픽을 받아온 증거가 있으니까요. 저장소는 그 사실을 받아적는 겁니다. 드리프트 해소 = 클러스터를 git에 맞추는 게 아니라, git에게 현실을 가르치는 일이에요. 이걸 거꾸로 하면 그게 곧 장애입니다.

앱 하나당 3분입니다. startpage로 예를 들면 — 먼저 살아있는 진짜 값을 물어봅니다.

ssh bit-habit — 클러스터에게 정답을 묻는다bash
sudo k3s kubectl get deploy startpage \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'

예상: localhost:5000/startpage:latestIfNotPresent. 이 두 값을 apps/startpage/deployment.yaml그대로 받아적습니다.

어디서 파일을 고칠까 — 서버가 아니라 노트북에서

서버에서 고치면 오늘 아침의 그 문제가 반복됩니다 — 푸시 안 된 커밋이 서버에만 쌓이는 것. 노트북에서 고치고 → push → 서버에서 pull 하세요. 이게 “git이 정답”을 몸으로 연습하는 과정이고, ArgoCD를 켠 뒤에는 강제되는 흐름이기도 합니다. 미리 그 습관을 만드는 셈이에요.

내 노트북 — 고치고 올린다bash
cd ~/workspace/bit-habit-infra
# apps/startpage/deployment.yaml 을 편집: image / imagePullPolicy 를 위 값으로
git add apps/startpage/deployment.yaml
git commit -m "fix(startpage): record the live image ref (local registry)"
git push
ssh bit-habit — 받아서 0이 됐는지 확인bash
cd ~/workspace/bit-habit-infra && git pull
sudo k3s kubectl diff -R -f apps/startpage/ ; echo "exit=$?"

예상: 출력이 비어 있고 exit=0. 이 순간 startpage는 GitOps 준비가 끝난 앱입니다. 나머지도 똑같이 반복하세요.

이 예제는 이미 실행됐습니다 — 커밋으로 확인하세요

위 절차를 startpage를 포함한 8개 앱에 그대로 적용한 게 커밋 c544eb6입니다. 읽기만 하지 말고 진짜 diff를 열어보세요 — 문서의 설명과 실제 변경이 같은지 확인하는 습관이, 나중에 남의 저장소를 읽을 때 가장 크게 남습니다.

어디서든bash
git show c544eb6 --stat
git show c544eb6 -- apps/startpage/deployment.yaml
특별 취급이 필요한 3개

bithabit-api — 이미지뿐 아니라 포트와 Endpoints가 다릅니다. 손으로 쓴 Endpoints는 Pod로 옮겨간 지금 필요 없습니다. Service에 제대로 된 selector를 주고 Endpoints 블록을 지우는 게 맞습니다 — 가장 신중하게, 마지막에 하세요.
oauth2-proxy — 클러스터에 아예 없습니다. “쓸 건가?”를 먼저 결정하고, 안 쓸 거면 apps/에서 빼거나 .disabled로 두세요. 안 쓰는 매니페스트가 저장소에 있으면 ArgoCD가 그걸 만듭니다.
base/ingress.yaml — TLS 호스트 목록에서 www.blog·www.wiki가 빠집니다. 와일드카드 *.bit-habit.com3단계 하위도메인을 안 덮으므로 이 둘은 인증서를 잃습니다. 되살려 두세요.

게이트 — 여기를 통과해야 3단계

멈춤 신호 — 무엇을 보면 즉시 되돌리나

미리 적어두나

사이트가 내려간 뒤에 “이거 되돌려야 하나?”를 판단하면 늦습니다. 그 순간이 판단력이 가장 나쁜 순간이에요 — 아드레날린이 돌고, 방금 자기가 한 일이라 인정하기 싫고, “조금만 더 보면 될 것 같은데”가 계속 이깁니다. 멈춤 신호는 지금의 침착한 내가, 나중의 당황한 나에게 내리는 명령입니다. 그래서 숫자와 명령어로 적습니다 — “상황 봐서”는 신호가 아닙니다.

이 클러스터는 폭발 반경이 세 층으로 나뉩니다. 오늘 실제로 확인한 구조예요 — 층마다 신호도 다르고 대응도 다릅니다.

멈춤 신호대응
1층 · 앱 하나
나머지 19개는 멀쩡
Sync 후 5분 안에 해당 Pod이 Running 1/1로 안 돌아옴
(ErrImageNeverPull / CrashLoopBackOff)
당황하지 말 것. 그 앱만 rollout undo.
ArgoCD는 켜둔 채로 원인부터 읽기.
2층 · 앱 여러 개
한 번의 sync가 여러 개를 건드림
Ready 미달 Deployment가 2개 이상 동시에 비상 브레이크 먼저 — 자동 동기화를 떼어낸다.
ArgoCD의 손을 묶어야 되돌리기가 먹힙니다.
3층 · 게이트
전부 동시에 죽음
bit-habit.com200이 아님 · 인증서 경고 ·
base/ingress.yaml을 건드린 직후 아무 사이트도 안 열림
즉시. 30초도 보지 말 것.
브레이크 → git revert → 손으로 apply.
3층이 특별한 이유

앱 하나가 죽으면 사이트 하나가 죽습니다. 그런데 base/ingress.yaml모든 호스트명의 라우팅 표라 잘못되면 20개가 동시에 내려갑니다. 게다가 인증서까지 얽혀 있어서 복구가 “Pod 하나 살리기”보다 훨씬 깁니다. 그래서 이 파일은 이 페이지의 맨 마지막(7번)에, 그것도 selfHeal: false로만 건드립니다.

신호를 실제로 읽는 명령 (검증됨)

ssh bit-habit — 1·2층: Ready 미달 Deployment만bash
sudo k3s kubectl get deploy -A --no-headers |
  awk '{split($3,a,"/"); if (a[1]!=a[2]) print $1"/"$2, $3}'

예상: 아무것도 안 나오면 정상. 한 줄 = 1층, 두 줄 이상 = 2층, 브레이크.

쓰면 안 되는 명령 — 실제로 속았습니다

이걸 쓰고 싶어집니다: kubectl get pods -A --field-selector=status.phase!=Running. 이 클러스터에서 실행하면 9줄이 쏟아집니다 — 그런데 전부 Completed, 즉 배포하며 교체된 옛날 Pod의 시체예요. 아무 문제도 없는데 문제처럼 보입니다. 비상시에 늑대소년을 내는 지표는 지표가 아니라 소음입니다. 그래서 “Pod이 Running이냐”가 아니라 “Deployment가 원하는 개수만큼 Ready냐”를 봅니다 — 위의 awk가 그겁니다.

ssh bit-habit — 3층: 게이트가 살아있나bash
for h in bit-habit.com habit.bit-habit.com blog.bit-habit.com wiki.bit-habit.com status.bit-habit.com; do
  printf '%-26s %s\n' "$h" "$(curl -s -o /dev/null -m 8 -w '%{http_code}' https://$h)"
done

예상: 전부 200. 하나라도 000(연결 실패)이나 5xx면 3층입니다. 000은 TLS 실패도 포함하니 인증서 사고까지 같이 잡힙니다.

비상 브레이크 — 되돌리기 전에 치는 명령

순서가 반대면 아무것도 안 먹힙니다

selfHeal: true가 켜진 상태에서 rollout undo를 치면, ArgoCD가 몇 초 안에 다시 덮습니다. 되돌려도 되돌려도 원위치로 가는 지옥이 여기서 나와요. 그러니 항상 브레이크 먼저, 복구는 그 다음입니다.

ssh bit-habit — ① ArgoCD의 손을 묶는다bash
# 자동 동기화(selfHeal 포함)만 떼어낸다. 앱은 그대로 돌아감.
sudo k3s kubectl -n argocd patch application bit-habit-apps \
  --type merge -p '{"spec":{"syncPolicy":null}}'
ssh bit-habit — ② 그 다음에 복구bash
sudo k3s kubectl rollout undo deploy/<앱이름>      # 1층: 그 앱만
sudo k3s kubectl rollout status deploy/<앱이름>    # 돌아왔는지 확인
이 두 명령만은 아직 검증 못 했습니다

이 페이지의 다른 명령은 전부 이 클러스터에서 실행해 출력을 확인한 것입니다. 그런데 위 patchArgoCD가 아직 설치돼 있지 않아 지금은 돌려볼 수가 없습니다. 설치 직후, 아직 아무것도 망가지지 않았을 때 연습으로 한 번 쳐보세요 — 비상 브레이크를 비상시에 처음 밟아보는 건 최악의 순서입니다. (되돌리려면 application.yaml을 다시 apply하면 syncPolicy가 살아납니다.)

시간 예산 — “조금만 더”를 이기는 장치

5분. Sync를 누르고 5분 안에 위 신호가 정상으로 안 돌아오면, 원인을 모르는 채로 되돌립니다. 원인 분석은 되돌린 뒤에 해도 됩니다 — 되돌린 상태는 안전하고, 로그는 도망가지 않아요. 디버깅과 복구를 동시에 하려는 게 대부분의 장애를 길게 만듭니다.

이 목록은 당신 것입니다

위 숫자(5분, 2개, 200)는 오늘 이 클러스터의 구조에서 뽑은 초안입니다. 운영하면서 “이건 너무 예민했다” / “이건 너무 늦었다”가 반드시 나옵니다 — 그때 이 표를 고치세요. 멈춤 신호는 한 번 쓰고 마는 문서가 아니라 장애를 겪을 때마다 한 줄씩 자라는 문서입니다. 그게 현업에서 말하는 포스트모템의 실체예요.

3단계 — 그때 아래로 내려가세요

게이트를 통과했다면 git이 진짜로 정답이 된 상태입니다. 그때부터 ArgoCD는 되돌리는 기계가 아니라 지켜주는 기계가 됩니다. 아래 0번부터 시작하세요.

0. 왜 GitOps인가? (설치하기 전에)

지금 이 클러스터에 뭔가를 바꾸는 방법은 하나입니다. 서버에 들어가 kubectl apply -f ...를 치는 것. 이걸 push 모델이라고 합니다 — 변경이 바깥에서 안으로 밀려 들어갑니다.

문제는 클러스터가 git을 절대 보지 않는다는 겁니다. 저장소의 YAML은 “우리가 원한다고 믿는 상태”일 뿐, 실제로 도는 것과 같다는 보장이 없습니다. 누가 급해서 kubectl edit으로 replica를 2로 올려두면, git에는 1이라 적혀 있는데 현실은 2입니다. 이 어긋남이 드리프트(drift)입니다. 드리프트는 조용합니다 — 아무도 모르고, 다음 배포 때 갑자기 되돌아갑니다.

flowchart LR
  subgraph NOW["지금 — push"]
    Y["사람 / CI"] ==>|"kubectl apply"| K1["클러스터
(git을 안 봄)"] end subgraph GO["ArgoCD — pull"] A["ArgoCD
(클러스터 안에 산다)"] -->|"저장소를 계속 읽음"| G["git"] A ==>|"클러스터를 git과 같게 만듦"| K2["클러스터"] end
바뀌는 건 딱 하나: 방향. 밖에서 밀어넣던 걸, 안에서 당겨 읽습니다. 그 순간 git이 “믿음”이 아니라 사실이 됩니다.
🧠 push의 문제
누가 kubectl edit으로 replica를 1 → 2로 손수 바꿔놨습니다. git에는 1이라 적혀 있습니다. 지금(push 모델) 어떻게 되나요?
핵심: 쿠버네티스는 git의 존재를 모릅니다. YAML 파일은 그냥 apply를 칠 때 읽히는 입력값일 뿐이에요. “저장소 = 현실”은 사람이 규율로 지켜온 약속이었고, 규율은 언젠가 깨집니다. ArgoCD는 그 약속을 프로그램으로 바꿉니다.
🧠 pull 모델
ArgoCD를 깔면 “배포”라는 행위는 무엇이 되나요?
핵심: 이게 GitOps의 실질적 보상입니다. 배포 권한 = git 쓰기 권한이 되고, “누가 언제 뭘 바꿨나”는 커밋 로그가 곧 감사 로그가 됩니다. 롤백도 git revert 하나로 끝납니다.

0.5 kubectl 하던 사람을 위한 ArgoCD 용어 (걸리는 것만)

당신은 이미 Pod·Deployment·Service·Ingress·네임스페이스를 압니다. 그래서 ArgoCD에서 “어? 이건 뭐지” 싶은 것만 골라, 이미 아는 것과 나란히 놓습니다. 이 다섯 개를 넘으면 나머지는 그냥 설치입니다.

Application = 새로 배우는 오브젝트 타입

ArgoCD를 설치하면 CRD(Custom Resource Definition)가 딸려옵니다 — 쿠버네티스에게 Application이라는 새 오브젝트 종류를 가르치는 것. 그때부터 kubectl get application이 Pod처럼 동작합니다. Application 하나는 문장 하나예요: “이 저장소의 이 경로를, 이 클러스터의 이 네임스페이스와 같게 유지해라.”

🧠 Application
ArgoCD의 Application 오브젝트가 담는 내용은?
핵심: Application은 앱이 아니라 매핑입니다. source(repoURL·path·targetRevision)와 destination(server·namespace) 두 쪽을 잇는 것 — 그게 전부예요. 앱 자체는 여전히 그냥 Deployment입니다.
② Sync ≠ Health — 서로 독립인 두 사실

Sync: 클러스터가 git과 같은가? (Synced / OutOfSync)  ·  Health: 그래서 그게 실제로 잘 도는가? (Healthy / Degraded / Progressing)
Synced인데 Degraded가 완벽히 가능합니다 — git에 적힌 대로 정확히 배포했는데 그 이미지가 크래시 루프인 경우. 이 구분을 머리에 넣어두세요. ArgoCD 보드를 읽는 문법입니다.

🧠 Synced + Degraded
타일이 Synced인데 Degraded입니다. 무슨 뜻인가요?
핵심: 이 조합이 디버깅할 때 제일 유용합니다. Synced가 “정상”을 뜻하지 않아요 — “지시대로 했다”만 뜻합니다. 지시가 틀렸으면 충실하게 틀린 걸 배포합니다.
③ 수동 동기화 vs automated — 관측만 할지, 손을 댈지

Application에 syncPolicy 블록이 없으면 수동입니다. ArgoCD는 차이를 보여주기만 하고 당신이 Sync 버튼을 누르기 전엔 아무것도 건드리지 않습니다. 처음엔 반드시 여기서 시작합니다 — 읽기 전용 모드로 신뢰를 쌓은 뒤 자동으로 넘어갑니다.

selfHealprune — 이 튜토리얼에서 제일 위험한 두 글자

selfHeal: true = 손으로 고친 걸 git으로 되돌린다. (원하는 동작)
prune: true = git에서 사라진 것을 클러스터에서 지운다. (위험)
파일 하나를 실수로 지우고 커밋하면, prune이 켜져 있을 때 ArgoCD는 그걸 “의도된 삭제”로 읽고 실제 서비스를 지웁니다. 그래서 이 저장소는 prune: false, selfHeal: true로 갑니다 — 자가 치유는 하되, 삭제는 절대 자동으로 하지 않는 조합.

🧠 prune
prune: true인 상태에서 실수로 apps/ghost/deployment.yaml을 지우고 커밋하면?
핵심: ArgoCD에게 “git에 없다”와 “지워라”는 같은 말입니다. 실수한 git rm 하나가 운영 서비스 삭제가 되는 경로예요. prune: false는 그 경로를 끊습니다 — 대신 진짜로 지우고 싶을 땐 손으로 지워야 합니다. 그게 맞는 거래입니다.
⑤ TLS secret은 네임스페이스를 넘지 못한다

Ingress는 자기 네임스페이스에 있는 TLS secret만 씁니다. 이 클러스터의 와일드카드 인증서 (tls-secret)는 default에 있고, 복제기가 없습니다. 그래서 argocd 네임스페이스로 손으로 복사해야 합니다 — headlamp도 똑같이 했습니다. cert-manager는 원본만 갱신하니, 갱신될 때마다(약 90일) 다시 복사해야 합니다.

1. 사전 점검 — 명령 형태부터 고정

아무것도 설치하기 전에, 앞으로 계속 쓸 명령 형태가 이 노드에서 통하는지 확인합니다. 여기서 통하면 이후 모든 단계가 통합니다.

터미널 · 노드 확인
sudo k3s kubectl get nodes

성공: 노드 한 개가 STATUS = Ready로 보입니다.

🧠 왜 sudo k3s kubectl
그냥 kubectl이 아니라 sudo k3s kubectl을 쓰는 이유는?
핵심: 클러스터의 문제가 아니라 이 노드의 접속 설정 문제입니다. 나중에 ~/.kube/config를 제대로 깔면 맨 kubectl도 됩니다 — 지금은 확실히 되는 형태로 고정하고 갑니다.
문제가 생기면
permission deniedsudo를 뺐습니다. 모든 명령에 sudo를 유지하세요.
command not found: k3sPATH에 없습니다. sudo /usr/local/bin/k3s kubectl get nodes로 시도.
NotReady노드 자체 문제입니다. ArgoCD 이전에 그것부터 해결해야 합니다.

2. ArgoCD 설치

네임스페이스를 만들고 공식 매니페스트 하나로 설치합니다.

왜 네임스페이스를 따로?

네임스페이스는 클러스터 오브젝트의 폴더입니다. ArgoCD는 파드가 대여섯 개(controller, repo-server, api-server, redis, dex 등) 붙어 다니는 시스템이라, 당신의 앱들과 섞이면 get pods 결과가 난장판이 됩니다. argocd라는 폴더에 몰아넣습니다.

터미널 · 네임스페이스 생성
sudo k3s kubectl create namespace argocd
터미널 · 공식 매니페스트로 설치
sudo k3s kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

... created 줄이 수십 개 지나갑니다. CRD·서비스어카운트·롤·디플로이먼트 — 쿠버네티스에게 Application이라는 새 타입을 가르치고, 자기 파드들을 올리는 중입니다.

터미널 · 파드가 뜨는지 확인 (ARM 이미지 pull에 1~3분)
sudo k3s kubectl get pods -n argocd

성공: 파드 7개 안팎이 전부 Running / READY 1/1. 다 안정될 때까지 명령을 다시 쳐보세요.

🧠 CRD
저 매니페스트가 CRD를 설치한다는 건 무슨 뜻인가요?
핵심: 쿠버네티스가 확장 가능한 이유가 이겁니다. ArgoCD는 “쿠버네티스 위에 얹힌 별도 도구”가 아니라 쿠버네티스의 어휘를 늘린 것에 가깝습니다. cert-manager의 Certificate도 정확히 같은 방식입니다.
문제가 생기면
ImagePullBackOff아직 당기는 중이거나 느린 겁니다. 기다렸다 재확인. sudo k3s kubectl describe pod -n argocd <파드명>
Pending스케줄러가 자원을 기다리는 중. 작은 노드에선 1분쯤 걸립니다. 계속되면 describe로 이유 확인.
argocd-dex CrashLoop여기선 무해합니다 — dex는 외부 SSO용이고 우리는 안 씁니다. 로그인을 막지 않습니다.

3. Traefik 뒤에서 돌게 만들기 (--insecure)

argocd-server에게 “평문 HTTP로 서빙해라”라고 시킵니다. 이상하게 들리지만, 여기선 그게 맞습니다.

왜 일부러 insecure를?

ArgoCD는 기본적으로 자기가 HTTPS를 하고 HTTP 요청을 HTTPS로 리다이렉트합니다. 그런데 이 클러스터에선 Traefik이 이미 TLS를 끝냅니다. HTTPS 하는 놈 뒤에 HTTPS 하는 놈을 두면 리다이렉트가 서로를 튕겨 무한 루프가 납니다(ERR_TOO_MANY_REDIRECTS). --insecure는 ArgoCD를 내부에서만 평문으로 만듭니다 — 바깥에서 오는 트래픽은 여전히 Traefik이 암호화합니다. 다른 앱들과 똑같은 구조예요.

터미널 · args에 플래그 추가
sudo k3s kubectl -n argocd patch deployment argocd-server --type json \
  -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--insecure"}]'
터미널 · 새 파드가 뜰 때까지 대기
sudo k3s kubectl -n argocd rollout status deployment argocd-server

성공: deployment "argocd-server" successfully rolled out

🧠 insecure가 안전한 이유
--insecure인데 왜 보안이 안 깨지나요?
핵심: TLS 종료(termination)는 한 군데서 하는 게 원칙입니다. 여기선 Traefik이 그 지점이에요. insecure라는 이름 때문에 오해하기 쉽지만, 정확히는 “내가 TLS를 안 한다”는 선언입니다 — 앞의 리버스 프록시가 하니까.
문제가 생기면
already has value두 번 실행했습니다. 무해합니다 — 플래그는 이미 들어갔으니 넘어가세요.
rollout이 멈춤새 파드가 pull/스케줄에 걸린 겁니다. sudo k3s kubectl get pods -n argocd로 확인.

4. UI를 argocd.bit-habit.com에 노출

와일드카드 TLS secret을 argocd 네임스페이스로 복사한 뒤, 저장소에 이미 있는 ingress를 적용합니다.

터미널 · 인증서+키를 꺼내 argocd에 새 secret으로 생성
sudo k3s kubectl get secret tls-secret -n default -o jsonpath='{.data.tls\.crt}' | base64 -d | sudo tee /tmp/tls.crt >/dev/null
sudo k3s kubectl get secret tls-secret -n default -o jsonpath='{.data.tls\.key}' | base64 -d | sudo tee /tmp/tls.key >/dev/null
sudo k3s kubectl create secret tls tls-secret -n argocd --cert=/tmp/tls.crt --key=/tmp/tls.key
sudo rm -f /tmp/tls.crt /tmp/tls.key

성공: secret/tls-secret created. (YAML을 통째로 복사하는 것보다 원본 인증서+키에서 새로 만드는 편이 깨끗합니다 — 지워야 할 낡은 메타데이터가 안 따라옵니다.)

🧠 왜 복사가 필요한가
default 네임스페이스에 이미 있는 tls-secret을 argocd ingress가 그냥 쓰면 안 되나요?
핵심: 네임스페이스는 이름 정리용 폴더이면서 동시에 보안 경계입니다. 이걸 자동화하는 도구(reflector, kubed 같은 secret 복제기)가 있지만 이 클러스터엔 없습니다 — 그래서 인증서가 갱신될 때마다(약 90일) 이 복사를 다시 해야 한다는 숙제가 남습니다.
터미널 · 저장소에 이미 있는 ingress 적용
sudo k3s kubectl apply -f /home/ubuntu/workspace/bit-habit-infra/apps/argocd/ingress.yaml
sudo k3s kubectl get ingress -n argocd

성공: argocd-ingress가 호스트 argocd.bit-habit.com으로 보입니다. 와일드카드 DNS가 이미 이 서버를 가리키므로 이름은 바로 풀립니다.

이 ingress가 실제로 하는 말

argocd.bit-habit.com으로 오는 websecure(443) 트래픽을 argocd-server 서비스의 80 포트로 넘겨라 — 그리고 TLS는 tls-secret으로 처리해라. 80인 게 핵심입니다: 3단계에서 --insecure를 켰기 때문에 평문 포트로 보내는 겁니다.

문제가 생기면
브라우저에서 404ingress는 붙었는데 서비스명/포트가 안 맞는 경우. 저장소 ingress는 argocd-server:80을 봅니다 — 3단계가 적용돼 있어야 맞습니다.
인증서 경고argocdtls-secret이 없거나 낡았습니다. 위 복사 블록을 다시 실행.
ERR_TOO_MANY_REDIRECTS3단계가 안 먹었습니다. sudo k3s kubectl -n argocd get deploy argocd-server -o yaml | grep insecure로 확인.

5. 로그인하고 보드 읽기

터미널 · 초기 admin 비밀번호 확인
sudo k3s kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath='{.data.password}' | base64 -d; echo

임의의 문자열이 출력됩니다. 아이디는 admin. 브라우저에서 https://argocd.bit-habit.com으로 접속해 로그인하세요.

🔎 여기서 1분만 머무르세요

지금 보이는 빈 Applications 보드가 이 제품의 전부입니다. 앞으로 타일 하나가 Application 하나이고, 각 타일은 독립된 두 사실을 보여줍니다 — Sync(클러스터가 git과 같은가)와 Health(그게 실제로 도는가). 0.5절의 ②가 여기서 실물이 됩니다.

🔐 권장 — 비밀번호를 바꾸고 부트스트랩 secret 삭제

UI에서 User Info → Update Password로 바꾼 다음, 초기 비밀번호가 평문으로 들어 있는 secret을 지웁니다. 안 지우면 클러스터에 읽을 수 있는 관리자 비밀번호가 계속 남습니다.

터미널
sudo k3s kubectl -n argocd delete secret argocd-initial-admin-secret
🧠 초기 비밀번호
비밀번호를 바꾼 뒤 argocd-initial-admin-secret을 지우는 이유는?
핵심: 부트스트랩 크리덴셜은 수명이 짧아야 합니다. 쓰고 나면 지우는 것 — 이 클러스터의 다른 secret들이 gitignore된 것과 같은 이유입니다.
선택 — ArgoCD CLI (arm64). 없어도 이 튜토리얼은 다 됩니다
터미널
sudo curl -sSL -o /usr/local/bin/argocd \
  https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-arm64
sudo chmod +x /usr/local/bin/argocd
argocd login argocd.bit-habit.com --username admin

여기 나오는 모든 작업은 웹 UI로 가능합니다. CLI는 편의일 뿐이니 건너뛰어도 됩니다.

6. 첫 Application — 작은 앱 하나, 수동 동기화

이제 핵심 루프를 눈으로 봅니다. 자동은 아직입니다.

왜 수동으로, 왜 한 개만

이 Application에는 syncPolicy없습니다. 즉 ArgoCD는 git과 클러스터의 차이를 보여주기만 하고, 당신이 Sync를 누르기 전엔 아무것도 안 건드립니다. 대상은 startpage — git의 매니페스트 그대로 이미 돌고 있는 Deployment 하나짜리 앱이라, 가능한 가장 안전한 첫 목표입니다.

터미널 · Application 생성 (수동 동기화)
sudo k3s kubectl apply -n argocd -f - <<'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: startpage
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/bookseal/bit-habit-infra.git
    targetRevision: main
    path: apps/startpage
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  # syncPolicy 블록 없음 = 수동. 관찰만 하고 스스로 손대지 않는다.
EOF

성공: application.argoproj.io/startpage created. UI를 새로고침하면 타일이 하나 생깁니다.

👀 타일을 읽어보세요

십중팔구 이미 Synced + Healthy일 겁니다 — 지금 도는 앱이 바로 이 매니페스트에서 apply된 것이니까요. 그게 “아하” 포인트입니다: git은 이미 클러스터와 같았습니다. ArgoCD가 한 일은 그 사실을 처음으로 증명 가능하게 만든 것뿐입니다. 만약 OutOfSync라면 타일 → App Diff에서 정확히 뭐가 어긋났는지 보고, Sync로 git에 맞추세요.

터미널 · 같은 사실을 CLI로도
sudo k3s kubectl get application -n argocd

startpage의 SYNC STATUS와 HEALTH STATUS가 보입니다 — 타일과 같은 두 사실입니다.

🧠 수동 모드의 의미
이 Application 상태에서 누가 startpage를 손으로 replicas=2로 바꾸면?
핵심: 이게 수동 모드의 가치입니다 — 드리프트 감지는 얻고, 자동 개입은 아직 안 주는 상태. ArgoCD가 뭘 하려는지 며칠 지켜본 뒤에 자동을 켜는 게 순서입니다. 참고로 ArgoCD는 git을 절대 고치지 않습니다. 방향은 언제나 git → 클러스터.
실험(선택) — 일부러 드리프트를 만들어 ArgoCD가 잡는 걸 보기
터미널
sudo k3s kubectl scale deployment startpage -n default --replicas=2

UI 새로고침 → OutOfSync(git은 1이라고 하니까). Sync를 누르면 다시 1. 수동 모드라서 누르기 전엔 스스로 안 고칩니다 — 여기서 시작하는 이유가 이겁니다. 이 한 번의 실험이 GitOps의 전부를 보여줍니다.

7. 저장소 전체로 확대 (그리고 base/는 맨 마지막)

루프가 이해됐으면 apps/ 전체를 맡깁니다. 여기서 처음으로 자동을 켭니다.

두 개의 다이얼

selfHeal: true = 손으로 고친 걸 git으로 되돌린다. prune: false = git에서 사라졌다는 이유만으로는 절대 지우지 않는다. 이 조합이 우리가 원하는 것입니다 — 자가 치유는 하되, 삭제는 사람 손으로. 저장소의 apps/argocd/application.yaml도 정확히 이 설정입니다.

터미널 · apps/ 전체 관리 (자동 치유, prune 없음)
sudo k3s kubectl apply -n argocd -f - <<'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: bit-habit-apps
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/bookseal/bit-habit-infra.git
    targetRevision: main
    path: apps
    directory:
      recurse: true
      exclude: "argocd/*"   # ArgoCD가 자기 자신의 Application 파일과 싸우지 않게
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: false
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
EOF

보드가 타일로 채워집니다. 일부는 OutOfSync일 수 있습니다 — 살아 있는 클러스터가 git에서 흘러간 지점입니다. 자동 치유를 믿기 전에 각 diff를 먼저 보세요. (앞의 startpage Application은 중복이니 지워도 됩니다: sudo k3s kubectl delete application startpage -n argocd)

🧠 exclude
exclude: "argocd/*"가 없으면 무슨 일이 생기나요?
핵심: apps/argocd/에는 Application 정의와 ingress가 들어 있습니다. ArgoCD가 자기를 관리하게 만드는 패턴(app-of-apps)도 있지만, 그건 의도적으로 설계해야 하는 것 — 실수로 그렇게 되면 디버깅이 아주 괴로워집니다.
🛑 base/는 맨 마지막에, 그리고 보고 나서

base/ingress.yaml모든 서브도메인의 라우팅 표입니다. 여기서 동기화 한 번 잘못되면 사이트가 한꺼번에 내려갑니다. apps 보드가 조용해진 뒤에 붙이고, 그때도 수동으로 두는 것을 고려하세요. 저장소 파일에는 base/apps/ Application이 둘 다 정의돼 있습니다:

터미널 · 준비됐을 때만
sudo k3s kubectl apply -f /home/ubuntu/workspace/bit-habit-infra/apps/argocd/application.yaml

엣지까지 넘길 준비가 됐을 때 적용하세요. 아니면 bit-habit-base 쪽만 떼어내 selfHeal: false로 유지하는 것도 좋은 선택입니다.

🧠 왜 base가 위험한가
base/를 앱들보다 나중에 맡기는 이유는?
핵심: 자동화를 도입할 땐 폭발 반경이 작은 것부터. startpage 하나 → apps 전체 → 마지막에 엣지. 각 단계에서 신뢰를 확인하고 넘어가는 순서 자체가 이 튜토리얼의 설계입니다.
🔑 GitOps가 여기서 아직 못 하는 것: secret

Route53 키, basic-auth, oauth2-proxy 클라이언트 시크릿은 gitignore돼 있습니다. 즉 ArgoCD가 동기화할 수 없고, 빈 클러스터에 이 저장소를 그대로 적용하면 secret이 빠진 채 올라옵니다. 이걸 닫는 게 진짜 다음 과제입니다 — Sealed Secrets나 External Secrets. 그전까지 secret은 수동 kubectl apply로 남습니다. (개요 페이지의 “키울 것” 목록 참고)

8. 되돌리기 — 안전한 제거

⚠️ 먼저 알아야 할 것

Application에 resources-finalizer가 붙어 있으면, Application을 지울 때 그 앱의 실제 리소스까지 연쇄 삭제됩니다. 이 튜토리얼의 Application들에는 그게 없어서 지워도 돌고 있는 앱은 그대로입니다 — 하지만 지우기 전에 항상 확인하세요.

터미널 · Application부터 제거 (앱은 계속 돌아감)
sudo k3s kubectl delete application --all -n argocd
터미널 · ArgoCD 자체 제거
sudo k3s kubectl delete -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
sudo k3s kubectl delete namespace argocd

클러스터는 push 모델로 돌아갑니다 — 앱은 전부 그대로 돕니다. ArgoCD는 워크로드를 소유한 적이 없고, 지켜봤을 뿐이니까요.

🧠 소유 vs 관찰
ArgoCD를 통째로 지워도 앱들이 계속 도는 이유는?
핵심: 이 성질이 ArgoCD 도입을 되돌리기 쉬운 결정으로 만듭니다. 런타임에 끼어드는 게 아니라 apply를 대신 쳐주는 존재라서, 빼도 남는 건 평범한 쿠버네티스 오브젝트뿐입니다. 새 도구를 도입할 때 항상 물어야 할 질문이기도 합니다 — “이걸 빼면 뭐가 남지?”

9. 마무리 · 이해도 체크

명령이 아니라 머릿속에 남았는지를 확인하세요. 다 채우면 이 페이지가 100%가 됩니다. 🎉

이번에 얻은 감각

“저장소가 곧 현실”이라는 말이 믿음에서 기계가 강제하는 사실로 바뀌었습니다. 그리고 자동화를 도입하는 순서도 함께 얻었습니다 — 관측 먼저, 개입은 나중, 폭발 반경이 작은 것부터. 이 순서는 다음에 무엇을 자동화하든 똑같이 적용됩니다.

돌아가기← 인프라 개요