-
Notifications
You must be signed in to change notification settings - Fork 0
lessons learned
Aegis-Pi를 만들고 운영하면서 실제로 검증한 **모범사례(Best Practices)**와, 직접 부딪혀 해결한 **실패사례(Failure Cases)**를 한곳에 모았다. 새 노드·새 공장·새 운영자가 같은 문제를 다시 겪지 않도록 하는 것이 목적이다.
한쪽 평면에 치우치지 않도록 **엣지/제어 평면(factory-a Safe-Edge·Hub)**과 데이터/대시보드 평면(Data Pipeline·Dashboard VPC) 양쪽 사례를 함께 담는다.
상태: 운영 검증 기준 기준일: 2026-06-14
- 이 페이지는 요약·원칙·재발 방지를 담는다. 항목별 명령어 수준의 원본 기록은 아래 운영
문서에 있다.
- 엣지/제어 평면:
docs/ops/04_troubleshooting.md(factory-a·Safe-Edge 40건),docs/ops/09_failover_failback_test_results.md,docs/ops/10_edge_workload_placement.md - 데이터/대시보드 평면:
docs/ops/22_data_dashboard_vpc_runbook.md,docs/ops/23_data_pipeline.md,docs/ops/31_risk_alert_dispatcher.md
- 엣지/제어 평면:
- 결정을 바꾼 배경은 ADR 인덱스의 개별 ADR을 따른다.
- AWS Hub build/destroy 과정의 장애는 AWS Hub 트러블슈팅에 따로 있다.
safe-edge-integrated-ai, safe-edge-audio, bme280-sensor는 노드의 실제 장치
(/dev, /dev/snd, /dev/i2c-1, camera)를 직접 잡는다. Deployment 기본 전략인
RollingUpdate는 새 Pod를 먼저 띄우고 기존 Pod를 나중에 종료하므로, 짧은 시간이라도 두
Pod가 같은 장치를 동시에 열어 초기화 실패와 재시작 루프가 생긴다.
- 장치를 잡는 Pod는 모두
strategy: Recreate로 둔다. - Longhorn RWO PVC를 쓰는 Pod(InfluxDB, Grafana)도
Recreate로 둔다. RollingUpdate는 새 Pod가 다른 노드에서 먼저 뜨면서 RWO volume Multi-Attach 대기를 만든다.
재부팅 직후 노드의 외부 DNS/registry 경로가 늦게 올라오면, 이미지가 이미 있어도 Always
pull 경로를 타다가 ErrImagePull로 복구가 막힌다.
- 운영 Pod에는
imagePullPolicy: IfNotPresent를 명시한다. - worker1/worker2 양쪽에 AI/Audio 이미지를 미리 받아두는
image-prepullDaemonSet을 둔다. prepull 적용 후 LAN 제거 테스트에서 이미지 Pulling으로 인한 장시간 지연이 사라졌다.
CronJob 기반 자동 failback은 worker2가 Ready로 돌아온 직후, 워크로드가 아직 안정화되기
전에 Pod를 옮기거나 이미 정상인 worker2 Pod를 중복으로 죽일 위험이 있다.
- failback은 master OS cron이 실행하는 외부 스크립트로 처리한다(SSH·비밀번호 미사용).
- 스크립트는 worker2에 대상 Pod가 이미 있으면 아무 것도 하지 않는다를 최우선 가드로 둔다.
- worker1에 남은 대상 Pod만 삭제 후보로 본다.
- 결정 배경은 Failback OS cron ADR 참조.
추론 결과(위험도·탐지값)는 InfluxDB(Longhorn PVC)에 남기되, snapshot 이미지는 AI Pod의 failover를 막지 않도록 RWO PVC를 쓰지 않는다.
/app/snapshots → hostPath /var/lib/safe-edge/snapshots- 24시간 초과 파일은 cleanup sidecar/CronJob이 정리하고, 장기 보관은 후속 비동기 전송 계층(snapshot-uploader presigned PUT → S3)이 담당한다.
- 배경: AI Snapshot hostPath ADR.
Longhorn은 RWO 볼륨에 두 writer가 붙는 것을 막기 위해 worker1 attach를 거부한다. 이는 데이터 손상을 막는 정상 동작이다.
-
worker2가NotReady여도 실제 OS·containerd·기존 컨테이너·VolumeAttachment가 즉시 사라진다고 가정하지 않는다. - self-healing fencing(worker2 재부팅으로 빠른 자기 복구)과 long failover fencing (전원 차단/외부 power fencing으로 확실히 격리 후 worker1 장기 운영)을 구분한다.
- 기존 writer가 죽었음을 fencing으로 보장한 뒤에만 stale Pod/VolumeAttachment를 정리한다.
장애 직후에는 kubectl의 마지막 관측값이 남아 오판하기 쉽다.
- 장애 시작 후 첫 5분은 성공/실패를 판정하지 않는다.
-
worker2 Ready만으로 failback 완료로 보지 않는다. 대상 Pod 3개가 worker2 Running이 되어야 failback 완료다. - 랜선 제거 시각은 테스트 시작 시각으로만 쓰고,
worker2 NotReady/Unreachable전환 시각을 따로 기록한다. -
k3s-agent stop과 실제 랜선 제거는 다르다. agent stop은 기존 컨테이너가 잠시 계속 write해 중복이 보일 수 있고, 랜선 제거는 Pod network·node-exporter·Longhorn/CSI까지 끊긴다. - 검증 결과: 전원/LAN 제거 모두 RTO 약 1
2분, hostPath 구성에서 Multi-Attach 재발 없음 (test_07test_09). 상세는 Failover & 복구 시나리오.
factory-a 3노드는 두 인터페이스를 동시에 쓴다. 역할이 섞이면 flannel·Tailscale·패키지 설치가 한꺼번에 깨진다.
-
eth0: K3s 내부망(10.10.10.0/24) 전용. default gateway/DNS를 두지 않는다 (ipv4.never-default yes,ipv4.ignore-auto-routes/dns yes, route-metric은 높게). -
wlan0: 인터넷 default route·DNS·Tailscale control plane·image pull 담당(route-metric은 낮게). - NetworkManager profile 이름과 device 이름을
eth0로 통일한다.nmcli con up eth0의eth0는 device가 아니라 connection profile 이름이다.
자동 선택에 맡기면 Wi-Fi 대역 IP나 잘못된 overlay 경로가 잡힌다.
- server/agent 실행 인자에
--node-ip/--advertise-address로 독립망 IP를 명시한다. - 세 노드 모두
--flannel-iface eth0를 명시해 overlay 경로 불일치를 막는다. - 노드 시간 동기화(
systemd-timesyncd, NTP)를 켜둔다. 시간이 틀어지면 인증서certificate not valid오류로 agent가 못 올라온다.
10.10.10.10/8처럼 넓은 마스크는 10.0.0.0/8 전체를 eth0로 보내는 위험한 route를 만든다.
/24로 정상화하되, 로컬 작업 머신이 같은 대역에 없으면 직접 SSH가 끊길 수 있으니 운영
접근은 master Tailscale IP / worker wlan0 IP로 한다.
- control-plane과 edge 처리 Pod를 분리한다. master에는 Grafana 같은 가벼운 UI만 두고 반드시 request/limit을 건다(master는 4GiB 노드, k3s-server가 1GiB+ 사용).
- 장애 도메인을 분리한다. InfluxDB는 데이터 생산처(worker2의 AI/audio/sensor)와 다른 노드(worker1)에 둬서 생산과 저장이 동시에 죽지 않게 한다.
- AI/audio/BME에는 requests/limits를 둬서 scheduler가 worker1 failover 수용 가능성을 계산할 수 있게 한다(failover 추가 부하 관측치 약 420m CPU / 2.6GiB).
- Downward API로
NODE_NAME/POD_NAME/POD_UID를 주입해 후속event_id·idempotency 기반을 마련한다.
- 비밀번호·Wi-Fi 키·Tailscale auth key·OAuth secret·certificate/private key는 문서·shell history에 남기지 않는다. AWS IoT 인증서/키는 Kubernetes Secret으로만 주입한다.
-
같은 이미지 태그를 덮어쓰지 않는다.
IfNotPresent에서는 노드 캐시가 우선돼 실제 실행 버전이 불명확해진다. 운영 이미지는 새 태그로 배포하고 values.yaml 갱신 → push → ArgoCD sync 순서를 지킨다. - AWS 리소스·상시 컴포넌트·저장소·네트워크 경로를 추가하면 비용 기준서를 함께 갱신한다 (비용 기준서). retired된 AMP/Prometheus Agent는 되살리지 않는다.
- 송신 컴포넌트(edge-agent)는 message_id idempotency key, last-sent checkpoint, MQTT QoS 1, reconnect/backoff, 최소 RBAC(cluster-admin 금지)를 기본으로 한다.
대시보드 history 조회는 구간 길이에 따라 다른 read model을 본다. 한 경로로 24시간을 모두 훑으면 DynamoDB 비용과 응답 시간이 폭증한다.
-
1h이하:HISTORY#STATE원시 스냅샷 조회 +max_items=500cap(ScanIndexForward=False). -
6h/12h/24h:GRAPH#5M5분 집계 item 조회(각각 최대 72/144/288개). - 원시(
HISTORY#STATE)에는risk.top_causes원인 필드가 있지만 집계(GRAPH#5M)에는 없으므로, 긴 구간 Timeline은top_causes 없음으로 표시한다. 배경: Multi-resolution History ADR.
수신 시점에만 pipeline_status/risk를 갱신하면, 메시지가 완전히 끊겼을 때 직전 값
(risk.score=100 등)이 stale로 굳는다.
-
DataProcessorRefresh1m스케줄러가 1분마다 DataProcessor를action=refresh_pipeline_status로 호출해, 신선도가 떨어진 공장을pipeline_status=critical로 내린다. - 대시보드 staleness 임계값은 파이프라인 refresh 주기와 맞춘다(staleness threshold ADR 참고).
데모 destroy에도 살아남아야 하는 리소스(Cognito·Route53·ECR·S3 web·CloudFront 등)는 별도 permanent root로 옮긴다.
- 신규 root에서
import로 기존 리소스를 인수하고, 기존 root에서는state rm으로 관리에서만 빼낸다.state rm은 AWS 리소스를 삭제하지 않는다. - 양쪽 root 모두
plan결과가No changes가 되는지 확인한 뒤 마무리한다. - 배경: DynamoDB foundation 이관 ADR, Dashboard VPC 설계.
- Slack webhook URL 같은 값은 Terraform이 metadata만 관리하고, 실제 값은 빌드 스크립트가 로컬
.secrets/파일에서 Secrets Manager로 주입한다. URL 값은 repo와 Terraform state에 저장하지 않는다.
- 같은 section에 구체(specific) 알림이 있으면 generic fallback 알림을 억제한다(예: EKS specific이
있으면
eks_management_*generic 억제). - 일시적 warning은 일정 시간 안에 서로 다른 snapshot을 2회 관측한 뒤에만 보낸다(예:
backend_runtime_warning은 90초 내 2회,pods_warning은 450초 내 2회). - ALB unhealthy host, Lambda/DynamoDB throttle, collector error, danger/critical은 즉시 알림이다.
- DynamoDB dedupe state로 같은 알림의 반복 전송을 막는다. 운영 기준은 Alerting 운영.
- Lambda zip에
__pycache__/*.pyc를 넣지 않는다. stale bytecode가 들어가면 소스 변경과 실제 런타임 동작이 어긋난다. - 같은 결과물(
processed/)을 덮어쓰는 중복 IoT Rule/Lambda를 만들지 않는다. 구형 경로는 비활성화해 한 경로만 read model을 갱신하게 한다. - 주기적으로 실제 AWS 배포본(
get-function)과 repo 코드를 대조해 drift를 확인한다.
각 항목은 증상 → 원인 → 해결 → 재발 방지 순서로 정리했다. 전체 명령어는 위의 원본 운영
기록을 참조한다.
-
증상. worker2가 죽으면 BME·audio는 worker1로 잘 넘어가는데, AI만
ContainerCreating에 계속 머물렀다. 이벤트에는Multi-Attach error for volume ... Volume is already used by pod(s)가 찍혔다. -
원인. AI가 Longhorn RWO PVC(
safe-edge-ai-snapshots)를 마운트하고 있었다. worker2가NotReady로 바뀌어도 기존 Pod와 VolumeAttachment가 stale 상태로 남아, Longhorn이 worker1 attach를 거부했다. 이건 두 writer가 붙는 것을 막는 정상적인 방어 동작이다. -
해결. snapshot 저장을 RWO PVC에서 node-local hostPath로 바꾸고 worker2 watchdog fencing을
걷어냈다. 이후
test_07~09에서 Multi-Attach는 재발하지 않았다. - 재발 방지. 복구 우선순위가 낮은 데이터에는 RWO PVC를 쓰지 않는다. fencing이 보장되기 전에는 force-detach를 자동화하지 않는다(§1.5).
-
manager가 master에 안 뜸. master의
NoScheduletaint에 toleration이 없어 DaemonSet이 worker 2개에만 배치됐다 → Longhorntaint-toleration설정과 DaemonSet toleration을 추가한다. -
KernelModulesLoaded=False.dm_crypt모듈이 안 올라와 있었다 →modprobe dm_crypt와/etc/modules-load.d/longhorn.conf등록 후 manager를 재시작한다. -
데이터 경로 없음.
/var/lib/longhorn이 없었다 → 세 노드에 미리 만들어 둔다. -
Grafana PVC 권한 오류. 컨테이너 UID/GID가 맞지 않아
/var/lib/grafana is not writable가 났다 →securityContext에fsGroup/runAsUser/runAsGroup: 472를 지정한다.
- 증상. worker2가 복구되자 하드웨어 의존 Pod가 worker2로 올라갔다가 계속 다시 생성됐다.
- 원인. Kubernetes 관점에서 Ready여도 하드웨어가 아직 불안정했다. cooldown이나 중복 검사 없이 Pod를 삭제하다 보니 정상 동작 중인 Pod까지 죽었다.
- 해결·재발 방지. failback을 master OS cron + Kubernetes-only 스크립트로 바꾸고, "worker2에 이미 대상 Pod가 있으면 손대지 않는다"는 가드를 최우선에 뒀다(§1.3).
§1.1에서 정리한 그대로다. 같은 카메라·오디오·I2C 장치를 두 Pod가 잠깐이라도 동시에 열면서
초기화에 실패한다. Recreate 전략으로 해결한다.
-
증상. worker2를 재부팅하니 node-exporter가
ErrImagePull/ImagePullBackOff에 빠졌고, 로그에는 registry DNS 조회 실패가 찍혔다. -
원인. 태그가
latest이고 pull policy가 비어 있어 매번 외부 registry로 나가려 했는데, 내부망 노드는 재부팅 직후 외부 경로가 불안정했다. -
해결·재발 방지.
imagePullPolicy: IfNotPresent를 명시하고 이미지를 미리 받아 둔다(§1.2). 다음 단계로latest를 고정 버전 태그로 바꾼다.
-
증상. worker2가
NotReady에 머물고 k3s-agent가activating을 반복했다. 로그에는flannel ... unable to find default route, MetalLB·Longhorn CSI는 CrashLoopBackOff였다. - 원인. default route가 없으면 flannel이 기본 인터페이스를 못 고른다. 게다가 노드마다 flannel public-ip가 다른 대역(wlan0 vs eth0)으로 잡혀 overlay 경로가 어긋났다.
-
해결·재발 방지. 세 노드에
--flannel-iface eth0를 명시하고, 시간 동기화를 켜고,10.43.0.0/16 dev cni0service-cidr route를 복구하는 service를 둔다. 빠른 복구는 경량 스크립트(safe-edge-preflight-repair.sh)로 처리한다.
-
증상. Ansible
start_test.yml의 Tailscale 검증이 실패했다. master에서tailscale status는NoState/logged out,wlan0는 disconnected, default route도 없었다. - 원인. master의 인터넷 경로는 wlan0 default route에 의존한다. Wi-Fi 프로필이 꼬이면 control plane과 동기화가 끊긴다.
- 해결. SSH는 eth0/Tailscale 경로로 유지한 채, wlan0 Wi-Fi 프로필만 삭제하고 다시 만든 뒤 tailscaled를 재시작했다.
-
재발 방지. Tailscale 실패를 보면 hostname 불일치부터 의심하지 말고, wlan0 상태·default
route·DNS·
controlplane.tailscale.com접근성을 먼저 확인한다. 인터페이스 역할을 고정한다(§2.1).
-
증상.
eth0에 IPv4가 안 붙고nmcli connection up eth0가unknown connection으로 실패했다./8을/24로 정상화한 뒤로는 로컬에서10.10.10.x로 직접 SSH가 안 됐다. -
원인. profile 이름(
Wired connection 1)과 device 이름(eth0)을 혼동했다. 그리고 직접 SSH가 막힌 건 설정 실패가 아니라, 그동안 지나치게 넓던/8route에 기대던 접근 방식이 사라졌기 때문이다. -
해결·재발 방지. profile=device=
eth0로 통일하고, eth0는 static + never-default로 두며, 운영 접근은 Tailscale/wlan0 경로를 쓴다(§2.1, §2.3). eth0 프로필을 재생성하기 전에 우회 접근 경로부터 확보한다.
-
증상. Pod는
2/2 Running이고 로그도 카메라 연결까지 정상인데, InfluxDB에ai_detection이 안 쌓였다. 로그 끝에는Camera frontend has timed out이 보였다. -
원인. 장치를 인식하는 것과 실제 프레임을 캡처하는 것은 다르다.
capture_array()가 막히면 그 뒤의 write가 실행되지 않는데, Pod는 Running이라 Kubernetes가 비정상으로 판단하지 못한다. -
해결. AI Deployment를
scale 0 → 1로 재기동한 뒤ai_detection최신값과[AI STATUS]로그를 확인했다. - 재발 방지. capture loop heartbeat, livenessProbe, 또는 일정 시간 write가 없으면 프로세스를 종료하는 watchdog을 추가한다(후보).
- I2C가 비활성(
#dtparam=i2c_arm=on)이라 장치가 안 보였다 → config.txt에서 활성화하고i2cdetect로 주소0x76을 확인한다. - 비대화형 SSH의 PATH에
/usr/sbin이 없어i2cdetect를 못 찾았다 → 절대경로/usr/sbin/i2cdetect로 실행한다.
-
증상.
/history?window=24h로 3개 공장을 동시에 조회하자 504 Gateway Timeout이 연쇄로 발생했다. -
원인. 24시간 구간을
HISTORY#STATE원시 스냅샷으로 모두 훑으면서 DynamoDB 조회량과 응답 시간이 한계를 넘었다. -
해결. 구간별로 read model을 나눴다.
1h이하는HISTORY#STATE에max_items=500cap을,6h/12h/24h는GRAPH#5M5분 집계를 조회하게 했다(Multi-resolution History ADR). -
재발 방지.
HISTORY#STATETTL을 줄여 테이블 아이템 수를 낮추고, 긴 구간은 항상 집계 경로로만 조회한다.
-
증상. factory-a가 메시지 송신을 멈춘 뒤에도 DynamoDB
LATEST의risk.score=100이 그대로 남아, 대시보드가 위험을 정상으로 보여줬다. -
원인. 메시지를 받을 때만
pipeline_status와risk를 갱신하는 구조라, 데이터가 완전히 끊기면 갱신을 일으킬 trigger 자체가 없었다. -
해결.
DataProcessorRefresh1m스케줄러가 1분마다 DataProcessor를action=refresh_pipeline_status로 호출하게 했다. 적용 후 factory-a는pipeline_status=critical,risk.score=0으로 정상 전환됐다(메시지가 계속 들어오는 factory-b/c는 영향 없음). - 재발 방지. "수신 기반 갱신"에는 항상 신선도 만료를 강제하는 주기 refresh를 함께 둔다(§5.2).
-
증상. Spoke K3s에서 adapter rollout 시
403 Forbidden과ErrImagePull이 났다. -
원인. Spoke K3s는 EKS node role을 상속받지 못하므로 ECR 토큰이 들어 있는
ai-apps/ecr-registryimagePullSecret이 만료되면 이미지를 못 받는다. - 해결·재발 방지. imagePullSecret을 주기적으로 갱신한다. rollout 전에 secret 신선도를 점검 항목에 넣는다.
-
증상. 같은
processed/결과를 여러 IoT Rule/Lambda가 만들면서 최신 결과가 구형 Lambda 출력으로 덮였다. 별개로, 소스를 고쳤는데 런타임 동작이 안 바뀌는 경우도 있었다. -
원인. 중복 처리 경로가 살아 있었고, Lambda zip에
__pycache__/*.pycstale bytecode가 섞여 들어갔다. -
해결·재발 방지. 중복 IoT Rule/Lambda를 비활성화해 read model 갱신 경로를 하나로 모으고,
zip 패키징에서
__pycache__/*.pyc를 제외한다. 실제 배포본과 repo 코드를 주기적으로 대조한다(§5.6).
- 증상. Cloud infra slow collector가 EKS Kubernetes API 호출에서 401을 받아 노드/파드/ArgoCD 상태를 못 모았다.
- 원인. collector IAM role에 대한 EKS access entry가 없었다.
-
해결·재발 방지.
AEGIS-IAMRole-Lambda-CloudInfraSlowCollector에AmazonEKSAdminViewPolicycluster scope read 권한을 부여하니errors=[]로 정상 수집됐다. 데이터 평면에서 제어 평면을 조회할 때는 읽기 전용 최소 권한만 부여한다.
Hub build/destroy 과정의 MFA/STS 접근 실패, EKS CloudWatch Log Group 중복, state tainted 등 AWS
측 장애와 untaint/import 해법은 AWS Hub 트러블슈팅에 정리돼 있다.
공통 원칙은 AWS 리소스는 정상인데 state만 어긋났으면 새로 만들지 말고 untaint/import로
맞춘다는 것이다.
관련 문서
- 시스템 아키텍처
- 제어 & 데이터 플레인
- Dashboard VPC 설계
- 하드웨어 배치
- Hub EKS 네임스페이스
- Tailscale Mesh VPN
- 데이터 생명주기
- 데이터 조회 모델
- 실시간 갱신 구조
- IoT 데이터 계약
- Reporting Pipeline
- 로컬 스토리지
- 클라우드 스토리지
- Edge Agent
- Edge AI 탐지
- Factory-A Log Adapter
- Dummy Sensor
- Edge IoT Publisher
- Lambda Data Processor
- Risk Normalizer
- Risk Score Engine
- Pipeline Status Aggregator
- Graph Aggregator 5m
- Cloud Infra Collector
- Daily Report Generator
- Risk Alert Dispatcher
- Image Snapshot Pipeline
- Dashboard Backend
- Dashboard Web
- AI 채팅 어시스턴트