Docker 컨테이너화, OTA 업데이트, 롤백 전략을 통한 안전하고 효율적인 엣지 AI 배포 시스템 구축
엣지 AI 배포 파이프라인은 모델 학습부터 엣지 디바이스 배포까지의 전체 과정을 자동화합니다. 특히 모델 최적화, 컨테이너 빌드, OTA 배포 단계가 핵심입니다.
클라우드에서 모델 학습 및 검증. 정확도 임계값 통과 시 다음 단계 진행
TensorRT/ONNX 변환, 양자화, 프루닝으로 엣지 최적화
멀티 아키텍처(ARM64/AMD64) Docker 이미지 빌드
단계적 롤아웃으로 엣지 디바이스에 안전하게 배포
엣지 AI 애플리케이션을 Docker 컨테이너로 패키징하면 의존성 관리, 버전 관리, 배포가 간소화됩니다. NVIDIA Container Toolkit을 통해 GPU 가속도 지원됩니다.
# Dockerfile for Edge AI Application (Jetson) # Base image: NVIDIA L4T with TensorRT FROM nvcr.io/nvidia/l4t-tensorrt:r8.5.2-runtime # Set environment variables ENV DEBIAN_FRONTEND=noninteractive ENV PYTHONUNBUFFERED=1 ENV MODEL_PATH=/app/models ENV CONFIG_PATH=/app/config # Install system dependencies RUN apt-get update && apt-get install -y --no-install-recommends \ python3-pip \ python3-dev \ libopencv-dev \ libgstreamer1.0-dev \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies COPY requirements.txt /tmp/ RUN pip3 install --no-cache-dir -r /tmp/requirements.txt # Copy application code WORKDIR /app COPY src/ ./src/ COPY models/ ./models/ COPY config/ ./config/ # Create non-root user for security RUN useradd -m -u 1000 edgeai && \ chown -R edgeai:edgeai /app USER edgeai # Health check HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD python3 -c "import requests; requests.get('http://localhost:8080/health')" # Entrypoint ENTRYPOINT ["python3", "src/main.py"] CMD ["--config", "/app/config/production.yaml"]
# docker-compose.yml for Edge AI Stack version: '3.8' services: edge-ai-inference: image: registry.example.com/edge-ai:${VERSION:-latest} runtime: nvidia environment: - NVIDIA_VISIBLE_DEVICES=all - MODEL_VERSION=${MODEL_VERSION} - LOG_LEVEL=INFO volumes: - ./models:/app/models:ro - ./config:/app/config:ro - ./logs:/app/logs - /dev/video0:/dev/video0 # Camera access devices: - /dev/video0 ports: - "8080:8080" restart: unless-stopped deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] edge-gateway: image: registry.example.com/edge-gateway:${VERSION:-latest} environment: - MQTT_BROKER=${MQTT_BROKER} - CLOUD_API_URL=${CLOUD_API_URL} volumes: - ./data:/app/data ports: - "1883:1883" depends_on: - edge-ai-inference prometheus: image: prom/prometheus:latest volumes: - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090"
OTA 업데이트를 통해 물리적 접근 없이 원격으로 엣지 디바이스의 소프트웨어를 업데이트할 수 있습니다. 안전한 OTA를 위해 원자적 업데이트, 롤백 지원, 서명 검증이 필수입니다.
import asyncio import hashlib import subprocess from dataclasses import dataclass from enum import Enum from typing import Optional, Callable import aiohttp import docker class UpdateState(Enum): IDLE = "idle" DOWNLOADING = "downloading" VERIFYING = "verifying" INSTALLING = "installing" VALIDATING = "validating" COMPLETED = "completed" FAILED = "failed" ROLLING_BACK = "rolling_back" @dataclass class OTAUpdate: version: str image_url: str checksum: str signature: str release_notes: str min_version: Optional[str] = None class OTAUpdateManager: """OTA 업데이트 관리자""" def __init__( self, device_id: str, current_version: str, update_server_url: str, validation_fn: Optional[Callable] = None ): self.device_id = device_id self.current_version = current_version self.server_url = update_server_url self.validation_fn = validation_fn self.state = UpdateState.IDLE self.docker_client = docker.from_env() self.previous_image: Optional[str] = None async def check_for_updates(self) -> Optional[OTAUpdate]: """서버에서 업데이트 확인""" async with aiohttp.ClientSession() as session: params = { "device_id": self.device_id, "current_version": self.current_version } async with session.get( f"{self.server_url}/updates/check", params=params ) as resp: if resp.status == 200: data = await resp.json() return OTAUpdate(**data) if data else None return None async def apply_update(self, update: OTAUpdate) -> bool: """업데이트 적용 (원자적)""" try: # 1. 이미지 다운로드 self.state = UpdateState.DOWNLOADING await self._pull_image(update.image_url) # 2. 체크섬 검증 self.state = UpdateState.VERIFYING if not self._verify_checksum(update.image_url, update.checksum): raise ValueError("Checksum mismatch") # 3. 이전 버전 백업 self.previous_image = self._get_current_image() # 4. 새 버전 설치 self.state = UpdateState.INSTALLING await self._deploy_container(update.image_url) # 5. 헬스 체크 self.state = UpdateState.VALIDATING if not await self._validate_deployment(): raise RuntimeError("Validation failed") # 6. 완료 self.state = UpdateState.COMPLETED self.current_version = update.version return True except Exception as e: self.state = UpdateState.FAILED await self.rollback() return False async def rollback(self) -> bool: """이전 버전으로 롤백""" if not self.previous_image: return False self.state = UpdateState.ROLLING_BACK try: await self._deploy_container(self.previous_image) self.state = UpdateState.IDLE return True except: self.state = UpdateState.FAILED return False async def _pull_image(self, image_url: str): """Docker 이미지 풀""" self.docker_client.images.pull(image_url) async def _deploy_container(self, image: str): """컨테이너 배포 (무중단)""" # 기존 컨테이너 중지 try: old = self.docker_client.containers.get("edge-ai") old.stop(timeout=30) old.remove() except docker.errors.NotFound: pass # 새 컨테이너 시작 self.docker_client.containers.run( image, name="edge-ai", runtime="nvidia", detach=True, restart_policy={"Name": "unless-stopped"}, environment={"NVIDIA_VISIBLE_DEVICES": "all"}, ports={"8080/tcp": 8080} ) async def _validate_deployment(self) -> bool: """배포 검증 (헬스 체크 + 커스텀 검증)""" # 헬스 체크 대기 for _ in range(30): try: async with aiohttp.ClientSession() as session: async with session.get( "http://localhost:8080/health" ) as resp: if resp.status == 200: break except: pass await asyncio.sleep(1) else: return False # 커스텀 검증 (정확도 체크 등) if self.validation_fn: return self.validation_fn() return True
업데이트 실패 시 빠르고 안전하게 이전 버전으로 복원할 수 있어야 합니다. 자동 롤백 조건과 수동 롤백 절차를 모두 준비해야 합니다.
| 트리거 조건 | 롤백 유형 | 액션 |
|---|---|---|
| 헬스 체크 실패 (30초 내) | 자동 | 즉시 이전 이미지로 전환 |
| 추론 정확도 임계값 미달 | 자동 | Shadow 모드 중 이전 버전 유지 |
| 메모리/CPU 과다 사용 | 자동 | 리소스 제한 초과 시 롤백 |
| 운영자 수동 요청 | 수동 | API/CLI를 통한 롤백 명령 |
| 배포 후 24시간 내 이상 | 수동 | 모니터링 알람 기반 판단 |
class RollbackPolicy: """롤백 정책 관리""" def __init__( self, health_check_timeout: int = 30, accuracy_threshold: float = 0.95, max_cpu_percent: float = 80, max_memory_mb: int = 4096, observation_period_hours: int = 24 ): self.health_check_timeout = health_check_timeout self.accuracy_threshold = accuracy_threshold self.max_cpu = max_cpu_percent self.max_memory = max_memory_mb self.observation_period = observation_period_hours self.deployed_at: Optional[datetime] = None self.metrics_history: List[dict] = [] def should_rollback(self, current_metrics: dict) -> tuple: """롤백 필요 여부 판단""" reasons = [] # 정확도 체크 if current_metrics.get("accuracy", 1.0) < self.accuracy_threshold: reasons.append(f"Accuracy below threshold: {current_metrics['accuracy']}") # 리소스 체크 if current_metrics.get("cpu_percent", 0) > self.max_cpu: reasons.append(f"CPU usage too high: {current_metrics['cpu_percent']}%") if current_metrics.get("memory_mb", 0) > self.max_memory: reasons.append(f"Memory usage too high: {current_metrics['memory_mb']}MB") # 추론 지연 체크 if current_metrics.get("p99_latency_ms", 0) > 100: reasons.append(f"Latency too high: {current_metrics['p99_latency_ms']}ms") return len(reasons) > 0, reasons def in_observation_period(self) -> bool: """관찰 기간 내 여부""" if self.deployed_at is None: return False elapsed = datetime.now() - self.deployed_at return elapsed < timedelta(hours=self.observation_period) def record_metrics(self, metrics: dict): """메트릭 기록 (롤백 판단용)""" self.metrics_history.append({ "timestamp": datetime.now(), **metrics }) # 최근 1시간만 유지 cutoff = datetime.now() - timedelta(hours=1) self.metrics_history = [ m for m in self.metrics_history if m["timestamp"] > cutoff ]
엣지 디바이스는 ARM64(Jetson, Raspberry Pi), AMD64(산업용 PC) 등 다양한 아키텍처를 사용합니다. Docker Buildx를 사용하여 단일 이미지로 멀티 아키텍처를 지원합니다.
# GitHub Actions workflow for multi-arch build name: Build and Push Edge AI Image on: push: tags: - 'v*' jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Registry uses: docker/login-action@v3 with: registry: ${{ secrets.REGISTRY_URL }} username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Build and Push uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | ${{ secrets.REGISTRY_URL }}/edge-ai:${{ github.ref_name }} ${{ secrets.REGISTRY_URL }}/edge-ai:latest cache-from: type=gha cache-to: type=gha,mode=max - name: Notify OTA Server run: | curl -X POST ${{ secrets.OTA_SERVER }}/releases \ -H "Authorization: Bearer ${{ secrets.OTA_TOKEN }}" \ -d '{"version":"${{ github.ref_name }}","image":"edge-ai:${{ github.ref_name }}"}'
대규모 엣지 디바이스 플릿에 대한 배포는 중앙 오케스트레이터를 통해 관리됩니다. 디바이스 그룹별 단계적 롤아웃으로 위험을 최소화합니다.
from dataclasses import dataclass from typing import List, Dict import asyncio @dataclass class DeploymentTarget: device_id: str device_group: str current_version: str status: str class FleetDeploymentOrchestrator: """대규모 엣지 플릿 배포 오케스트레이터""" def __init__(self, api_endpoint: str): self.api = api_endpoint self.deployment_status: Dict[str, str] = {} async def staged_rollout( self, targets: List[DeploymentTarget], new_version: str, stages: List[float] = [0.05, 0.25, 0.5, 1.0], # 5%, 25%, 50%, 100% validation_wait_sec: int = 300 ): """단계적 롤아웃""" total = len(targets) for stage_idx, stage_percent in enumerate(stages): target_count = int(total * stage_percent) batch = targets[:target_count] print(f"Stage {stage_idx + 1}: Deploying to {len(batch)} devices ({stage_percent*100}%)") # 배치 배포 results = await asyncio.gather(*[ self._deploy_single(t.device_id, new_version) for t in batch ]) # 결과 확인 failed = sum(1 for r in results if not r) if failed / len(batch) > 0.1: # 10% 이상 실패 시 중단 print(f"Too many failures ({failed}), aborting rollout") await self._rollback_batch(batch) return False # 안정화 대기 print(f"Waiting {validation_wait_sec}s for validation...") await asyncio.sleep(validation_wait_sec) # 메트릭 기반 검증 if not await self._validate_stage(batch): print("Stage validation failed, rolling back") await self._rollback_batch(batch) return False print(f"Stage {stage_idx + 1} completed successfully") return True async def _validate_stage(self, targets: List[DeploymentTarget]) -> bool: """스테이지 검증 (메트릭 수집 및 분석)""" # 각 디바이스의 메트릭 수집 metrics = [] for target in targets: m = await self._get_device_metrics(target.device_id) metrics.append(m) # 이상 탐지 avg_fps = statistics.mean([m["fps"] for m in metrics]) avg_latency = statistics.mean([m["latency_ms"] for m in metrics]) error_rate = sum(1 for m in metrics if m["has_error"]) / len(metrics) return avg_fps > 30 and avg_latency < 50 and error_rate < 0.05
단계적 롤아웃에서 각 스테이지 사이에 충분한 검증 시간을 두어 문제를 조기에 발견하고, 전체 플릿에 영향이 확산되기 전에 롤백할 수 있도록 합니다.