9. 배포 — Argo CD와 GitOps
“이 클러스터에 뭐가 깔려 있죠?” 에 Git 저장소 주소로 답할 수 있어야 한다
이 장에서 처음 나오는 말6개
GitOps- 클러스터의 원하는 상태를 Git에 두고, 컨트롤러가 그 상태를 클러스터에 계속 맞추는 방식. 배포가
kubectl apply가 아니라 머지가 된다. 드리프트Drift- Git에 적힌 상태와 클러스터 실제 상태가 어긋난 것. 누군가
kubectl edit으로 급하게 고친 흔적이 대부분이다. App of Apps- Argo CD
Application하나가 다른Application들을 배포하는 구조. 클러스터 전체를 한 리소스에서 펼친다. sync wave동기화 웨이브- 리소스에 순번을 매겨 설치 순서를 강제하는 Argo CD 기능. CRD가 먼저, 그것을 쓰는 리소스가 나중이어야 할 때 쓴다.
부트스트랩Bootstrap- 아무것도 없는 클러스터에서 자기 자신을 관리할 첫 조각을 손으로 심는 일. Argo CD 최초 설치가 그것이다.
미러 레지스트리Mirror Registry- 외부 이미지를 사내에 복제해 두는 저장소. 인터넷에 직접 못 나가는 온프렘에서는 사실상 필수다.
문제 — 손으로 깐 것은 재현되지 않는다
섹션 제목: “문제 — 손으로 깐 것은 재현되지 않는다”여기까지 오면 클러스터에 이만큼이 깔려 있다.
MetalLB · Gateway 구현체 · cert-manager · external-dns · oauth2-proxy · CloudNativePG · kube-prometheus-stack · Loki · Tempo · Grafana · Alloy · Velero — 열두 개, 각각 Helm values가 수십 줄.
| 질문 | 손으로 깔았다면 |
|---|---|
| “지금 Loki 보존 기간이 며칠이죠?” | 클러스터에 들어가 봐야 안다 |
| “누가 이 값을 바꿨죠?” | 알 수 없다 |
| “이 설정 왜 이렇게 돼 있죠?” | 3개월 전 그 사람만 안다 |
| “DR(Disaster Recovery, 재해 복구) 사이트에 같은 걸 세워 주세요” | 처음부터 다시, 기억에 의존해서 |
| “테스트 클러스터랑 뭐가 다르죠?” | 비교할 방법이 없다 |
Git에 두면 이 다섯이 전부 해결된다. 그게 GitOps의 실용적인 이유다 — “선언적”이라는 말보다 이쪽이 와닿는다.
Argo CD — 원하는 상태를 계속 맞춘다
섹션 제목: “Argo CD — 원하는 상태를 계속 맞춘다”| Argo CD | Flux | |
|---|---|---|
| UI | 있다 (실물 화면이 운영에서 꽤 유용하다) | 없다 (CLI·CRD) |
| 모델 | Application 중심 | 여러 컨트롤러(Source·Kustomize·Helm) |
| 고를 때 | 사람이 화면으로 상태를 보는 게 중요하면 | 완전 자동화·GitOps 순수주의 |
이 덱은 Argo CD를 기준으로 쓴다 — 온프렘 운영에서 “지금 뭐가 안 맞나”를 화면으로 보여 주는 것의 가치가 크기 때문이다.
저장소 구조
섹션 제목: “저장소 구조”디렉터리platform/
디렉터리bootstrap/
- root-app.yaml 모든 것의 시작 — 이것 하나만 손으로 적용한다
디렉터리infra/
디렉터리metallb/ values.yaml
- …
디렉터리gateway/ values.yaml
- …
디렉터리cert-manager/ values.yaml, clusterissuer.yaml
- …
디렉터리external-dns/ values.yaml
- …
디렉터리data/
디렉터리cnpg-operator/
- …
디렉터리cnpg-clusters/ platform-pg.yaml
- …
디렉터리observability/
디렉터리kube-prometheus-stack/ values.yaml, rules/
- …
디렉터리loki/ values.yaml
- …
디렉터리tempo/ values.yaml
- …
디렉터리grafana/ values.yaml, dashboards/
- …
디렉터리apps/
디렉터리team-a/
- …
디렉터리team-b/
- …
# root-app.yaml — App of AppsapiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: platform-root namespace: argocdspec: project: default source: repoURL: https://git.example.internal/platform/platform.git targetRevision: main path: bootstrap/apps # 여기에 각 컴포넌트의 Application이 들어 있다 destination: server: https://kubernetes.default.svc syncPolicy: automated: prune: true # Git에서 지운 것은 클러스터에서도 지운다 selfHeal: true # 손으로 고친 것을 되돌린다설치 순서 — sync wave
섹션 제목: “설치 순서 — sync wave”플랫폼은 순서가 있다. CRD가 없으면 그 CRD를 쓰는 리소스가 실패하고, Issuer가 없으면 Certificate가 발급되지 않는다.
metadata: annotations: argocd.argoproj.io/sync-wave: "-2" # 숫자가 작을수록 먼저| wave | 내용 | 왜 이 순서 |
|---|---|---|
| -3 | 네임스페이스, CRD | 나머지가 이걸 참조한다 |
| -2 | MetalLB, cert-manager, CNPG 오퍼레이터 | 컨트롤러가 먼저 떠야 CR을 처리한다 |
| -1 | ClusterIssuer, IPAddressPool, Gateway | 오퍼레이터가 살아 있어야 만들어진다 |
| 0 | CNPG Cluster, Keycloak, 오브젝트 스토리지 연동 | 상태 계층 |
| 1 | 관측 스택, Argo CD 자신의 설정 | |
| 2 | 앱 |
시크릿 — 여기서는 자리만 잡는다
섹션 제목: “시크릿 — 여기서는 자리만 잡는다”지금까지 나온 것만 해도 비밀이 이만큼이다 — 오브젝트 스토리지 액세스 키, Postgres 비밀번호, Keycloak client secret, DNS API 토큰, Grafana OAuth secret. 이걸 평문으로 Git에 올릴 수는 없는데, GitOps는 Git이 단일 소스여야 성립한다.
| 방식 | Git에 올라가는 것 | 누가 푸나 |
|---|---|---|
| Sealed Secrets | 암호문 | 클러스터 안 컨트롤러 |
| ESO + 외부 금고 | 참조만 | 오퍼레이터가 금고에서 읽어 온다 |
| SOPS(Secrets OPerationS) + age | 암호문(파일) | 배포 시 사람·CI(Continuous Integration, 지속적 통합) 파이프라인 |
이미지 — 사내 레지스트리
섹션 제목: “이미지 — 사내 레지스트리”온프렘에서 배포가 실패하는 흔한 이유는 매니페스트가 아니라 이미지를 못 받아 오는 것이다.
| 문제 | 대응 |
|---|---|
| 인터넷 직접 접근 불가 | 사내 레지스트리(Harbor 등)에 미러링 |
| 외부 레지스트리 rate limit | 마찬가지. 미러가 rate limit도 해결한다 |
| 에어갭 | 이미지를 tar로 반입해 push. 차트도 함께 |
| 태그가 조용히 바뀜 | 다이제스트 고정(@sha256:…) 또는 최소한 고정 태그. latest 금지 |
| 사내 CA를 노드가 안 믿음 | 노드의 CA 저장소에 추가 (4장) |
# Helm values에서 레지스트리를 한 곳으로 몰아 두면 미러 전환이 한 줄이 된다global: imageRegistry: registry.example.internal imagePullSecrets: [ regcred ]운영에서 부딪히는 것들
섹션 제목: “운영에서 부딪히는 것들”| 항목 | 내용 |
|---|---|
prune이 무섭다 | 처음엔 prune: false로 두고 화면에서 차이만 본다. 익숙해지면 켠다 |
| CRD가 지워질 뻔한다 | CRD에 argocd.argoproj.io/sync-options: Prune=false를 달아 보호한다. CRD가 지워지면 그 CR의 데이터가 전부 사라진다 |
| 드리프트가 계속 난다 | selfHeal이 켜져 있는데 누군가 계속 손으로 고친다는 뜻이다. 왜 손으로 고쳐야 하는지를 먼저 없앤다 |
| Helm 차트 버전 고정 | targetRevision을 범위로 두면 어느 날 조용히 올라간다. 고정한다 |
| 차트 저장소 이전 | Loki 차트처럼 저장소가 옮겨 가는 일이 있다 (관측 덱 3장) |
| Argo CD 자신의 알림 | argocd_app_info{sync_status="OutOfSync"}를 Prometheus 알림으로 (관측 덱 2장) |
# ① 전체 앱 상태 — OutOfSync·Degraded가 있나kubectl -n argocd get applicationsargocd app list
# ② 왜 다른가argocd app diff platform-observability
# ③ 시크릿이 실제로 만들어졌나kubectl -n observability get secret loki-s3 -o jsonpath='{.data}' | jq 'keys'
# ④ 이미지가 사내 레지스트리에서 오나kubectl -n observability get pods -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | sort -u
# ⑤ sync wave 순서대로 떴나 (앞 wave가 Degraded면 뒤는 볼 필요 없다)kubectl -n argocd get applications -o custom-columns=\'NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status'9장 요약
섹션 제목: “9장 요약”- GitOps의 실용적 이유는 “뭐가 깔려 있고, 누가 언제 바꿨고, 어떻게 다시 세우나” 에 답하는 것이다
- App of Apps + sync wave로 클러스터 전체와 설치 순서를 선언한다
- 닭과 달걀을 인정한다 — Argo CD 최초 설치, Git 저장소, 레지스트리는 클러스터 밖에 둔다
- 시크릿은 부트스트랩 순서에서 앱보다 먼저 온다 — 실무 상세는 10장
- 온프렘 배포 실패의 단골은 매니페스트가 아니라 이미지다 — 사내 레지스트리와 태그 고정
- CRD는 prune에서 보호한다. CRD가 지워지면 그 CR의 데이터가 전부 사라진다
10. 시크릿 — 값을 넣고 · 지키고 · 바꾸기Git에 못 올리는 값을 어떻게 다루나 — 봉인, 스코프 함정, 네임스페이스 경계, 회전과 복구.