- Published on
폐쇄망 쿠버네티스 Day 2 운영 — 인증서 만료로 죽는 클러스터를 막는 법
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며 — 폐쇄망 클러스터는 대개 인증서로 죽습니다
- 인증서 — 탐지가 먼저이고 갱신은 그다음입니다
- 시간과 이름 — NTP, 내부 DNS, OS 패키지 미러
- etcd 백업과 리허설된 복구
- 업스트림 없는 업그레이드 — 스테이징 번들과 스큐 규칙
- SaaS 없는 관측성과 반출 가능한 서포트 번들
- 일간·주간·분기 점검표
- 마치며 — 폐쇄망 운영은 달력 위에서 이루어집니다
- 참고 자료
들어가며 — 폐쇄망 클러스터는 대개 인증서로 죽습니다
폐쇄망 클러스터 장애 대응을 몇 번 하다 보면 패턴이 보입니다. 화려한 장애는 오히려 드뭅니다. 압도적으로 많은 것은 이런 형태입니다.
작년에 구축해서 잘 돌던 클러스터에 어느 날 kubectl이 안 됩니다. 에러는 인증서 관련 메시지 한 줄입니다. 검색을 하려는데 인터넷이 없습니다. 사내 위키에는 구축 절차만 있고 갱신 절차는 없습니다. 구축한 사람은 퇴사했습니다. API 서버가 안 뜨니 로그도 파드 로그로 못 봅니다.
이 시나리오의 원인은 언제나 같습니다. 인증서가 1년 만기이고, 그 1년 동안 아무도 클러스터를 재기동하지 않았습니다. 인터넷이 되는 환경이라면 이 문제도 결국 검색 몇 번으로 풀리지만, 폐쇄망에서는 복구까지 며칠이 걸립니다.
이 글은 설치 다음 날부터의 이야기입니다. 검증 기준은 아래와 같습니다.
| 항목 | 확인한 값 | 확인 시점 | 출처 |
|---|---|---|---|
| kubeadm 클라이언트 인증서 | 기본 1년 | 2026-07-31 | kubeadm certs |
| kubeadm CA 인증서 | 기본 10년 | 2026-07-31 | kubeadm certs |
| k3s 클라이언트·서버 인증서 | 365일, 재기동 시 만료 120일 이내면 자동 갱신 | 2026-07-31 | k3s certificate |
| k3s 자동 갱신 임계값 변경 | 2025년 5월 릴리스 이전에는 90일 | 2026-07-31 | k3s certificate |
| k3s 스냅샷 기본 보존 개수 | 5 | 2026-07-31 | k3s etcd-snapshot |
인증서 — 탐지가 먼저이고 갱신은 그다음입니다
갱신 명령을 외우는 것보다 중요한 것은 만료가 다가온다는 사실을 미리 아는 것입니다. 만료 당일에 갱신 명령을 아는 것은 별 도움이 안 됩니다. 그때는 이미 서비스가 멈춘 뒤입니다.
탐지
kubeadm 클러스터에서는 전용 명령이 있습니다.
# 컨트롤 플레인 노드에서
sudo kubeadm certs check-expiration
출력에는 인증서별 만료일과 남은 기간, 서명한 CA, 그리고 외부에서 관리되는 인증서인지가 표시됩니다. 중요한 것은 kubelet 클라이언트 인증서가 이 출력에 나오지 않는다는 점입니다. 공식 문서는 kubelet 클라이언트 인증서가 자동 회전으로 관리되며 kubelet 디렉터리 아래에 따로 있다고 설명합니다. 별도로 봐야 합니다.
# kubelet 인증서는 따로 확인합니다
sudo ls -la /var/lib/kubelet/pki/
sudo openssl x509 -in /var/lib/kubelet/pki/kubelet-client-current.pem -noout -enddate
k3s에는 전용 서브커맨드가 있습니다. 출력 형식을 지정할 수 있어 스크립트에 넣기 좋습니다.
sudo k3s certificate check --output table
그리고 k3s는 만료가 다가오면 쿠버네티스 이벤트를 남깁니다. 공식 문서 기준 이유(reason)는 CertificateExpirationWarning이고, 해당 노드에 연결됩니다. 이 이벤트를 알림으로 잡아 두면 탐지가 자동화됩니다.
# 만료 경고 이벤트 조회
sudo k3s kubectl get events -A --field-selector reason=CertificateExpirationWarning
배포판과 무관하게 동작하는 탐지 스크립트를 하나 두고 cron에 걸어 두는 것을 권합니다. 폐쇄망에서는 외부 모니터링 SaaS가 없으므로, 이 스크립트가 사실상 유일한 조기 경보입니다.
#!/usr/bin/env bash
# cert-watch.sh — 매일 실행. 만료 60일 이내면 비정상 종료로 알립니다.
set -uo pipefail
THRESHOLD_DAYS=60
NOW=$(date +%s)
WARN=0
check_pem() {
local f="$1"
[ -f "${f}" ] || return 0
local end epoch left
end=$(openssl x509 -in "${f}" -noout -enddate 2>/dev/null | cut -d= -f2) || return 0
epoch=$(date -d "${end}" +%s 2>/dev/null) || return 0
left=$(( (epoch - NOW) / 86400 ))
if [ "${left}" -lt "${THRESHOLD_DAYS}" ]; then
echo "WARN ${left}일 남음 ${f} (${end})"
WARN=$((WARN+1))
else
echo "ok ${left}일 남음 ${f}"
fi
}
# kubeadm 계열
for f in /etc/kubernetes/pki/*.crt /etc/kubernetes/pki/etcd/*.crt; do
check_pem "${f}"
done
# kubelet
check_pem /var/lib/kubelet/pki/kubelet-client-current.pem
# k3s 계열
for f in /var/lib/rancher/k3s/server/tls/*.crt; do
check_pem "${f}"
done
echo "경고 ${WARN}건"
exit "${WARN}"
kubeadm 클러스터 갱신
kubeadm에는 실무적으로 중요한 성질이 하나 있습니다. kubeadm upgrade를 실행하면 모든 인증서가 자동으로 갱신됩니다. 즉 정기 업그레이드를 수행하는 클러스터는 인증서 문제를 거의 겪지 않습니다. 폐쇄망 클러스터가 인증서로 죽는 이유도 여기에 있습니다. 반입 심의 때문에 업그레이드를 안 하니 갱신 기회도 없는 것입니다.
수동 갱신은 다음과 같습니다.
# 전체 갱신
sudo kubeadm certs renew all
# 개별 갱신도 가능합니다
sudo kubeadm certs renew apiserver
sudo kubeadm certs renew admin.conf
공식 문서가 명시하는 중요한 주의사항이 있습니다. 수동 갱신 후에는 컨트롤 플레인 컴포넌트를 직접 재시작해야 합니다. kubeadm이 알아서 재시작해 주지 않습니다. 갱신만 하고 재시작을 잊으면, 명령은 성공했는데 서비스는 여전히 옛 인증서를 들고 있는 상태가 됩니다.
# 스태틱 파드를 재기동시키는 가장 단순한 방법 — 매니페스트를 잠깐 옮겼다 되돌립니다
sudo mkdir -p /tmp/kube-manifests
sudo mv /etc/kubernetes/manifests/*.yaml /tmp/kube-manifests/
sleep 20
sudo mv /tmp/kube-manifests/*.yaml /etc/kubernetes/manifests/
# 새 admin.conf 를 반영
sudo cp /etc/kubernetes/admin.conf "${HOME}/.kube/config"
sudo chown "$(id -u):$(id -g)" "${HOME}/.kube/config"
# 확인
sudo kubeadm certs check-expiration
kubectl get nodes
유효 기간 자체를 늘리고 싶다면 클러스터 설정에서 지정합니다. 폐쇄망처럼 재기동 기회가 드문 환경에서는 검토할 가치가 있습니다.
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
certificateValidityPeriod: 8760h # 기본 365일
caCertificateValidityPeriod: 87600h # 기본 3650일
값은 Go의 duration 형식이며 지원되는 가장 큰 단위는 시간입니다. 다만 유효 기간을 늘리는 것은 회전 주기를 늦추는 것이므로, 보안 정책과 충돌하지 않는지 먼저 확인하십시오. 그리고 외부 CA를 쓰는 경우에는 kubeadm이 인증서를 관리할 수 없습니다. 이 경우 kubeadm certs generate-csr로 CSR을 만들어 외부에 서명을 받아야 하며, 갱신은 전적으로 수동입니다.
k3s 갱신
k3s는 서비스 재기동 시 만료 120일 이내인 인증서를 자동으로 갱신합니다(2025년 5월 릴리스 이전에는 90일). 이 동작은 기존 키를 재사용하면서 유효 기간만 늘리는 방식입니다.
# 자동 갱신을 유도하는 가장 단순한 방법 — 재기동
sudo systemctl restart k3s
# 명시적 회전
sudo systemctl stop k3s
sudo k3s certificate rotate
sudo systemctl start k3s
# 특정 서비스만 회전
sudo k3s certificate rotate --service api-server,etcd,kubelet
회전 가능한 서비스 이름은 공식 문서에 admin, api-server, controller-manager, scheduler, k3s-controller, k3s-server, cloud-controller, etcd, auth-proxy, kubelet, kube-proxy로 나열되어 있습니다.
CA는 별개입니다. k3s가 최초 기동 시 생성하는 자체 서명 CA는 10년 유효하며 자동 갱신되지 않습니다. 회전에는 전용 명령이 있습니다.
# 자체 서명 CA 회전 — 교차 서명으로 신뢰 체인을 유지합니다
# (스크립트는 반입 시 함께 들여와야 합니다. 폐쇄망에서 curl 로 못 받습니다.)
sudo bash /opt/staging/rotate-default-ca-certs.sh
sudo k3s certificate rotate-ca --path=/var/lib/rancher/k3s/server/rotate-ca
# 사내 CA 로 교체하는 경우 — 별도 디렉터리에 준비한 뒤 적용
sudo k3s certificate rotate-ca --path=/opt/k3s/server
# 적용 후 모든 노드에서 k3s 를 재기동합니다.
# 루트 CA 자체를 바꾸면 --force 가 필요하며, 조인된 노드의 토큰 재설정이 선행되어야 합니다.
rotate-default-ca-certs.sh는 k3s 저장소에 있는 스크립트입니다. 폐쇄망에서는 인터넷에서 받을 수 없으므로 최초 반입 목록에 넣어 두어야 합니다. 필요해진 시점에 없다는 것을 알게 되는 대표적인 항목입니다.
시간과 이름 — NTP, 내부 DNS, OS 패키지 미러
인증서 다음으로 폐쇄망 클러스터를 조용히 무너뜨리는 것이 시각입니다. 그리고 시각 문제의 증상은 항상 시각 문제처럼 보이지 않습니다.
TLS 검증은 인증서의 유효 시작·종료 시각을 현재 시각과 비교합니다. 노드 시계가 몇 분만 어긋나도 "인증서가 아직 유효하지 않다"거나 "이미 만료되었다"는 오류가 납니다. etcd는 더 민감합니다. 리스 만료와 리더 선출이 시간에 기반하므로, 노드 간 편차가 커지면 리더가 반복적으로 바뀌면서 쓰기 지연이 튀고 최악의 경우 정족수 판단이 흔들립니다.
폐쇄망에서 이 문제가 특히 잘 생기는 이유는 명확합니다. 기본 NTP 설정이 공인 NTP 풀을 가리키고, 그건 폐쇄망에서 도달할 수 없습니다. 설치 당시에는 하드웨어 시계가 맞아 있으니 아무도 눈치채지 못하고, 몇 달에 걸쳐 서서히 벌어집니다.
# 현재 동기화 상태 확인 — 여기가 폐쇄망 점검의 1번 항목입니다
timedatectl status
chronyc sources -v 2>/dev/null || ntpq -p 2>/dev/null
chronyc tracking 2>/dev/null
# 사내 NTP 서버로 고정
sudo tee /etc/chrony.d/internal.conf > /dev/null <<'EOF'
server 10.10.10.11 iburst prefer
server 10.10.10.12 iburst
makestep 1.0 3
EOF
# 공인 풀을 가리키는 기본 설정을 제거해야 합니다 (배포판에 따라 경로가 다릅니다)
sudo grep -rn "pool\|ntp.org" /etc/chrony.conf /etc/chrony.d/ 2>/dev/null
sudo systemctl restart chronyd
sleep 5
chronyc sources -v
# 클러스터 전체 노드의 시각 편차를 한 번에 보기
for n in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
printf "%-20s " "${n}"
ssh "${n}" "chronyc tracking 2>/dev/null | awk '/System time/{print \$4, \$5, \$6}'" || echo "unreachable"
done
내부 DNS도 같은 성격의 문제입니다. CoreDNS는 클러스터 도메인 밖의 질의를 노드 리졸버로 전달하는데, 그 리졸버가 도달 불가한 공인 DNS를 가리키면 질의마다 타임아웃을 기다립니다. 증상은 "느리다"로 나타나고, 원인이 DNS라는 것을 알아내는 데 시간이 걸립니다.
# CoreDNS 가 무엇을 바라보는지 확인
kubectl -n kube-system get configmap coredns -o yaml
# 노드 리졸버
cat /etc/resolv.conf
# 사내 DNS 응답 확인
dig @10.10.10.53 registry.internal.example +short
OS 패키지 미러는 인증서·NTP만큼 급하지는 않지만, 없으면 어느 날 갑자기 아무 작업도 못 하게 됩니다. 커널 보안 패치, 컨테이너 런타임 의존 패키지, 디버깅 도구(tcpdump, strace, zstd) 설치가 전부 막힙니다. 폐쇄망 구축 시 사내 패키지 미러를 함께 세우고, 클러스터 노드가 그것만 바라보게 설정하십시오. 미러 자체의 동기화가 정기 반입 대상에 들어가야 합니다.
etcd 백업과 리허설된 복구
백업은 대부분의 팀이 합니다. 복구는 대부분의 팀이 해 본 적이 없습니다. 폐쇄망에서 이 차이는 치명적입니다. 복구 절차를 처음 실행하는 시점이 장애 한복판이면, 문서를 검색할 수도 없고 외부에 물어볼 수도 없습니다.
k3s는 스냅샷 기능이 내장되어 있습니다. 공식 문서 기준 기본값은 12시간 간격 스케줄, 보존 5개, 저장 위치는 데이터 디렉터리 아래 db/snapshots입니다.
# /etc/rancher/k3s/config.yaml — 폐쇄망 권장 설정
etcd-snapshot-schedule-cron: '0 */6 * * *'
etcd-snapshot-retention: 12
etcd-snapshot-compress: true
etcd-snapshot-dir: /var/backups/k3s-snapshots
# 온디맨드 스냅샷 — 업그레이드나 위험한 변경 직전에 반드시
sudo k3s etcd-snapshot save
# 목록과 정리
sudo k3s etcd-snapshot ls
sudo k3s etcd-snapshot prune --snapshot-retention 12
sudo k3s etcd-snapshot delete on-demand-node1-1753900000
폐쇄망에서 반드시 함께 백업해야 하는 파일이 하나 더 있습니다. 공식 문서가 명시적으로 경고하는 항목입니다. 서버 토큰입니다. 토큰이 기밀 데이터 암호화에 쓰이므로, 복구 시 같은 토큰 값이 없으면 스냅샷을 쓸 수 없습니다.
# 스냅샷과 토큰을 한 세트로 보관합니다
sudo install -m 0600 /var/lib/rancher/k3s/server/token \
/var/backups/k3s-snapshots/token-$(date +%Y%m%d)
# 백업 세트를 클러스터 밖 스토리지로 (폐쇄망 내부의 별도 NAS 등)
sudo rsync -a --delete /var/backups/k3s-snapshots/ backup-nas:/k3s/prod/
복구 절차입니다. 이 절차는 분기마다 스테이징에서 실제로 실행해 보십시오. 읽어 본 것과 해 본 것은 다릅니다.
# 단일 서버
sudo systemctl stop k3s
sudo k3s server \
--cluster-reset \
--cluster-reset-restore-path=/var/backups/k3s-snapshots/etcd-snapshot-node1-1753900000
# 위 명령이 완료 메시지를 내면 Ctrl-C 로 빠져나온 뒤
sudo systemctl start k3s
# 다중 서버 — 순서를 지켜야 합니다
# 1) 모든 서버에서 k3s 중지
sudo systemctl stop k3s
# 2) 첫 번째 서버에서만 cluster-reset 복구 수행 (위 명령)
# 3) 첫 번째 서버 기동
sudo systemctl start k3s
# 4) 나머지 서버에서 데이터 디렉터리 삭제 — 이 단계를 빠뜨리면 합류하지 못합니다
sudo rm -rf /var/lib/rancher/k3s/server/db/
# 5) 나머지 서버 기동
sudo systemctl start k3s
kubeadm 클러스터라면 etcdctl을 직접 씁니다.
# 스냅샷
sudo ETCDCTL_API=3 etcdctl snapshot save /var/backups/etcd-$(date +%Y%m%d%H%M).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
# 스냅샷 무결성 확인 — 저장만 하고 확인 안 하는 팀이 정말 많습니다
sudo ETCDCTL_API=3 etcdctl snapshot status \
/var/backups/etcd-*.db --write-out=table
복구 리허설을 분기 일정에 넣고, 리허설 결과를 문서에 기록하십시오. 기록해야 할 것은 성공 여부가 아니라 걸린 시간과 막혔던 지점입니다. 그것이 실제 장애 시의 복구 목표 시간이 됩니다.
업스트림 없는 업그레이드 — 스테이징 번들과 스큐 규칙
폐쇄망 업그레이드는 "새 아티팩트를 반입해서 교체"입니다. 절차 자체는 어렵지 않은데, 어려운 것은 계획입니다.
가장 먼저 확인할 것은 버전 스큐 정책입니다. 컨트롤 플레인과 kubelet, kubectl 사이에 허용되는 버전 차이는 릴리스마다 갱신되므로, 반입 계획을 세우기 전에 공식 버전 스큐 정책 문서에서 대상 버전의 허용 범위를 확인하고 그 값을 반입 계획서에 명시적으로 적어 두십시오. 이 값을 기억에 의존해 적으면 반드시 틀립니다.
그리고 컨트롤 플레인은 마이너 버전을 건너뛰어 올릴 수 없습니다. 반입 심의가 분기에 한 번인데 마이너 버전이 그사이 두 번 올랐다면, 한 번의 반입에 두 단계 업그레이드를 위한 아티팩트를 모두 담아야 합니다. 이 계산을 미리 하지 않으면 반입 매체를 받아 놓고 절반만 올릴 수 있는 상황이 됩니다.
# 반입 계획 산출 — 현재 버전과 목표 버전 사이의 단계 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.kubeletVersion}{"\n"}{end}'
kubectl version -o json | jq -r '.serverVersion.gitVersion'
스테이징 번들이라는 개념을 도입하십시오. 반입 매체 하나에 한 번의 업그레이드 회차에 필요한 모든 것을 담고, 그 안에서 버전이 어긋나지 않도록 매니페스트로 고정하는 방식입니다.
/opt/staging/upgrade-2026Q3/
├── VERSION # 목표 버전 한 줄
├── MANIFEST.sha256
├── step1-v1.35.7/
│ ├── k3s
│ └── k3s-airgap-images-amd64.tar.zst
├── step2-v1.36.2/
│ ├── k3s
│ └── k3s-airgap-images-amd64.tar.zst
├── addons/ # 이 회차에 함께 올릴 부가 컴포넌트 이미지
└── ROLLBACK.md # 각 단계별 롤백 판단 기준과 명령
업그레이드 실행 전 체크리스트는 짧아야 실제로 지켜집니다.
#!/usr/bin/env bash
# pre-upgrade.sh
set -uo pipefail
ERR=0
echo "[1] 스냅샷"
sudo k3s etcd-snapshot save || ERR=$((ERR+1))
echo "[2] 토큰 백업"
sudo cp /var/lib/rancher/k3s/server/token "/var/backups/token-$(date +%Y%m%d%H%M)" || ERR=$((ERR+1))
echo "[3] 번들 무결성"
(cd /opt/staging/upgrade-2026Q3 && sha256sum -c MANIFEST.sha256) || ERR=$((ERR+1))
echo "[4] 노드 상태"
kubectl get nodes --no-headers | grep -v " Ready " && ERR=$((ERR+1))
echo "[5] PodDisruptionBudget"
kubectl get pdb -A
echo "[6] 인증서 잔여 기간"
sudo k3s certificate check --output table
echo "오류 ${ERR}건"
exit "${ERR}"
업그레이드 자체는 아카이브 교체와 바이너리 교체입니다. 이때 옛 아카이브를 반드시 지워야 합니다. 남겨 두면 이미지 디렉터리에 두 버전이 공존하면서 예측하기 어려운 상태가 됩니다.
sudo systemctl stop k3s
sudo rm -f /var/lib/rancher/k3s/agent/images/k3s-airgap-images-amd64.tar.zst
sudo rm -f /var/lib/rancher/k3s/agent/images/.cache.json
sudo cp /opt/staging/upgrade-2026Q3/step2-v1.36.2/k3s-airgap-images-amd64.tar.zst \
/var/lib/rancher/k3s/agent/images/
sudo install -m 0755 /opt/staging/upgrade-2026Q3/step2-v1.36.2/k3s /usr/local/bin/k3s
sudo systemctl start k3s
sudo k3s kubectl get nodes -o wide
SaaS 없는 관측성과 반출 가능한 서포트 번들
폐쇄망에서는 관측 스택도 전부 안쪽에 있어야 합니다. 원격 쓰기 대상도, 대시보드 마켓플레이스도, 알림 수신처도 없습니다. 설계할 때 결정해야 할 것은 세 가지입니다.
첫째, 대시보드와 룰은 반입물입니다. Grafana 대시보드를 UI에서 가져오기로 받는 경로는 폐쇄망에서 동작하지 않습니다. 수집 단계에서 JSON을 내려받아 반입하고, 프로비저닝으로 배포하십시오.
# DMZ 수집 단계 — 대시보드 JSON 을 파일로 확보
mkdir -p dashboards
curl -fL -o dashboards/node-exporter.json \
"https://grafana.com/api/dashboards/1860/revisions/latest/download"
sha256sum dashboards/*.json >> MANIFEST.sha256
# 폐쇄망 내부 — 프로비저닝으로 배포 (컨피그맵 마운트 방식)
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-provider
namespace: monitoring
data:
provider.yaml: |
apiVersion: 1
providers:
- name: airgap
orgId: 1
folder: 'Airgap'
type: file
disableDeletion: true
options:
path: /var/lib/grafana/dashboards
둘째, 알림 수신처를 내부 시스템으로 정하십시오. 외부 메신저나 페이징 SaaS로는 나갈 수 없습니다. 실무적으로 남는 선택지는 사내 SMTP, 사내 메신저의 웹훅 엔드포인트, 사내 티켓 시스템 API 정도입니다. 셋 다 없다면 최후 수단은 노드에서 로컬 로그를 남기고 사람이 주기적으로 확인하는 것인데, 그러면 알림이 아니라 점검입니다. 알림 경로가 없는 관측 스택은 대시보드일 뿐 모니터링이 아닙니다.
# Alertmanager — 사내 SMTP 로만 나가는 구성
route:
receiver: internal-mail
group_wait: 30s
repeat_interval: 4h
receivers:
- name: internal-mail
email_configs:
- to: 'k8s-ops@internal.example'
from: 'alertmanager@internal.example'
smarthost: 'smtp.internal.example:25'
require_tls: false
셋째, 서포트 번들을 만들 수 있어야 합니다. 폐쇄망에서 벤더나 외부 전문가의 도움을 받는 유일한 방법은, 진단에 필요한 정보를 아카이브로 만들어 반출 심의를 거쳐 밖으로 들고 나가는 것입니다. 그런데 아무 준비 없이 만들면 두 가지 문제가 생깁니다. 필요한 정보가 빠져 있거나, 반출하면 안 되는 정보가 들어 있어 심의에서 반려됩니다.
#!/usr/bin/env bash
# support-bundle.sh — 반출 심의를 고려한 진단 번들
set -uo pipefail
TS="$(date +%Y%m%d-%H%M)"
DIR="/var/tmp/support-${TS}"
mkdir -p "${DIR}"/{cluster,nodes,logs}
K="sudo k3s kubectl"
echo "== 클러스터 리소스 (시크릿 제외)"
${K} get nodes -o wide > "${DIR}/cluster/nodes.txt" 2>&1
${K} get pods -A -o wide > "${DIR}/cluster/pods.txt" 2>&1
${K} get events -A --sort-by=.lastTimestamp > "${DIR}/cluster/events.txt" 2>&1
${K} describe nodes > "${DIR}/cluster/nodes-describe.txt" 2>&1
${K} api-resources > "${DIR}/cluster/api-resources.txt" 2>&1
${K} version -o json > "${DIR}/cluster/version.json" 2>&1
# 시크릿은 이름만 — 값은 절대 담지 않습니다
${K} get secrets -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,TYPE:.type \
> "${DIR}/cluster/secrets-list.txt" 2>&1
echo "== 실패 중인 파드의 로그만"
${K} get pods -A --field-selector=status.phase!=Running --no-headers 2>/dev/null \
| while read -r ns name _; do
${K} -n "${ns}" logs "${name}" --all-containers --tail=500 \
> "${DIR}/logs/${ns}_${name}.log" 2>&1 || true
${K} -n "${ns}" describe pod "${name}" \
> "${DIR}/logs/${ns}_${name}.describe" 2>&1 || true
done
echo "== 노드 정보"
uname -a > "${DIR}/nodes/uname.txt" 2>&1
free -h > "${DIR}/nodes/mem.txt" 2>&1
df -h > "${DIR}/nodes/disk.txt" 2>&1
timedatectl status > "${DIR}/nodes/time.txt" 2>&1
chronyc tracking > "${DIR}/nodes/chrony.txt" 2>&1
sudo journalctl -u k3s -n 5000 --no-pager > "${DIR}/logs/k3s.log" 2>&1
sudo tail -n 5000 /var/lib/rancher/k3s/agent/containerd/containerd.log \
> "${DIR}/logs/containerd.log" 2>&1
sudo k3s certificate check --output table > "${DIR}/cluster/certs.txt" 2>&1
echo "== 반출 전 점검 — 토큰과 키 패턴 탐지"
grep -rIlE 'BEGIN (RSA |EC )?PRIVATE KEY|K10[0-9a-f]{16,}|password:\s*\S+' "${DIR}" \
> "${DIR}/REDACTION-REVIEW.txt" 2>/dev/null || true
echo "다음 파일을 반출 전 반드시 검토하십시오:"
cat "${DIR}/REDACTION-REVIEW.txt"
tar -czf "/var/tmp/support-${TS}.tar.gz" -C /var/tmp "support-${TS}"
sha256sum "/var/tmp/support-${TS}.tar.gz"
echo "생성 완료: /var/tmp/support-${TS}.tar.gz"
마지막 단계의 민감 정보 탐지를 반드시 넣으십시오. 서포트 번들에 서버 토큰이나 개인 키가 섞여 나가면, 반출 심의에서 반려되는 것으로 끝나면 다행이고 최악의 경우 보안 사고로 처리됩니다. 탐지 결과가 비어 있는지 사람이 눈으로 확인한 뒤에 반출 신청을 올리십시오.
일간·주간·분기 점검표
폐쇄망 운영에서 자동화할 수 없는 것들은 결국 사람이 주기적으로 봐야 합니다. 주기를 나누는 기준은 "이것이 방치되면 얼마 만에 서비스가 멈추는가"입니다.
| 주기 | 점검 항목 | 확인 명령 또는 방법 | 방치했을 때 |
|---|---|---|---|
| 일간 | 노드 Ready 상태 | kubectl get nodes | 워크로드가 조용히 한쪽으로 몰림 |
| 일간 | 비정상 파드 | kubectl get pods -A 상태 필터 | 재시도 루프가 노드 리소스를 잠식 |
| 일간 | 노드 디스크 사용률 | df -h 와 이미지 GC 임계값 | 이미지 pull 불가 — 폐쇄망에서는 복구가 느림 |
| 일간 | 시각 동기화 상태 | chronyc tracking | TLS 검증 실패와 etcd 불안정 |
| 주간 | 인증서 잔여 기간 | cert-watch.sh 또는 certificate check | 만료 당일 클러스터 정지 |
| 주간 | etcd 스냅샷 생성 여부와 크기 | etcd-snapshot ls 와 파일 크기 추이 | 백업이 있다고 믿었는데 없음 |
| 주간 | 취약점 DB 신선도 | 반입 대장의 마지막 Trivy DB 반입일 | 스캔 결과가 무의미해짐 |
| 주간 | 이미지 반입 대장과 실제 레지스트리 대조 | 반입 로그와 레지스트리 태그 목록 비교 | 출처 불명 이미지가 클러스터에 존재 |
| 월간 | OS 보안 패치 적용 | 사내 패키지 미러 기준 갱신 목록 | 커널 취약점 누적 |
| 월간 | base 이미지 재반입 | 반입 파이프라인 실행 | 파생 이미지 전체가 같은 취약점 공유 |
| 분기 | etcd 복구 리허설 | 스테이징에서 cluster-reset 복구 실행 | 실제 장애 때 처음 해 보게 됨 |
| 분기 | 버전 업그레이드 반입과 적용 | 스테이징 번들 반입 후 단계별 적용 | 스큐가 벌어져 한 번에 못 올림 |
| 분기 | 서포트 번들 생성 훈련 | support-bundle.sh 실행과 반출 심의 절차 확인 | 장애 때 번들 형식으로 반려되어 시간 손실 |
| 반기 | CA 만료 확인과 회전 계획 점검 | CA 인증서 enddate 확인 | 10년 뒤 한 번에 전 클러스터 정지 |
| 반기 | 반입 목록과 실제 사용 목록 대조 | 목록 파일과 클러스터 이미지 목록 diff | 안 쓰는 것을 나르고 쓰는 것을 빠뜨림 |
이 표에서 가장 자주 건너뛰는 항목이 분기의 복구 리허설입니다. 그리고 그것이 실제 장애 때 가장 비싸게 돌아옵니다. 리허설은 클러스터를 하나 새로 만들어서 해도 되고, 스냅샷을 별도 노드에서 복원해 보는 것만으로도 상당 부분 확인됩니다.
마치며 — 폐쇄망 운영은 달력 위에서 이루어집니다
폐쇄망 클러스터의 Day 2 운영에는 특별한 기술이 별로 없습니다. 인증서 갱신 명령도, 스냅샷 복구 절차도, 업그레이드 순서도 전부 공식 문서에 적혀 있습니다. 그런데도 클러스터가 죽는 이유는 그 명령들이 필요한 시점에 실행되지 않기 때문입니다.
인터넷이 되는 환경에서는 이 문제가 잘 드러나지 않습니다. 문제가 생겨도 검색하고, 도구를 받고, 며칠이면 복구합니다. 폐쇄망에서는 같은 문제가 몇 주가 됩니다. 그래서 폐쇄망 운영의 실력은 사고 대응 능력이 아니라 달력을 지키는 능력으로 나타납니다.
점검표를 캘린더에 넣고, 담당자를 지정하고, 리허설 결과를 기록하십시오. 그 세 가지가 인증서 만료로 죽는 클러스터와 5년을 조용히 돌아가는 클러스터를 가릅니다.