Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions docs/CI-CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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를 동시에 빌드해
메모리 피크가 커지는 것을 피하면서 배포 시간도 전체 재빌드보다 줄입니다.
- 컨테이너 레지스트리와 다중 아키텍처 이미지 배포는 현재 단계에서 제외했습니다. 지금 규모에는
Expand All @@ -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에서 다음을 한 번 설정해야 합니다.

Expand All @@ -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 라벨, 환경 승인, 작업 경로를 검증합니다.
Expand Down Expand Up @@ -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 빌드: 배포 스크립트가 의도적으로 순차 실행
Expand All @@ -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. 운영 확인 명령

Expand Down