-
Notifications
You must be signed in to change notification settings - Fork 0
component image snapshot pipeline
Factory A의 AI event snapshot 원본은 presigned URL로 S3에 직접 업로드하고, IoT Core에는 이미지 binary가 아니라 S3 참조 metadata만 전달한다.
Safe-Edge AI
-> worker2 hostPath snapshot 생성
-> snapshot-uploader polling
-> SnapshotPresigner API
-> S3 presigned PUT (image binary)
-> outbox image_snapshot metadata
-> edge-iot-publisher
-> AWS IoT Core MQTT (metadata only)
-> IoT Rule
-> S3 raw metadata
-> Lambda Data Processor
-> S3 processed metadata
-> DynamoDB latest image reference
이미지 binary는 AWS IoT Core, MQTT message, raw metadata JSON, processed metadata JSON, DynamoDB를 통과하지 않는다. Binary 전송 경로는 snapshot-uploader -> presigned S3 PUT뿐이다.
image_snapshot은 factory_state payload의 선택 필드가 아니라 별도 canonical source type이다. 현재 publisher와 Data Processor가 허용하는 source type은 다음과 같다.
factory_state
infra_state
image_snapshot
safe-edge-integrated-ai가 AI event 이미지를 worker2의 node-local hostPath에 만든다.
Safe-Edge Pod path: /app/snapshots
Host path: /var/lib/safe-edge/snapshots
파일 예: 260608114226_event_FIRE.jpg
파일명 앞의 YYMMDDhhmmss를 UTC source_timestamp로 해석한다. 형식이 맞지 않으면 파일 mtime을 사용한다. _event_ 뒤 문자열은 대문자 event_type이 되며, 파싱할 수 없으면 UNKNOWN이다.
지원 확장자와 content type:
| 확장자 | content type |
|---|---|
.jpg, .jpeg
|
image/jpeg |
.png |
image/png |
snapshot-uploader는 filesystem event가 아니라 polling으로 hostPath root의 파일을 확인한다.
scan dir: /var/lib/safe-edge/snapshots
scan interval: 10초
state file: /var/lib/aegis/outbox/.snapshot-uploader-state.json
Scan 결과는 mtime과 파일명으로 정렬한다. 후보마다 크기, mtime, SHA-256, content type, event type, source timestamp를 계산한다. 기본 최대 크기는 5 MiB이며 초과 파일은 건너뛴다.
State entry에는 로컬 절대 경로를 key로 두고 다음 값을 기록한다.
{
"/var/lib/safe-edge/snapshots/example.jpg": {
"size_bytes": 60345,
"mtime_ns": 1780882955270612976,
"sha256": "<sha256>",
"s3_key": "image_snapshot/factory_id=factory-a/...",
"uploaded_at": "2026-06-08T00:45:00Z"
}
}같은 path의 size_bytes와 mtime_ns가 state와 같으면 재업로드하지 않는다. 둘 중 하나가 달라지면 새 후보로 처리한다.
State가 없으면 기존 hostPath 파일도 최초 scan에서 backlog로 업로드한다. State JSON이 손상되면 timestamp가 붙은 .bad 파일로 격리하고 빈 state로 계속한다. S3 PUT과 outbox 기록은 끝났지만 state 기록에 실패한 경우, 같은 message ID의 outbox metadata에서 size, SHA-256, S3 key가 일치하면 재업로드 없이 state를 복구한다.
Uploader는 snapshot hostPath를 read-only로 mount하며 원본 파일을 삭제하지 않는다. 로컬 보존과 삭제는 Safe-Edge cleanup/purge 정책의 책임이다.
Uploader는 이미지 bytes가 아닌 upload 요청 metadata를 API Gateway의 POST /image-snapshot/presign으로 보낸다.
{
"factory_id": "factory-a",
"node_id": "worker2",
"filename": "260608114226_event_FIRE.jpg",
"content_type": "image/jpeg",
"size_bytes": 60345,
"sha256": "<sha256>",
"source_timestamp": "2026-06-08T11:42:26Z",
"event_type": "FIRE"
}SnapshotPresigner Lambda의 검증 기준:
-
factory_id가ALLOWED_FACTORY_IDS에 포함되어야 한다. 현재 Factory A MVP 허용 값은factory-a다. - filename은 path separator와
..가 없는 basename이어야 한다. - content type은
image/jpeg,image/png만 허용한다. - size는 1 byte 이상, 기본 5 MiB 이하여야 한다.
- SHA-256은 64자리 hex 문자열이어야 한다.
- source timestamp는 timezone이 있는 ISO 8601이어야 한다.
Lambda가 S3 key를 생성하고 기본 300초 유효한 presigned PUT URL과 필수 Content-Type header를 반환한다. Edge가 임의 S3 key를 지정하지 않는다. Lambda IAM 권한도 data bucket의 image_snapshot/*에 대한 s3:PutObject로 제한한다.
Uploader는 반환 URL에 image bytes를 HTTP PUT한다. Pi에는 이 작업을 위한 장기 AWS access key를 저장하지 않는다.
S3 PUT 성공 후 uploader가 canonical envelope를 outbox에 atomic write한다.
/var/lib/aegis/outbox/tmp/<temporary>.tmp
-> fsync
-> /var/lib/aegis/outbox/{message_id}.json
핵심 필드:
{
"source_type": "image_snapshot",
"source_timestamp": "2026-06-08T11:42:26Z",
"payload": {
"event_type": "FIRE",
"content_type": "image/jpeg",
"size_bytes": 60345,
"sha256": "<sha256>",
"s3_bucket": "aegis-bucket-data",
"s3_key": "image_snapshot/factory_id=factory-a/yyyy=2026/mm=06/dd=08/hh=11/260608114226_event_FIRE.jpg",
"local_path": "/var/lib/safe-edge/snapshots/260608114226_event_FIRE.jpg",
"upload_status": "uploaded"
}
}edge-iot-publisher가 publish 직전에 published_at과 data_plane_instance_id를 갱신하고 다음 topic으로 QoS 1 publish ack 완료 후 outbox metadata를 삭제한다.
aegis/factory-a/image_snapshot
성공하면 outbox JSON을 삭제하고, MQTT/TLS 실패 시 그대로 두어 재시도한다. Invalid JSON이나 schema 오류는 outbox/quarantine/으로 이동한다.
세 경로는 데이터 성격과 생성 주체가 다르다.
| 구분 | S3 경로 | 내용 | 생성 주체 |
|---|---|---|---|
| Snapshot original | image_snapshot/factory_id={factory_id}/yyyy={YYYY}/mm={MM}/dd={DD}/hh={HH}/{filename} |
JPEG/PNG binary | presigned S3 PUT |
| Raw metadata | raw/{factory_id}/image_snapshot/yyyy={YYYY}/mm={MM}/dd={DD}/{message_id}.json |
IoT Core가 받은 canonical envelope | IoT Rule |
| Processed metadata | processed/{factory_id}/image_snapshot/yyyy={YYYY}/mm={MM}/dd={DD}/hh={HH}/{message_id}.json |
정규화된 S3 참조 metadata | Data Processor |
Raw metadata 경로에는 현재 hh partition이 없고, processed metadata에는 있다.
Data Processor는 payload를 정규화한 뒤 공장별 LATEST item에 최신 이미지 참조를 갱신한다.
pk = FACTORY#factory-a
sk = LATEST
latest_image_snapshot:
event_type
source_timestamp
processed_at
s3_bucket
s3_key
content_type
size_bytes
sha256
message_id
last_image_snapshot_at
DynamoDB에는 binary나 presigned URL을 저장하지 않는다.
위 단계까지는 원본 binary가 S3 image_snapshot/ prefix에, 참조 metadata가 processed/와 DynamoDB LATEST에 남는다. 본사 관제자는 이 객체를 직접 보지 않고 Dashboard를 통해 조회한다. 표시 경로는 다음과 같다.
Dashboard Web (/image-snapshots, /chat)
-> Dashboard Backend (FastAPI)
-> RBAC: System 권한 확인
-> S3 list_objects_v2 (image_snapshot/ prefix)
-> S3 presigned GET URL 생성 (object별)
-> 브라우저 <img>가 presigned URL로 원본을 직접 GET
이미지 binary는 Backend를 통과하지 않는다. Backend는 S3 객체 목록과 짧은 만료의 presigned GET URL만 생성하고, 실제 download는 브라우저가 presigned URL로 S3에서 직접 받는다. S3 객체는 여전히 non-public이며, public-read ACL을 쓰지 않는다.
routers/image_snapshots.py의 두 endpoint가 services/s3.py를 호출한다.
| Endpoint | 동작 | S3 호출 |
|---|---|---|
GET /image-snapshots/range?factory_id= |
picker용 가용 범위(가장 이른/늦은 partition, 객체 수) |
image_snapshot/factory_id=.../ prefix 전체 list |
GET /image-snapshots?factory_id=&start=&end=&limit= |
시간 범위 내 스냅샷 목록 + presigned GET URL | 시간별 hh= prefix list |
- 두 endpoint 모두
require_system_access(principal)로 System 권한을 강제한다. 공장 단위 권한이 아니라 시스템 조회 권한이 경계다. -
start/end는 ISO 8601 local time이며start < end, 미래 종료 보정 등은 Frontend가 선검증하고 Backend가 재검증(400 Invalid time range)한다. - 조회는 시간 범위가 겹치는
yyyy=/mm=/dd=/hh=partition prefix만 list하여 비용을 제한한다. 객체가limit(기본 120, 최대 300)에 도달하면 조기 종료한다. - S3 지연/실패는
504로 변환한다(S3UnavailableError).
각 객체에 대해 Backend가 만드는 응답 항목:
{
"factory_id": "factory-a",
"s3_key": "image_snapshot/factory_id=factory-a/yyyy=2026/mm=06/dd=08/hh=11/260608114226_event_FIRE.jpg",
"filename": "260608114226_event_FIRE.jpg",
"url": "<presigned GET URL, 기본 900초 만료>",
"last_modified": "2026-06-08T11:42:30+00:00",
"size_bytes": 60345,
"detection_type": "FIRE"
}detection_type은 파일명 _event_<TYPE> 패턴에서 파생한다(예: FIRE). DynamoDB나 metadata를 다시 읽지 않고 S3 key만으로 분류 라벨을 만든다.
apps/dashboard-web의 ImageSnapshotsPage가 갤러리를 그린다.
-
System 권한 전용:
/auth/me.can_view_system이 false이면 화면 진입 시 "시스템 조회 권한이 필요합니다" empty-state를 표시하고 조회를 시도하지 않는다(Backend403이 최종 경계). - 공장 selector + 시작/종료 시각 picker로 범위를 고른다. 진입 시
GET /image-snapshots/range로 S3 가용 시작 시각을 받아 picker 하한을 맞추고, 미래·역전 범위를 막는다. - 범위가 바뀌면
GET /image-snapshots를 다시 호출해 카드 그리드를 갱신한다. 각 카드는 presigned URL을<img loading="lazy">로 표시하고, 클릭 시 새 탭에서 원본을 연다. 카드 메타에는 탐지 종류(detection_type), 수정 시각, 크기를 보여준다. - 상태 처리: 로딩 spinner, S3 지연/오류 재시도,
403권한 거부, 빈 범위(해당 시간대 스냅샷 없음) empty-state를 구분한다.
AI 채팅에서 "사진/이미지/스냅샷/증빙" 류 키워드가 있으면, Backend가 같은 s3.list_image_snapshots를 질문 시각 범위(또는 탐지 spike 시각 ±10분)에 대해 호출해 답변에 **증빙 이미지(image_ref)**를 함께 싣는다. 이 경로도 require_system_access를 거치므로, 채팅이 RBAC 우회 통로가 되지 않는다. 자세한 동작은 AI 채팅 어시스턴트를 따른다.
Factory A 현재 배치는 다음과 같다.
safe-edge-integrated-ai: worker2 snapshot hostPath 사용
snapshot-uploader: worker2 단일 Deployment
edge-iot-publisher: worker2 배치
outbox: Longhorn RWO PVC
Snapshot hostPath는 node-local이므로 worker1에서 worker2의 기존 파일을 볼 수 없다. 또한 uploader와 publisher가 공유하는 outbox가 Longhorn RWO이므로 현재 MVP는 worker2의 단일 writer/publisher 배치를 전제로 한다.
Worker2 장애 시 AI workload 자체가 worker1에서 재기동될 수 있어도, worker1 snapshot upload failover와 worker2 backlog 인계는 구현 범위가 아니다. Worker별 uploader, state 분리, outbox 다중 writer, 중복 방지까지 포함한 failover 설계가 추가로 필요하다.
- S3 original object를
public-read로 만들지 않는다. Presigner와 uploader 어느 쪽도 ACL을 지정하지 않는다. - Raspberry Pi에 장기 AWS access key를 저장하지 않는다.
- Presigned URL은 짧은 만료 시간과 제한된 object key에만 사용한다.
- 실제 token, certificate, IoT endpoint secret은 repo와 위키에 기록하지 않는다.
- 현재
PRESIGN_SHARED_TOKEN기본값과 운영 적용값은 빈 문자열이라 bearer token 인증이 비활성화된 상태다. 운영 전 token 설정 또는 더 강한 API 인증으로 보강해야 한다.
관련 문서
- 시스템 아키텍처
- 제어 & 데이터 플레인
- 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 채팅 어시스턴트