Files
service-catalog/scripts/deploy-test/deploy-test-argo-cd.sh
T
wbsong111 3aa69e5eda argo-cd: 10.4.0 (ArgoCD v3.5.1) 추가 + 배포 테스트 스크립트
k8s 1.33 에서 Deployment/ReplicaSet 에 추가된 .status.terminatingReplicas 를
ArgoCD v2.14.5(k8s 라이브러리 0.31/0.32)가 몰라, 클러스터가 1.33 이상이면
ServerSideApply=true 인 Application 이 전부 아래 오류로 죽는다.

  ComparisonError: failed to calculate diff: error calculating structured
  merge diff: .status.terminatingReplicas: field not declared in schema

v3.5.1 은 k8s 라이브러리 0.36.1 을 써서 해소된다. dev 클러스터(1.35.7+rke2r1)
에서 실제 발생·해소를 확인했다.

- manifests/helm/argo-cd/10.4.0/ 추가 (chart_updater 로 생성)
  - custom-values.yaml 을 실제 배포 기준(dipup argo-cd-values.yaml.tpl)에 맞춤:
    kong → apisix, server.extraArgs(--insecure), configs.cm/rbac 추가
  - configs.params.controller.resource.health.persist=true 지정 —
    ArgoCD v3.0 부터 리소스 health 를 CR 에 저장하지 않는 것이 기본값인데,
    dip-console-api 가 .status.resources[].health 를 읽는다
  - CUSTOM-README.md 는 현재 차트 기준 배포 가이드로 재작성.
    승계된 cert-manager 예시가 10.4.0 에 없는 구버전 스키마(hosts/tls 리스트)라
    Ingress 가 생성되지 않는 상태였던 것도 함께 수정
  - breaking_change_check: breaking=false (제거 26건이 전부 미사용 key)

- scripts/deploy-test/deploy-test-argo-cd.sh 신규
  같은 클러스터에 두 번째 argo-cd 를 띄우려면 crds.install=false 가 필수다 —
  CRD 3종이 클러스터 전역이고 운영 릴리스가 소유해 Helm 이 ownership 충돌로
  거부한다. Ingress 도 항상 끈다(운영과 host 충돌).
  검증: 워크로드 롤아웃 / health.persist / api 버전 / admin 로그인

- argo-cd/7.8.11/BUILD-README.md 에 `helm repo add argo ...` 추가
  chart_version_detector 가 이 한 줄을 정규식으로 뽑아 업스트림 레포를 정하는데,
  argo-cd 에는 없어서 신규 버전 자동 감지가 조용히 실패하고 있었다
  (repo: null → latest_version: null). CLAUDE.md 의 "변경 금지" 규정도
  실제 파싱 계약에 맞게 정정했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:28:21 +09:00

268 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# deploy-test-argo-cd.sh
# argo-cd 차트가 지정한 values 로 실제 Kubernetes 배포 수준에서 정상 동작하는지
# 확인한다. helm template/lint 은 "렌더가 되는가"만 본다. 이 스크립트는
# "실제로 뜨고, API 가 응답하고, 로그인이 되는가"를 본다 —
# .claude/deploy-test-procedure.md 의 원칙.
#
# 운영 argo-cd 를 교체하기 전에 같은 클러스터에서 격리 검증하는 용도다.
#
# ── 같은 클러스터에 두 번째 argo-cd 를 띄울 때의 제약 (실측) ──────────────
# CRD 3종(applications/applicationsets/appprojects)은 클러스터 전역이고 이미
# 운영 릴리스가 소유한다(meta.helm.sh/release-name). 그대로 설치하면 Helm 이
# ownership 충돌로 거부하므로 반드시 crds.install=false 로 끈다. 대신 CRD 가
# 클러스터에 이미 있어야 하며, 스크립트가 사전에 확인한다.
# ClusterRole/Binding 은 릴리스명 접두라 충돌하지 않는다
# (운영 argo-cd-argocd-* vs 테스트 argocd-build-*).
# application-controller 는 자기 네임스페이스의 Application 만 감시하므로
# 운영 Application 을 건드리지 않는다.
#
# Ingress 는 항상 끈다 — 운영과 같은 host 로 Ingress 가 뜨면 트래픽을 가로챈다
# (.claude/pitfalls.md 의 ingress host 충돌).
#
# 사용:
# bash scripts/deploy-test/deploy-test-argo-cd.sh <OUT_DIR>
# VALUES_FILE=/path/to/values.yaml ADMIN_PASSWORD=xxx \
# bash scripts/deploy-test/deploy-test-argo-cd.sh /tmp/deploy-test-argocd
#
# 환경변수:
# CHART_DIR 기본 manifests/helm/argo-cd/10.4.0
# VALUES_FILE (선택) 검증할 values 파일. 미지정 시 차트 기본값만 사용
# ADMIN_PASSWORD (선택) 지정 시 admin 로그인까지 검증한다. 비밀번호를 승계해
# 재설치하는 경우 이 검증이 핵심이다(해시가 맞는지 확인)
# TEST_NAMESPACE 기본 argocd-test-build (매 실행 재생성)
# RELEASE 기본 argocd-build
# LOCAL_PORT 기본 18080 (port-forward 로컬 포트)
# WAIT_TIMEOUT 기본 420 (초) — 워크로드 롤아웃 대기 한도
# EXPECT_VERSION 기본 차트 Chart.yaml 의 appVersion (예: v3.5.1)
#
# 산출물 (OUT_DIR):
# deploy-test.log 전체 실행 로그
#
# 종료 코드: 0 = 기능 확인 통과, 그 외 = 실패.
# 정리는 종료 코드와 무관하게 항상 수행된다.
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
OUT_DIR="${1:?사용법: deploy-test-argo-cd.sh <OUT_DIR>}"
mkdir -p "$OUT_DIR"
LOG="$OUT_DIR/deploy-test.log"
: > "$LOG"
CHART_DIR="${CHART_DIR:-$REPO_ROOT/manifests/helm/argo-cd/10.4.0}"
VALUES_FILE="${VALUES_FILE:-}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
TEST_NAMESPACE="${TEST_NAMESPACE:-argocd-test-build}"
RELEASE="${RELEASE:-argocd-build}"
LOCAL_PORT="${LOCAL_PORT:-18080}"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-420}"
log() { echo "$(date -u +%H:%M:%S) $*" | tee -a "$LOG"; }
log "== 사전 확인 =="
for t in kubectl helm python3 curl; do
command -v "$t" >/dev/null || { log "::error:: $t 없음"; exit 2; }
done
[ -d "$CHART_DIR" ] || { log "::error:: 차트 없음: $CHART_DIR"; exit 2; }
if [ -n "$VALUES_FILE" ] && [ ! -f "$VALUES_FILE" ]; then
log "::error:: VALUES_FILE 없음: $VALUES_FILE"; exit 2
fi
if ! kubectl get ns >/dev/null 2>>"$LOG"; then
log "::error:: 클러스터 연결 실패 — kubeconfig/네트워크 확인"
exit 2
fi
EXPECT_VERSION="${EXPECT_VERSION:-$(python3 -c "
import sys,re
for line in open('$CHART_DIR/Chart.yaml'):
m = re.match(r'appVersion:\s*[\"\x27]?([^\"\x27\s]+)', line)
if m: print(m.group(1)); break
" 2>/dev/null)}"
log " OK (kubectl context: $(kubectl config current-context 2>/dev/null))"
log " chart=$CHART_DIR"
log " values=${VALUES_FILE:-<차트 기본값>}"
log " expect appVersion=$EXPECT_VERSION"
log " admin 로그인 검증=$([ -n "$ADMIN_PASSWORD" ] && echo|| echo '아니오 (ADMIN_PASSWORD 미지정)')"
# CRD 선행 확인 — crds.install=false 로 설치하므로 이미 있어야 한다.
log "== CRD 선행 확인 (crds.install=false 로 설치) =="
missing=0
for crd in applications.argoproj.io applicationsets.argoproj.io appprojects.argoproj.io; do
if kubectl get crd "$crd" >/dev/null 2>&1; then
log " 있음: $crd"
else
log "::error:: 없음: $crd"
missing=1
fi
done
if [ "$missing" != "0" ]; then
log "::error:: ArgoCD CRD 가 클러스터에 없다. 운영 argo-cd 가 설치된 클러스터에서 실행하거나,"
log " CRD 를 먼저 적용한 뒤 재실행한다 (이 스크립트는 CRD 를 만들지 않는다 —"
log " 클러스터 전역 리소스라 운영 릴리스 소유권을 침범하기 때문)."
exit 2
fi
# --- 정리 (선-정리·후-정리 공용) ---------------------------------------------
PF_PID=""
cleanup() {
if [ -n "$PF_PID" ] && kill -0 "$PF_PID" 2>/dev/null; then
kill "$PF_PID" 2>/dev/null
wait "$PF_PID" 2>/dev/null
fi
PF_PID=""
kubectl get ns "$TEST_NAMESPACE" >/dev/null 2>&1 || return 0
log "== 정리: $TEST_NAMESPACE =="
if helm status "$RELEASE" -n "$TEST_NAMESPACE" >/dev/null 2>&1; then
helm uninstall "$RELEASE" -n "$TEST_NAMESPACE" >>"$LOG" 2>&1
fi
kubectl -n "$TEST_NAMESPACE" delete pvc --all --wait --timeout=120s >>"$LOG" 2>&1
# storageClass longhorn 은 reclaimPolicy Retain 이라 PVC 삭제만으로는 볼륨이 안
# 지워진다(.claude/pitfalls.md). 이 네임스페이스가 남긴 PV 만 골라 지운다.
local pv
for pv in $(kubectl get pv -o json 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for p in d.get('items', []):
cr = p['spec'].get('claimRef') or {}
if cr.get('namespace') == '$TEST_NAMESPACE':
print(p['metadata']['name'])
" 2>/dev/null); do
kubectl delete pv "$pv" --ignore-not-found >>"$LOG" 2>&1 \
|| log "::warning:: PV $pv 삭제 실패 — 수동 확인 필요"
done
kubectl delete ns "$TEST_NAMESPACE" --wait --timeout=180s >>"$LOG" 2>&1
log " 완료"
}
RESULT=1
trap 'cleanup' EXIT
cleanup # 직전 실행이 비정상 종료했을 경우를 대비한 선-정리
log "== argo-cd: 설치 ($RELEASE/$TEST_NAMESPACE) =="
kubectl create ns "$TEST_NAMESPACE" >>"$LOG" 2>&1
helm_args=(
install "$RELEASE" "$CHART_DIR" -n "$TEST_NAMESPACE"
# 클러스터 전역 CRD 는 운영 릴리스 소유 — 건드리지 않는다
--set crds.install=false
# 운영과 같은 host 로 Ingress 가 뜨면 트래픽을 가로챈다
--set server.ingress.enabled=false
# 검증용이므로 단일 레플리카로 축소
--set controller.replicas=1
--set server.replicas=1
--set repoServer.replicas=1
--set applicationSet.replicas=1
--wait --timeout "${WAIT_TIMEOUT}s"
)
[ -n "$VALUES_FILE" ] && helm_args+=(-f "$VALUES_FILE")
if ! helm "${helm_args[@]}" >>"$LOG" 2>&1; then
log "::error:: 차트 설치 실패 — $LOG 확인"
kubectl -n "$TEST_NAMESPACE" get pods >>"$LOG" 2>&1
kubectl -n "$TEST_NAMESPACE" describe pods >>"$LOG" 2>&1
exit 1
fi
log "== 워크로드 롤아웃 확인 =="
rollout_fail=0
for kind in deployment statefulset; do
for name in $(kubectl -n "$TEST_NAMESPACE" get "$kind" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
if kubectl -n "$TEST_NAMESPACE" rollout status "$kind/$name" --timeout="${WAIT_TIMEOUT}s" >>"$LOG" 2>&1; then
log " OK $kind/$name"
else
log "::error:: 롤아웃 실패 $kind/$name"
rollout_fail=1
fi
done
done
if [ "$rollout_fail" != "0" ]; then
kubectl -n "$TEST_NAMESPACE" get pods -o wide >>"$LOG" 2>&1
kubectl -n "$TEST_NAMESPACE" describe pods >>"$LOG" 2>&1
exit 1
fi
# 이 카탈로그가 기본 지정하는 값이 실제 ConfigMap 까지 도달했는지 확인한다.
log "== argocd-cmd-params-cm 확인 =="
persist="$(kubectl -n "$TEST_NAMESPACE" get cm argocd-cmd-params-cm \
-o jsonpath='{.data.controller\.resource\.health\.persist}' 2>/dev/null)"
log " controller.resource.health.persist='${persist:-<미설정>}'"
if [ -n "$VALUES_FILE" ] && [ "$persist" != "true" ]; then
log "::warning:: health.persist 가 true 가 아니다 — dip-console-api 의 리소스 health 조회가 빈다"
fi
log "== port-forward (localhost:$LOCAL_PORT) =="
SERVER_SVC="$(kubectl -n "$TEST_NAMESPACE" get svc -l app.kubernetes.io/name=argocd-server \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
if [ -z "$SERVER_SVC" ]; then
log "::error:: argocd-server Service 를 찾지 못함"
exit 1
fi
kubectl -n "$TEST_NAMESPACE" port-forward "svc/$SERVER_SVC" "${LOCAL_PORT}:80" >>"$LOG" 2>&1 &
PF_PID=$!
ready=0
for _ in $(seq 1 30); do
sleep 1
if curl -sk --max-time 3 "http://localhost:${LOCAL_PORT}/healthz" >/dev/null 2>&1; then
ready=1; break
fi
done
if [ "$ready" != "1" ]; then
log "::error:: port-forward 로 서버에 접속하지 못했다"
exit 1
fi
log " 연결 OK"
log "== 기능 확인: /api/version =="
ver_json="$(curl -sk --max-time 10 "http://localhost:${LOCAL_PORT}/api/version" 2>>"$LOG")"
got_version="$(printf '%s' "$ver_json" | python3 -c "
import json,sys
try: print(json.load(sys.stdin).get('Version',''))
except Exception: print('')
" 2>/dev/null)"
log " Version=$got_version (기대 $EXPECT_VERSION)"
if [ -z "$got_version" ]; then
log "::error:: /api/version 응답을 파싱하지 못함: $ver_json"
exit 1
fi
case "$got_version" in
"$EXPECT_VERSION"*) : ;;
*) log "::error:: appVersion 불일치 — 기대 $EXPECT_VERSION, 실제 $got_version"; exit 1 ;;
esac
if [ -n "$ADMIN_PASSWORD" ]; then
log "== 기능 확인: admin 로그인 =="
token="$(curl -sk --max-time 10 -X POST "http://localhost:${LOCAL_PORT}/api/v1/session" \
-H 'Content-Type: application/json' \
-d "$(python3 -c "
import json,os
# dict() 로 만든다 — {'a':1,'b':2} 리터럴은 bash brace expansion 에 쪼개진다(실측).
print(json.dumps(dict(username='admin', password=os.environ['ADMIN_PASSWORD'])))
")" 2>>"$LOG" | python3 -c "
import json,sys
try: print(json.load(sys.stdin).get('token',''))
except Exception: print('')
" 2>/dev/null)"
if [ -z "$token" ]; then
log "::error:: admin 로그인 실패 — 비밀번호(bcrypt 해시)가 맞지 않거나 admin 계정이 비활성"
exit 1
fi
log " 로그인 OK (token 길이 ${#token})"
else
log "== admin 로그인 검증 생략 (ADMIN_PASSWORD 미지정) =="
fi
log "== 결과: PASS =="
RESULT=0
exit "$RESULT"