실제 브라우저로 '다 조립된 앱'(프론트+백엔드+DB)을 태우는 통합 테스트 6개: - 인증 가드(비로그인→로그인), 로그인 실패 안내, 학생 로그인→대시보드 - 학습 센터→강좌 열기, 퀴즈 풀기→제출→서버 채점 결과 - 멘토 로그인→멘토 화면+개인정보 접근 기록(SEC-02 감사로그까지 검증) 운영 무오염 설계: - deploy/docker-compose.e2e.yml — 별도 프로젝트(mirime2e)·네트워크·휘발성 DB(tmpfs) - deploy/Caddyfile.e2e — 평문 HTTP로 SPA+/api 프록시(운영과 동일 구조) - deploy/e2e.sh — 스택 기동→테스트 학생 API 생성→퀴즈 slug 자동조회→ Playwright(브라우저 내장 이미지) 실행→down -v(정리) - 백엔드 힙 256MB 제한(작은 인스턴스에서 운영과 공존) EC2에서 6/6 통과 확인. 러너가 운영과 같은 인스턴스라 push마다가 아닌 온디맨드(릴리스 전)로 운영 — 전용 러너 확보 시 CI 게이트로 승격. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
8.6 KiB
Markdown
143 lines
8.6 KiB
Markdown
# CI — Gitea Actions (push 시 자동 테스트)
|
|
|
|
> push/PR마다 백엔드 테스트 + 프론트 빌드를 자동으로 돌려, 회귀를 배포 전에 잡는다.
|
|
> 워크플로 정의: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml)
|
|
|
|
## 구성 요소
|
|
|
|
| 요소 | 위치 | 역할 |
|
|
|---|---|---|
|
|
| Actions 활성화 | `docker-compose.prod.yml` gitea env `GITEA__actions__ENABLED=true` | Gitea에 Actions 기능 켜기 |
|
|
| 러너(runner) | compose 서비스 `runner`(`gitea/act_runner`) | 일감을 받아 job 컨테이너를 띄우는 일꾼 |
|
|
| 러너 설정·등록 | 호스트 `/opt/mirim-runner/` (`config.yaml`, `.runner`) | 최초 등록 정보·job 컨테이너 네트워크 설정 |
|
|
| 워크플로 | `.gitea/workflows/ci.yml` | 무엇을 검사할지(백엔드 test, 프론트 build) |
|
|
|
|
## 동작 흐름
|
|
|
|
1. 누가 push하면 Gitea가 워크플로를 큐에 넣는다.
|
|
2. 러너가 job을 받아 `catthehacker/ubuntu:act-22.04` 이미지로 job 컨테이너를 띄운다.
|
|
3. job 컨테이너는 `deploy_default` 네트워크 + `--add-host edu.awesomedevapp.com:host-gateway`로
|
|
Gitea에서 코드를 clone한 뒤, 백엔드 `mvn test` / 프론트 `npm ci && npm run build`를 실행한다.
|
|
4. 하나라도 실패하면 커밋에 빨간 X, 다 통과하면 초록 체크가 붙는다. (Gitea 웹 → Actions 탭)
|
|
|
|
## 러너를 처음부터 다시 세워야 한다면
|
|
|
|
```bash
|
|
# 1) Actions 활성화 (compose env, 이미 되어 있음) 후 gitea 재기동
|
|
# 2) 등록 토큰 발급
|
|
sudo docker exec -u git mirim-gitea gitea --config /data/gitea/conf/app.ini actions generate-runner-token
|
|
|
|
# 3) 설정 파일 (/opt/mirim-runner/config.yaml)
|
|
# labels: ubuntu-latest → catthehacker/ubuntu:act-22.04
|
|
# container.network: deploy_default
|
|
# container.options: --add-host=edu.awesomedevapp.com:host-gateway ← job이 Gitea에 clone하려면 필수
|
|
# 4) 최초 1회만 토큰으로 등록 (등록되면 .runner 생성, 이후 compose가 관리)
|
|
sudo docker run --rm --network deploy_default \
|
|
-v /var/run/docker.sock:/var/run/docker.sock -v /opt/mirim-runner:/data -w /data \
|
|
-e GITEA_INSTANCE_URL=http://mirim-gitea:3000 \
|
|
-e GITEA_RUNNER_REGISTRATION_TOKEN=<토큰> -e CONFIG_FILE=/data/config.yaml \
|
|
gitea/act_runner:0.2.11
|
|
# 5) compose로 상시 실행
|
|
sudo docker compose -f deploy/docker-compose.prod.yml up -d runner
|
|
```
|
|
|
|
## 함정 메모 (겪은 것)
|
|
|
|
- **Maven 다운로드**: dlcdn 미러는 옛 버전을 지운다(3.9.9가 404로 사라져 CI가 깨졌었다).
|
|
→ 모든 버전을 영구 보관하는 `archive.apache.org`를 쓴다.
|
|
- **clone 실패**: job 컨테이너가 Gitea(`edu.awesomedevapp.com:3000`)에 못 닿으면 checkout이 실패한다.
|
|
→ `--add-host edu.awesomedevapp.com:host-gateway`로 컨테이너에서 호스트(→ 게시된 3000)로 잇는다.
|
|
- **actions/checkout은 node 필요**: job 이미지에 node가 있어야 한다(catthehacker 이미지에 포함).
|
|
그래서 백엔드 job도 이 이미지를 쓰고 Maven만 따로 내려받는다.
|
|
|
|
## 배포 스모크 테스트 (`deploy/smoke-test.sh`)
|
|
|
|
CI는 "코드"를 배포 전에 검사한다. 하지만 실제로 났던 사고(보안헤더로 학습 문서 iframe이
|
|
안 뜬 것)는 코드가 아니라 **Caddy 설정** 문제라 CI 테스트로는 안 잡혔다.
|
|
스모크 테스트는 배포된 **살아있는 사이트**를 실제 사용자처럼 두드려, 설정·헤더·인프라까지
|
|
포함해 "정말 열리는가"를 확인한다. → CI(코드 그물) + 스모크(설정·운영 그물).
|
|
|
|
```bash
|
|
# 배포 직후 반드시 실행 — 하나라도 실패하면 exit 1
|
|
bash deploy/smoke-test.sh
|
|
```
|
|
|
|
검사 항목(11): 홈 200 · HTTP→HTTPS 리다이렉트 · 보안헤더 3종 ·
|
|
**학습 문서 200 + X-Frame-Options=SAMEORIGIN + no-cache + 한국어 본문**(그 회귀를 직접 감지) ·
|
|
백엔드 API 401(Caddy→백엔드→DB 경로) · SPA 라우트 폴백.
|
|
|
|
> **권장 배포 흐름**: 빌드 → 업로드 → `docker compose up` → **`bash deploy/smoke-test.sh`**.
|
|
> 스모크가 빨간불이면 배포에 문제가 있는 것이니 바로 롤백/수정한다.
|
|
|
|
## 품질 게이트 (push/PR마다 자동 검사)
|
|
|
|
CI가 통과해야 하는 관문이 아래처럼 늘었다. **로컬에서 먼저 돌려 보고 밀면** 빨간불을 피한다.
|
|
|
|
| 게이트 | 무엇을 막나 | 로컬에서 고치는 법 |
|
|
|---|---|---|
|
|
| Spotless(백엔드) | 안 쓰는 import·줄끝 공백·파일끝 개행 | `cd backend && mvn spotless:apply` |
|
|
| JaCoCo(백엔드) | 라인 커버리지 30% 미만으로 **후퇴** | 테스트 추가 (`mvn test` 후 `target/site/jacoco/index.html`) |
|
|
| ESLint(프론트) | 안 쓰는 변수·없는 전역·React 훅 오용 | `cd frontend && npm run lint` |
|
|
| 커버리지(프론트) | 채점 로직 `runSql.js` 40% 미만 | `npm run coverage` 로 확인 후 테스트 보강 |
|
|
|
|
> 커버리지 바닥(30%/40%)은 "지금보다 나빠지지 마라"는 **래칫(ratchet)**이다.
|
|
> 테스트를 늘리면 이 숫자도 함께 올려 바닥을 끌어올리는 게 건강한 흐름.
|
|
|
|
## 의존성 취약점 스캔 (라이브러리 CVE)
|
|
|
|
우리가 직접 쓴 코드뿐 아니라 "가져다 쓰는 라이브러리"에도 취약점이 생긴다. CI가 이를 훑는다.
|
|
|
|
| 대상 | 도구 | 방식 |
|
|
|---|---|---|
|
|
| 프론트(npm) | `npm audit --omit=dev --audit-level=high` | 온디맨드(대용량 DB 없음) |
|
|
| 백엔드(Maven) | OSV-Scanner v2.4.0, `scan source -r backend` | osv.dev 온디맨드 질의 |
|
|
|
|
**둘 다 비차단(`continue-on-error`)으로 "보고 전용" 시작한다.** 이유: 이 스캔은 "코드"가
|
|
아니라 "외부 권고 DB 상태"에 결합돼, 우리가 한 줄도 안 고쳐도 새 CVE가 공개되면 차단형이면
|
|
무관한 PR까지 전부 CI가 붉어진다. 팀이 수용목록(allow-list) 운영에 익숙해지면 차단으로 승격한다.
|
|
|
|
> ✅ **첫 스캔이 값어치를 증명하고, 바로 해결됐다**: 백엔드 OSV 스캔이 Spring Boot 3.4.2의
|
|
> 누적 CVE(tomcat-embed-core·spring-security-* 등, 일부 CVSS 9점대)를 다수 찾아냈다.
|
|
> **Spring Boot 3.4.2 → 3.5.16 업그레이드로 전부 해결**(tomcat 10.1.34→10.1.55,
|
|
> spring-security 6.4.2→6.5.11, spring-web 6.2.2→6.2.19 등 취약 의존성 모두 수정버전 이상).
|
|
> "스캔으로 찾고 → 버전 상향으로 고치고 → 재스캔/테스트로 확인"의 한 사이클 예시.
|
|
|
|
## main 브랜치 보호 (PR 워크플로)
|
|
|
|
`main`은 보호 규칙이 걸려 있다(Gitea → 저장소 설정 → 브랜치):
|
|
- **CI 두 개(backend-test·frontend-build)가 초록불이어야** merge 가능
|
|
- **PR에 승인 1개 필요** (멘토가 학생 PR을 리뷰·승인)
|
|
- 학생은 `main`에 직접 push 불가 → **브랜치 만들어 PR** 여는 흐름을 익힌다
|
|
- 운영·배포용으로 관리자 계정만 직접 push 허용(화이트리스트)
|
|
|
|
학생 실습 흐름: `git switch -c feat/내작업` → 커밋 → push → Gitea에서 PR 생성 →
|
|
CI 초록불 + 멘토 승인 → merge. (실무 표준 흐름을 그대로 경험)
|
|
|
|
## E2E 테스트 (`deploy/e2e.sh`)
|
|
|
|
단위/컴포넌트 테스트가 "부품"을 본다면, E2E는 **다 조립된 앱을 진짜 브라우저로** 눌러 본다 —
|
|
프론트+백엔드+DB가 함께 도는 통합 경로를 태워 "연결 지점의 회귀"를 잡는다(Playwright).
|
|
|
|
```bash
|
|
bash deploy/e2e.sh # 격리 임시 스택 기동 → 테스트 → down -v(정리)
|
|
```
|
|
|
|
검사하는 여정(6): 인증 가드(비로그인→로그인) · 로그인 실패 안내 · 학생 로그인→대시보드 ·
|
|
학습 센터→강좌 · **퀴즈 풀기→제출→채점** · 멘토 로그인→화면+감사로그.
|
|
|
|
**설계 — 운영 무오염**: 별도 프로젝트(`mirime2e`)·네트워크·**휘발성 DB(tmpfs)** 로 완전히 격리된
|
|
임시 스택을 띄우고(호스트 포트 8899), 테스트 학생을 API로 만들고, 브라우저 내장 이미지
|
|
(`mcr.microsoft.com/playwright`)로 돌린 뒤 통째로 내린다. 운영 DB/컨테이너는 건드리지 않는다.
|
|
|
|
> **왜 push마다 CI로 안 돌리나?** 러너가 운영과 같은 작은 인스턴스(3.7GB)에 있어, 매 push마다
|
|
> 두 번째 풀스택+브라우저를 띄우면 운영 메모리를 압박할 수 있다. 그래서 E2E는 **릴리스 전/필요 시
|
|
> 온디맨드**로 돌린다(백엔드 힙도 256MB로 제한). 전용 러너가 생기면 그때 CI 게이트로 승격한다.
|
|
|
|
## 다음 개선 (학생 티켓)
|
|
|
|
- Maven·npm 캐시로 CI 실행 시간 단축
|
|
- 커버리지 바닥을 단계적으로 상향(30%→40%→…)
|
|
- 스모크 테스트를 배포 스크립트에 자동 편입(deploy 후 자동 실행·실패 시 알림)
|
|
- React 컴포넌트 테스트(Testing Library) — 프론트 커버리지 확장
|
|
- 전용 러너 확보 시 E2E를 CI 게이트로 승격(현재는 온디맨드 `deploy/e2e.sh`)
|