본문 바로가기
01. AI 테크 & 트렌드 (Main)/Claude & Anthropic

[보안포털 구축기 1편]보안포털 테스트 하네스 구축기 — CI/CD 자동화 완성

by 몽블86 2026. 4. 17.

 

보안포털 테스트 하네스 구축기 — 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 databasedatabase.engine = engine_testfrom 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에 저장하면 로그에도 ***로 자동 마스킹됩니다
.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초

 

🚀 이제 git push 한 번으로 테스트 → 배포까지 자동화 완성!
코드를 수정하고 push만 하면 GitHub Actions가 알아서 검사하고 서버에 올려줍니다.