보안포털 테스트 하네스 구축기 — CI/CD 자동화 완성
회사 내부 보안 관리 포털(FastAPI + PostgreSQL + Nginx)에 테스트 자동화 환경을 구축한 경험을 공유합니다. 초보자도 따라할 수 있도록 삽질 포인트까지 정리했습니다.
1. 기술 스택
| 구분 | 기술 |
|---|---|
| Backend | Python FastAPI |
| Database | PostgreSQL |
| Server | Ubuntu 24.04 VPS |
| CI/CD | GitHub Actions |
| 테스트 | pytest, pytest-asyncio |
| 보안 스캔 | bandit, flake8 |
2. 왜 테스트 하네스가 필요한가?
❌ 전에는 이랬어요
코드 수정 → git push → 바로 서버 반영
(문제가 있어도 그냥 올라감 😱)
✅ 하네스 구축 후
코드 수정 → git push
↓
① 코드 스타일 검사 (flake8)
② 보안 취약점 스캔 (bandit)
③ 자동 테스트 12개 통과
↓
자동 배포 🚀
💡 한 줄 요약: git push 한 번만 하면 테스트 → 배포까지 자동!
3. 디렉토리 구조
/opt/[프로젝트명]/
├── main.py
├── database.py
├── auth.py
├── models/
├── routers/
├── tests/
│ ├── conftest.py ← 핵심 설정
│ ├── unit/
│ │ └── test_basic.py
│ └── integration/
│ └── test_news.py
├── pytest.ini
└── .github/
└── workflows/
└── ci.yml
4. 패키지 설치
cd /opt/[프로젝트명]
source venv/bin/activate
pip install pytest pytest-asyncio pytest-cov httpx pytest-httpx pyotp bandit
5. pytest.ini 설정
[pytest]
asyncio_mode = auto
testpaths = tests
addopts = --cov=. --cov-report=term-missing --cov-fail-under=40
[coverage:run]
omit =
venv/*
tests/*
💡 커버리지 기준을 40%로 낮게 시작해서 테스트가 쌓이면 올리는 방식 추천!
6. conftest.py — 가장 중요한 파일
CI 환경에서 .env 파일 없이도 동작하려면 import 순서가 핵심입니다.
import os
import sys
import pytest
# CI 환경 환경변수 설정 (실제 값은 GitHub Secrets로 관리)
os.environ.setdefault("DB_USER", "****")
os.environ.setdefault("DB_PASSWORD", os.environ.get("DB_PASSWORD", "****"))
os.environ.setdefault("DB_HOST", "127.0.0.1")
os.environ.setdefault("DB_NAME", "****_testdb")
os.environ.setdefault("SECRET_KEY", os.environ.get("SECRET_KEY", "****"))
sys.path.insert(0, '/opt/[프로젝트경로]')
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# 테스트는 SQLite 사용 (CI/로컬 모두 동작, 별도 DB 불필요)
TEST_DATABASE_URL = "sqlite:///./tests/test.db"
engine_test = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False}
)
TestingSessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine_test
)
# ⭐ 핵심: main import 전에 DB 엔진 패치!
import database
database.engine = engine_test
database.SessionLocal = TestingSessionLocal
from database import Base, get_db
from main import app
@pytest.fixture(scope="session", autouse=True)
def setup_database():
Base.metadata.create_all(bind=engine_test)
yield
Base.metadata.drop_all(bind=engine_test)
@pytest.fixture
def client(db_session):
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
app.dependency_overrides.clear()
⚠️ 주의:
import database → database.engine = engine_test → from main import app 순서를 반드시 지켜야 합니다. 순서가 바뀌면 실제 PostgreSQL에 접속하려다 오류 발생!7. 단위 테스트 예시
class TestHealthCheck:
def test_root(self, client):
resp = client.get("/")
assert resp.status_code == 200
def test_health(self, client):
resp = client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "ok"
class TestAuthEndpoints:
def test_login_wrong_password(self, client, test_user):
# form 방식으로 전송 (OAuth2PasswordRequestForm)
resp = client.post("/auth/login", data={
"username": "testuser",
"password": "틀린비밀번호"
})
assert resp.status_code in (400, 401)
🚨 삽질 포인트: FastAPI OAuth2 로그인은
json={}이 아닌 data={}로 보내야 합니다. json으로 보내면 422 Unprocessable Entity 오류 발생!8. GitHub Actions CI/CD 파이프라인
name: Security Portal CI
on:
push:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install flake8 bandit
- run: flake8 . --max-line-length=120 --exclude=venv,tests
- run: bandit -r . --exclude ./venv -lll # High 이슈만 실패처리
test:
needs: lint
runs-on: ubuntu-latest
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }} # GitHub Secrets
SECRET_KEY: ${{ secrets.SECRET_KEY }} # GitHub Secrets
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt pytest httpx pyotp
- run: pytest tests/unit tests/integration -v --no-cov
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: root
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/[프로젝트경로]
source venv/bin/activate
git pull origin main
pip install -r requirements.txt
sudo systemctl restart [서비스명]
9. GitHub Secrets 등록 방법
GitHub repo → Settings → Secrets and variables → Actions → New repository secret
| Secret 이름 | 값 | 주의사항 |
|---|---|---|
VPS_HOST |
서버 IP 주소 | 공개 IP |
VPS_SSH_KEY |
SSH 개인키 전체 | BEGIN~END 포함 |
DB_PASSWORD |
DB 비밀번호 | 절대 코드에 넣지 말 것 |
SECRET_KEY |
JWT 시크릿 키 | 절대 코드에 넣지 말 것 |
🔒 보안 필수 사항
• SSH 개인키, DB 비밀번호는 절대 코드나 채팅에 붙여넣지 마세요
• GitHub Secrets에 저장하면 로그에도
•
• SSH 개인키, DB 비밀번호는 절대 코드나 채팅에 붙여넣지 마세요
• GitHub Secrets에 저장하면 로그에도
***로 자동 마스킹됩니다•
.env 파일은 반드시 .gitignore에 추가하세요10. 삽질 포인트 총정리
① venv 활성화 필수
# ❌ 이렇게 하면 pytest 못 찾음
python3 -m pytest
# ✅ 반드시 venv 활성화 후
source venv/bin/activate
pytest
② flake8 기존 코드 오류 많을 때
# 한 번에 ignore 처리 (기존 코드가 많을 때)
flake8 . --ignore=E302,E501,E128,E226,E241
③ main.py import 시 DB 연결 오류
# conftest.py에서 import 순서가 핵심!
import database
database.engine = engine_test # ① 먼저 패치
from main import app # ② 그 다음 import
④ systemctl reload vs restart
# ❌ reload는 일부 서비스에서 동작 안 함
sudo systemctl reload [서비스명]
# ✅ restart 사용
sudo systemctl restart [서비스명]
최종 결과
✅ lint (9s) — flake8 + bandit 통과
✅ test (1m 0s) — 12 passed, 0 failed
✅ deploy (8s) — VPS 자동 배포 완료
총 소요시간: 1분 27초
✅ test (1m 0s) — 12 passed, 0 failed
✅ deploy (8s) — VPS 자동 배포 완료
총 소요시간: 1분 27초
🚀 이제 git push 한 번으로 테스트 → 배포까지 자동화 완성!
코드를 수정하고 push만 하면 GitHub Actions가 알아서 검사하고 서버에 올려줍니다.
코드를 수정하고 push만 하면 GitHub Actions가 알아서 검사하고 서버에 올려줍니다.
'01. AI 테크 & 트렌드 (Main) > Claude & Anthropic' 카테고리의 다른 글
| [보안포털 구축기 5편] 정보자산 관리전면 고도화 (1) | 2026.05.01 |
|---|---|
| [보안포털 구축기 4편]도메인 연동 &보안이슈관리 고도화 (0) | 2026.04.27 |
| [보안포털 구축기 3편] systemd → Docker 전환 — 기존 데이터 무손실 이전 (1) | 2026.04.18 |
| [보안포털 구축기 2편]보안포털 코드 보안 강화 — 7가지 취약점 수정기 (2) | 2026.04.18 |
| Claude에서 Harness란 무엇인가? (0) | 2026.04.17 |