diff --git a/docs/CI-CD.md b/docs/CI-CD.md index 554c4ab..96e4c1a 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -22,8 +22,8 @@ ML 배포는 systemd 상태와 `/health` 응답을 확인해야 성공합니다. - CD는 `rallytrack-pi` 또는 `rallytrack-ml` 라벨을 가진 self-hosted runner에서만 실행합니다. 두 서버가 `192.168.219.x` 사설망에 있어 GitHub-hosted runner가 직접 접근할 수 없기 때문입니다. - 배포 job은 PR에서 절대 실행하지 않고 기본 브랜치 push에서만 실행합니다. 권한은 기본값을 - 없앤 뒤 `contents: read`만 부여했습니다. 저장소는 private으로 유지하고 외부 fork 코드를 - self-hosted runner에서 실행하지 않습니다. + 없앤 뒤 `contents: read`만 부여했습니다. 현재 저장소가 public이므로 runner group을 기본 + 브랜치의 지정 workflow로 제한하고, Environment 승인 전에는 self-hosted runner를 사용하지 않습니다. - 앱별 배포는 변경된 서비스만 순차 빌드합니다. Pi에서 backend와 frontend를 동시에 빌드해 메모리 피크가 커지는 것을 피하면서 배포 시간도 전체 재빌드보다 줄입니다. - 컨테이너 레지스트리와 다중 아키텍처 이미지 배포는 현재 단계에서 제외했습니다. 지금 규모에는 @@ -35,9 +35,11 @@ ML 배포는 systemd 상태와 `/health` 응답을 확인해야 성공합니다. ## 3. GitHub 최초 설정 -현재 저장소는 public이고 self-hosted runner가 등록돼 있지 않으므로 CD는 기본적으로 비활성입니다. -public 저장소의 fork PR이 사설 runner에서 실행될 여지를 만들지 않도록, 저장소를 private으로 -전환하거나 배포 전용 private 저장소/제한된 runner group으로 격리한 뒤 활성화해야 합니다. +2026-09-07 기준 저장소는 public이며 CD가 활성화돼 있습니다. Pi의 `JUN` runner는 +`pi-production` group에서 backend/devops의 `main` workflow와 frontend의 `develop` workflow만 +허용합니다. ML의 `ml-server` runner는 `ml-production` group에서 aiAnalysis-server의 `main` +workflow만 허용합니다. 두 group 모두 저장소와 workflow를 명시적으로 제한하며, 네 저장소의 +배포 Environment는 승인자를 요구합니다. 각 저장소의 Settings에서 다음을 한 번 설정해야 합니다. @@ -53,11 +55,11 @@ public 저장소의 fork PR이 사설 runner에서 실행될 여지를 만들지 직접 push 대신 PR을 요구합니다. runner 사용자는 Pi에서 GitHub 저장소를 fetch하고 Docker를 실행할 수 있어야 합니다. ML 서버에서는 -비밀번호 입력 없이 아래 두 명령만 수행할 수 있도록 sudoers 권한을 좁게 부여합니다. +비밀번호 입력 없이 `restart`만 수행할 수 있도록 sudoers 권한을 좁게 부여합니다. `is-active`는 +일반 사용자도 실행할 수 있으므로 sudoers에 추가하지 않습니다. ```text /usr/bin/systemctl restart rallytrack-ai -/usr/bin/systemctl is-active rallytrack-ai ``` 설정 후 `workflow_dispatch`로 CI/CD를 한 번 실행해 runner 라벨, 환경 승인, 작업 경로를 검증합니다. @@ -94,6 +96,13 @@ Pi compose의 상한은 MariaDB 600 MB, MinIO 512 MB, backend 1.2 GB, frontend 1 cloudflared 128 MB입니다. Java heap은 768 MB로 제한됩니다. 이 구성은 소규모 데모/팀 테스트와 낮은 동시 접속에는 적합하지만 아래 기능은 같은 Pi에 추가하지 않는 편이 안전합니다. +현재 Pi 커널에는 memory cgroup controller가 활성화돼 있지 않아 Docker가 위 `mem_limit`을 +실제로 적용하지 못하고 경고합니다. Java heap 768 MB와 MariaDB buffer pool 128 MB 같은 +애플리케이션 자체 제한은 계속 유효하지만 컨테이너 단위 OOM 격리는 없습니다. 이 서버의 +가용 메모리는 배포 직후 약 4.7 GiB였으므로 현재 데모 구성은 운용 가능하나, 서비스를 더 올리기 +전에는 점검 시간을 잡아 boot cmdline에 memory cgroup을 활성화하고 재부팅 후 Docker의 +`MemoryLimit=true`를 확인해야 합니다. + - 영상 인코딩이나 AI 추론: ML 서버에서 처리 - 대규모 로그/메트릭 스택: 외부 서비스 또는 가벼운 단일 에이전트 사용 - Pi에서 병렬 Docker 빌드: 배포 스크립트가 의도적으로 순차 실행 @@ -119,8 +128,20 @@ cloudflared 128 MB입니다. Java heap은 768 MB로 제한됩니다. 이 구성 분리해 허용된 환경에서 다시 실행했고 전체 테스트 통과를 확인했습니다. - Python compileall의 기본 캐시 경로가 workspace 밖이라 실패했습니다. CI/배포 모두 전용 임시 `PYTHONPYCACHEPREFIX`를 사용하게 해 소스와 무관한 권한 문제를 제거했습니다. +- 기존 운영 컨테이너와 볼륨은 Compose 프로젝트 `devops`로 만들어졌는데 최초 배포 스크립트는 + `rallytrack`을 강제했습니다. 새 빈 DB/MinIO 볼륨 생성 또는 고정 컨테이너 이름 충돌 위험을 + 컨테이너 label과 mount로 재현했고, 기본 프로젝트명을 `devops`로 고정한 뒤 CI 회귀 검사를 추가했습니다. +- 프론트는 외부에서 HTTP 200을 반환했지만 Docker healthcheck의 `localhost`가 Alpine에서 IPv6 + `::1`을 먼저 선택해 unhealthy가 됐습니다. 컨테이너 안에서 `localhost` 실패와 `127.0.0.1` + 성공을 비교한 뒤 healthcheck를 IPv4로 고정하고 실제 배포에서 healthy를 확인했습니다. +- Pi Docker가 `No memory limit support`를 보고했습니다. 이는 Compose 문법 오류가 아니라 커널의 + memory cgroup 비활성 상태이므로, 운영 중 무단 재부팅 대신 위 제약을 기록하고 별도 점검 창에서 + 커널 설정을 적용하기로 했습니다. - 프론트 번들은 정상 빌드되지만 약 1 MB의 단일 JS chunk 경고가 남습니다. 기능 오류는 아니므로 이번 배포의 차단 조건은 아니며, 초기 로딩 지표를 측정한 뒤 route 단위 code splitting을 적용합니다. +- 프론트 이미지 빌드의 `npm ci`는 현재 17개 취약점(critical 2개 포함)을 경고합니다. 자동 + `npm audit fix --force`는 잠금 파일과 런타임 호환성을 동시에 바꿀 수 있어 배포 중 적용하지 않았으며, + 별도 의존성 업데이트 PR에서 운영 번들 회귀 테스트와 함께 처리해야 합니다. ## 7. 운영 확인 명령