장애 대응과 자주 쓰는 명령. 증상에서 출발해 원인을 찾는 순서로 정리했다.
ssh ubuntu@"${SERVER_HOST}"SERVER_HOST는 공개 문서가 아니라 승인된 운영 인벤토리에서 확인한다.
k3s가 /usr/local/bin/kubectl을 심볼릭 링크로 만들어둔다. root면 그대로 되고,
일반 사용자로 쓰려면:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
kubeseal등 일부 도구는kubectl과 달리 KUBECONFIG를 자동 감지하지 못한다.export KUBECONFIG=/etc/rancher/k3s/k3s.yaml를 명시할 것.
인터넷에 노출하지 않는다. 공개된 ArgoCD + 팀 공용 비밀번호는 클러스터 전체를 내주는 것과 같다.
# 로컬 노트북에서
ssh -L 8080:localhost:8080 ubuntu@"${SERVER_HOST}"
# 서버에서
sudo k3s kubectl port-forward svc/argocd-server -n argocd 8080:80→ 브라우저 http://localhost:8080
# 배포 상태 — 여기가 전부 Synced/Healthy면 정상
kubectl -n argocd get applications
# 파드
kubectl get pods -A
# 자원 — metrics-server가 Ready일 때만 kubectl top 사용
kubectl -n kube-system get deployment metrics-server
kubectl top nodes
kubectl top pods -A --sort-by=memory
# 저용량 profile(metrics-server replicas 0)의 기본 확인
vmstat 1 6
cat /proc/pressure/cpu
cat /proc/pressure/memory
# 외부에서 서비스 확인 (반드시 서버 "밖"에서)
curl -I https://pin-log.com/api/<서비스>정상 기준선 (2026-07-20 구축 직후): CPU 13%, 메모리 3.5Gi / 12Gi 여유
kubectl -n pinlog-prod describe pod <파드명> | tail -20| 원인 | 확인 | 해결 |
|---|---|---|
| GHCR 패키지가 private (가장 흔함) | Team-PinLog → Packages |
Package settings → Change visibility → Public |
| 태그가 실제로 없음 | Packages에서 태그 목록 확인 | CI 빌드 성공 여부 확인 |
| 저장소 이름 오타 | values.yaml의 image.repository |
수정 후 커밋 |
새 서비스의 첫 푸시는 항상 private으로 생성된다. GitHub 기본 동작이라 피할 수 없고, 서비스마다 한 번씩 public 전환이 필요하다.
거의 항상 비루트 사용자 누락이다. 차트가 runAsNonRoot: true,
runAsUser: 1000을 강제하는데 이미지에 UID 1000이 없는 경우.
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app
USER 1000Secret 참조 오류일 수도 있다:
kubectl -n pinlog-prod describe pod <파드명> | grep -A5 Events프로브 실패로 Ready가 안 된 것이다. READY 열이 0/1인지 확인:
kubectl -n pinlog-prod get pods
kubectl -n pinlog-prod describe pod <파드명> | grep -A10 "Liveness\|Readiness"가장 흔한 원인은 context-path와 프로브 경로 불일치다.
context-path: /api/auth면 actuator 경로는 /api/auth/actuator/health가 된다.
차트 기본값 /actuator/health로는 404를 받는다.
# apps/prod/<서비스>/values.yaml
probes:
path: /api/<서비스>/actuator/health직접 확인:
kubectl -n pinlog-prod exec <파드명> -- wget -qO- http://localhost:8080/api/<서비스>/actuator/health포트 불일치도 확인할 것 — service.targetPort와 앱의 실제 리슨 포트.
경로 매칭 문제다.
# Ingress가 실제로 어떤 경로를 잡고 있는지
kubectl -n pinlog-prod get ingress -o wide
kubectl -n pinlog-prod describe ingress <서비스>체크리스트:
values.yaml의ingress.path와 앱의context-path가 동일한가- 컨트롤러가 경로를 중복해 붙이지 않았는가
(
context-path: /api/hello+@GetMapping("/api/hello")→/api/hello/api/hello) - 후행 슬래시 — Spring 6부터 자동 매칭이 제거되었다.
@GetMapping({"", "/"})
kubectl -n pinlog-prod describe pod <파드명> | grep -i -A3 "last state\|reason"
kubectl top pods -n pinlog-prodOOMKilled면:
- JVM이 cgroup 한도를 모르는 경우 —
JAVA_TOOL_OPTIONS에-XX:MaxRAMPercentage=70이 있는지 확인. 없으면 힙을 한도 너머로 잡는다 resources.limits.memory상향 — 단, swap이 없어서 메모리 압박은 느려짐이 아니라 즉사다. 전체 예산(§ architecture.md 4장)을 보고 올릴 것
기본 폴링 주기가 최대 3분이다. 먼저 기다려보고:
kubectl -n argocd get app <앱명> -o jsonpath='{.status.sync.status}{"\n"}'
kubectl -n argocd get app <앱명> -o jsonpath='{range .status.conditions[*]}{.type}: {.message}{"\n"}{end}'| 메시지 | 원인 |
|---|---|
InvalidSpecError: ... do not match any of the allowed destinations |
AppProject destinations에 해당 네임스페이스 누락 |
rpc error ... repository not found |
저장소 접근 실패 (public인지 확인) |
ComparisonError |
매니페스트 문법 오류 — 로컬에서 helm template으로 재현 |
강제 동기화:
kubectl -n argocd patch app <앱명> --type merge \
-p '{"operation":{"sync":{"revision":"main"}}}'ufw 포워딩 규칙 문제일 가능성이 높다.
# 카나리아 — 반드시 FQDN을 쓸 것
kubectl run dns-test --image=busybox@sha256:b7f3d86d6e84fc17718c48bcde1450807faa2d56704205c697b4bd5df7b9e29f --rm -i --restart=Never -- \
nslookup kubernetes.default.svc.cluster.local
⚠️ 짧은 이름(kubernetes.default)으로 확인하면 busybox가 search domain을 제대로 적용하지 못해 정상 클러스터에서도 NXDOMAIN이 나온다.
실패하면:
sudo ufw status | grep -i fwd # ALLOW FWD 규칙 4개가 보여야 정상
sudo /root/infra/bootstrap/00-preflight.sh# 반드시 서버 "밖"에서. 안에서 하면 보안그룹 문제가 가려진다
openssl s_client -connect pin-log.com:443 \
-servername pin-log.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -datesVerify return code: 0이어야 한다. 체인이 불완전하면 fullchain.pem이 아니라
cert.pem을 쓴 것이다.
# Secret 재동기화
sudo /opt/pinlog/bootstrap/sync-tls-secret.sh
# 인증서 개수 확인 — 1개면 리프만 있는 것(잘못됨), 여러 개여야 정상
kubectl -n kube-system get secret pinlog-wildcard-tls \
-o jsonpath='{.data.tls\.crt}' | base64 -d | grep -c "BEGIN CERTIFICATE"
# 타이머 상태
systemctl status pinlog-tls-sync.timer
journalctl -u pinlog-tls-sync.service -n 20설정을 뒤지기 전에 장애부터 확인한다. 구축 중 실제로 startup_failure가
두 번 났는데 전부 GitHub 측 장애였다.
curl -s https://www.githubstatus.com/api/v2/summary.json \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['status']['description']); [print(c['name'], c['status']) for c in d['components'] if c['name'] in ('Actions','API Requests')]"GitHub가 정상이면 PR의 required checks를 본다.
gh pr checks <PR번호>
gh run list --limit 10infra/main은 pr-policy, guardrails, helm이 모두 성공해야 merge된다. check를
우회하지 말고 실패 job의 로그와 Git/CI 거버넌스를 확인한다.
단일 노드에서 root UID의 inotify instance 수가 상한에 도달하면 Kubernetes API
로그 스트림이 선택된 앱의 로그처럼 이 문구를 반환한다. 먼저 instance·watch·전역
파일 테이블을 각각 확인한다. max_user_watches나 fs.file-max가 충분하면 함께
올리지 않는다.
sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches fs.file-max
# bounded probe: 성공하면 fd를 즉시 닫고, 실패하면 errno를 출력한다.
python3 -c 'import ctypes,os; l=ctypes.CDLL(None,use_errno=True); f=l.inotify_init1(os.O_CLOEXEC); e=ctypes.get_errno(); print(f,e,os.strerror(e) if e else "ok"); f >= 0 and os.close(f)'영구 선언의 저장소 정본은 bootstrap/sysctl.d/99-pinlog-inotify.conf, 호스트 소유
경로는 /etc/sysctl.d/99-pinlog-inotify.conf다. 필수 CI와 merge가 끝난 뒤
origin/main의 파일을 직접 추출하고 정확히 일치하는지 확인한 다음, 해당 key만
live 적용한다. 전체 sysctl reload, 재부팅, k3s·Pod restart는 하지 않는다.
cd /root/infra
git fetch origin main
MERGED_REV=$(git rev-parse origin/main)
tmp=$(mktemp)
git show "$MERGED_REV:bootstrap/sysctl.d/99-pinlog-inotify.conf" > "$tmp"
grep -Fx 'fs.inotify.max_user_instances = 512' "$tmp"
sudo install -o root -g root -m 0644 "$tmp" /etc/sysctl.d/99-pinlog-inotify.conf
cmp --silent "$tmp" /etc/sysctl.d/99-pinlog-inotify.conf
rm -f "$tmp"
sudo sysctl -w fs.inotify.max_user_instances=512
sysctl fs.inotify.max_user_instances fs.inotify.max_user_watches fs.file-max512는 고갈 시점의 root 사용량 128개 대비 4배이며 384개 여유를 만든다. watch 수와 전역 file table은 그대로 두므로 장애 경계만 최소 변경한다. 적용 후 bounded watcher, 새 Pod 로그의 Loki 도착, Backend·AI readiness, Alloy/Loki 최근 오류를 확인한다.
kubectl -n monitoring get \
statefulset/alertmanager-kube-prometheus-stack-alertmanager
systemctl is-active pinlog-sentinel-receiver.service
curl --fail --cacert /etc/pinlog-sentinel/tls.crt \
--resolve pinlog-sentinel-receiver.monitoring.svc.cluster.local:9765:127.0.0.1 \
https://pinlog-sentinel-receiver.monitoring.svc.cluster.local:9765/healthz노드 전체 장애는 내부 Alertmanager/Sentinel도 함께 멈춘다. 그 경우 GitHub
external-https-monitor 실행과 Mattermost external-monitor 메시지를 확인한다.
상세 route·mention·재시도 절차는 운영 알림을 따른다.
클러스터 안 백업만으로는 부족하다. 단일 노드·단일 디스크라 인스턴스 장애나 외부 관리 영역의 재이미징 한 번으로 전부 사라진다.
# 서버에서 최신 덤프 위치 확인
sudo k3s kubectl -n pinlog-prod exec postgres-0 -- ls -lh /backup/ 2>/dev/null || \
sudo find /var/lib/rancher/k3s/storage -name 'pinlog-*.dump' -newer /tmp -ls 2>/dev/null | tail -3
# 로컬로 복사
scp ubuntu@"${SERVER_HOST}":<덤프경로> ./backups/테스트하지 않은 백업은 백업이 아니다.
kubectl -n pinlog-prod create job --from=cronjob/postgres-backup backup-test
kubectl -n pinlog-prod wait --for=condition=complete job/backup-test --timeout=180s
kubectl -n pinlog-prod logs job/backup-test
# 스크래치 DB에 실제 복원해볼 것 (덤프가 열리는지까지 확인)
kubectl -n pinlog-prod exec postgres-0 -- psql -U pinlog -d postgres -c "CREATE DATABASE restore_check;"
# ... pg_restore 후 행 수 확인 ...
kubectl -n pinlog-prod exec postgres-0 -- psql -U pinlog -d postgres -c "DROP DATABASE restore_check;"
kubectl -n pinlog-prod delete job backup-testdev는 기본 replicaCount: 0이다 (자원 부족으로 전체 미러링 불가).
kubectl -n pinlog-dev scale deploy/<서비스> --replicas=1
kubectl -n pinlog-dev port-forward svc/<서비스> 8080:80services-dev ApplicationSet은 selfHeal: false라 ArgoCD가 되돌리지 않는다.
# 현재 quota 사용량과 LimitRange 기본값
kubectl -n pinlog-prod describe resourcequota prod-quota
kubectl -n pinlog-prod describe limitrange prod-container-limits
# Pending 원인과 최근 scheduling event
kubectl -n pinlog-prod get pods --field-selector=status.phase=Pending
kubectl -n pinlog-prod describe pod <파드명>
kubectl -n pinlog-prod get events --sort-by='.lastTimestamp'
# workload별 선언값 확인
kubectl -n pinlog-prod get deploy,statefulset,cronjob -o yaml
# metrics-server가 Ready일 때만 실사용량 비교
kubectl -n kube-system get deployment metrics-server
kubectl -n pinlog-prod top pods --containers
# 저용량 profile에서는 host pressure로 대체
vmstat 1 6
cat /proc/pressure/cpu
cat /proc/pressure/memoryexceeded quota면 무조건 quota부터 올리지 않는다. 비정상 replica·과도한 request,
종료되지 않은 Job, 불필요한 PVC를 먼저 확인한다. 정상 증설이 필요하면
platform/namespaces/namespaces.yaml을 기능 브랜치에서 변경하고 PR 검증 후
Argo CD로 반영한다. 긴급 kubectl edit은 GitOps가 되돌리거나 drift를 만들므로
사용하지 않는다.
kubectl -n pinlog-prod exec -it postgres-0 -- psql -U pinlog -d pinlog
# 비밀번호가 필요하면 (SealedSecret이 복호화한 값)
kubectl -n pinlog-prod get secret postgres-credentials \
-o jsonpath='{.data.password}' | base64 -d; echokubectl -n pinlog-prod logs -f deploy/<서비스>
kubectl -n pinlog-prod logs deploy/<서비스> --previous # 재시작 전 로그
kubectl -n kube-system logs -f deploy/traefik # 라우팅 문제
kubectl -n argocd logs -f deploy/argocd-repo-server # 동기화 문제git이 진실의 원천이므로 git revert가 곧 롤백이다.
cd infra
git fetch origin main
git switch main
git merge --ff-only origin/main
git switch -c revert/<jira-key>-image
git revert <이미지_태그_올린_커밋>
git push -u origin HEAD
gh pr create --base mainPR의 필수 checks가 성공하고 merge되면 ArgoCD가 이전 이미지로 되돌린다.
kubectl rollout undo는 쓰지 말 것 — ArgoCD가 다시 되돌려 놓는다.
git switch -c feat/<jira-key>-add-secret
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl create secret generic <이름> \
--namespace pinlog-prod \
--from-literal=<키>='<값>' \
--dry-run=client -o yaml \
| kubeseal --format yaml \
--controller-name sealed-secrets-controller \
--controller-namespace kube-system \
> secrets/prod/<이름>.sealedsecret.yaml
git add secrets/prod/<이름>.sealedsecret.yaml
git commit -m "feat: <이름> 시크릿 추가"
git push -u origin HEAD
gh pr create --base main# 마스터에서 토큰 확인
sudo cat /var/lib/rancher/k3s/server/node-token
# 새 서버에서
curl -sfL https://get.k3s.io | K3S_URL=https://172.26.14.189:6443 \
K3S_TOKEN=<토큰> sh -PostgreSQL에는 이미
nodeSelector가 걸려 있어 마스터에 고정된다.local-pathPV가 노드에 묶이기 때문이며, 이 설정이 없으면 파드가 다른 노드로 스케줄되어 데이터를 찾지 못한다.
sudo systemctl restart k3s
kubectl get pods -A -wsudo /usr/local/bin/k3s-uninstall.sh
⚠️ PVC 데이터가 사라진다.local-path-retainStorageClass가reclaimPolicy: Retain이라/var/lib/rancher/k3s/storage/아래 디렉터리는 남지만, 재구축 후 수동으로 연결해야 한다. 반드시 백업을 먼저 확보할 것.
재구축은 bootstrap/ 스크립트를 순서대로 실행하면 된다
(README.md 참고). 단, Sealed Secrets 개인키를 복원하지 않으면
저장소의 모든 SealedSecret이 복호화 불가가 되므로 백업 키가 필수다.
curl -sk -o /dev/null -w "Gerrit: HTTP %{http_code}\n" "https://${SERVER_HOST}:8989/"
systemctl is-active gerrit httpd
sudo ufw status | grep -E "^(22|8989|443)"architecture.md— 구조와 설계 결정 근거git-governance.md— protected main, PR·CI, rollback 규칙alerting.md— Alertmanager, Sentinel, 외부 모니터 장애 대응../platform/pinlog-web-domain/README.md—pin-log.comCloudflare Tunnel activation·smoke·2단계 rollback../README.md— 부트스트랩 절차../examples/README.md— 새 서비스 추가../secrets/README.md— 시크릿 관리