2부 : 고급 Python CI/CD 파이프라인 산업화 및 보안
게시: 18 July 2025
소개
이 시리즈의 첫 번째 부분에서 우리는 Python 패키지를 PyPI에 배포하고 테스트를 자동화하기 위해 간단하면서도 효과적인 CI/CD 파이프라인을 구축했습니다.
이제 이 파이프라인을 전문화할 시간입니다. 이 두 번째 부분에서, 우리는 :
-
현대 표준인 pyproject.toml로 마이그레이션,
-
품질 및 안전 도구 추가 (Black, Mypy, Bandit, Safety),
-
멀티버전 테스트를 설정,
-
Test PyPI를 통해 점진적 배포를 통합하는
-
버전 관리를 자동화하고 파이프라인 모니터링을 개선하세요.
기능적인 파이프라인에서 전문적인 CI/CD 인프라로 전환할 준비를 하세요.
파이썬 애플리케이션을 PyPI에 배포하려면 테스트, 빌드, 패키지 출시를 자동화하는 강력한 CI/CD 파이프라인이 필요합니다. 이 문서는 Python CLI 애플리케이션을 위한 GitHub Actions를 사용한 완전한 파이프라인 구축을 자세히 설명하며, 현대 파이썬 생태계의 모범 사례를 기반으로 합니다.
CI/CD 파이프라인 아키텍처
우리가 구축할 파이프라인은 여러 단계로 구성된 접근 방식을 따릅니다 :
@startuml
!theme plain
title 파이썬 CI/CD 파이프라인 PyPI로
skinparam backgroundColor #f8f9fa
skinparam componentStyle rectangle
rectangle "개발자" as dev
rectangle "GitHub 저장소" as repo {
rectangle ".github/workflows/" as workflows
rectangle "시험/" as tests
rectangle "setup.py / pyproject.toml" as setup
rectangle "requirements.txt" as req
}
rectangle "GitHub 액션" as actions {
rectangle "테스트 러너" as test_runner
rectangle "빌드" as build
rectangle "보안 스캔" as security
rectangle "품질 검사" as quality
}
rectangle "PyPI" as pypi {
rectangle "테스트 PyPI" as test_pypi
rectangle "프로덕션 PyPI" as prod_pypi
}
rectangle "사용자들" as users
dev --> repo : push/PR
repo --> actions : trigger workflow
actions --> test_runner : run tests
actions --> security : security checks
actions --> quality : code quality
actions --> build : build package
build --> test_pypi : deploy (pre-release)
build --> prod_pypi : deploy (release)
prod_pypi --> users : install package
@enduml
프로젝트 구조
배포 준비가 된 Python CLI 애플리케이션은 표준화된 구조를 따라야 합니다 :
playlist-downloader/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── release.yml
│ └── security.yml
├── src/
│ └── playlist_downloader/
│ ├── __init__.py
│ ├── cli.py
│ ├── core/
│ └── adapters/
├── tests/
│ ├── unit/
│ ├── integration/
│ └── conftest.py
├── docs/
├── pyproject.toml
├── requirements.txt
├── requirements-dev.txt
├── MANIFEST.in
├── README.md
├── LICENSE
└── CHANGELOG.md
pyproject.toml을 사용한 패키지 구성
파일`pyproject.toml`는 Python 패키지를 구성하는 현대적인 표준입니다 :
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "playlist-downloader"
authors = [
{name = "Christophe Hérolivier", email = "[email protected]"},
]
description = "CLI tool for YouTube playlist management"
readme = "README.md"
requires-python = ">=3.8"
keywords = ["youtube", "playlist", "cli", "downloader"]
license = {text = "MIT"}
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Utilities",
]
dependencies = [
"typer>=0.9.0",
"yt-dlp>=2023.7.6",
"google-api-python-client>=2.0.0",
"google-auth-oauthlib>=1.0.0",
"pyyaml>=6.0",
"rich>=13.0.0",
]
dynamic = ["version"]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"pytest-mock>=3.10.0",
"black>=23.0.0",
"flake8>=6.0.0",
"mypy>=1.0.0",
"pre-commit>=3.0.0",
"tox>=4.0.0",
]
test = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"pytest-mock>=3.10.0",
]
[project.urls]
Homepage = "https://github.com/cheroliv/playlist-downloader"
Documentation = "https://github.com/cheroliv/playlist-downloader#readme"
Repository = "https://github.com/cheroliv/playlist-downloader.git"
"Bug Tracker" = "https://github.com/cheroliv/playlist-downloader/issues"
[project.scripts]
playlist-downloader = "playlist_downloader.cli:main"
[tool.setuptools_scm]
write_to = "src/playlist_downloader/_version.py"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--cov=src/playlist_downloader",
"--cov-report=html",
"--cov-report=term-missing",
"--cov-fail-under=85",
]
[tool.black]
line-length = 88
target-version = ['py38']
include = '\.pyi?$'
extend-exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"yt_dlp.*",
"googleapiclient.*",
"google_auth_oauthlib.*",
]
ignore_missing_imports = true
CI/CD 워크플로 - 테스트 및 품질
주요 워크플로 (ci.yml) 테스트를 여러 버전에서 Python에서 실행합니다 :
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cache/pip
~/.cache/pre-commit
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Lint with flake8
run: |
flake8 src tests --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 src tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Check code formatting with Black
run: black --check src tests
- name: Type checking with mypy
run: mypy src
- name: Run tests with pytest
run: |
pytest tests/ -v --cov=src/playlist_downloader \
--cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
if: matrix.python-version == '3.11'
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit[toml] safety
- name: Run security checks with bandit
run: bandit -r src/ -f json -o bandit-report.json
- name: Check dependencies with safety
run: safety check --json --output safety-report.json
- name: Upload security reports
uses: actions/upload-artifact@v3
if: always()
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
build:
needs: [test, security]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Check package with twine
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
릴리스 및 배포 워크플로우
릴리스 워크플로우 (release.yml) 자동 PyPI 배포를 관리 :
name: Release
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'test'
type: choice
options:
- test
- production
env:
PYTHON_VERSION: "3.11"
jobs:
release:
runs-on: ubuntu-latest
environment:
name: ${{ github.event.inputs.environment || (startsWith(github.ref, 'refs/tags/') && 'production' || 'test') }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Publish to Test PyPI
if: github.event.inputs.environment == 'test' || (startsWith(github.ref, 'refs/tags/') && contains(github.ref, 'rc'))
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
run: |
twine upload --repository testpypi dist/*
- name: Publish to PyPI
if: github.event.inputs.environment == 'production' || (startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'rc'))
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: ${{ contains(github.ref, 'rc') }}
post-release:
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Test installation from PyPI
run: |
sleep 60 # Attendre la propagation sur PyPI
pip install playlist-downloader
playlist-downloader --version
- name: Update documentation
run: |
# Script pour mettre à jour la documentation
echo "Documentation updated for version ${GITHUB_REF#refs/tags/}"
아키텍처 다이어그램
시퀀스 다이어그램 - 릴리스 프로세스
@startuml
!theme plain
title PyPI로의 릴리즈 시퀀스
actor Developer as dev
participant "GitHub" as gh
participant "GitHub Actions" as ga
participant "테스트 PyPI" as tpypi
participant "PyPI" as pypi
participant "사용자" as users
dev -> gh : git push --tags v1.2.3
gh -> ga : trigger release workflow
ga -> ga : checkout code
ga -> ga : setup Python environment
ga -> ga : install dependencies
ga -> ga : run tests
ga -> ga : build package (wheel + sdist)
ga -> ga : check package with twine
alt Pre-release (rc tag)
ga -> tpypi : upload to Test PyPI
tpypi -> ga : confirm upload
else Stable release
ga -> pypi : upload to PyPI
pypi -> ga : confirm upload
end
ga -> gh : create GitHub release
ga -> ga : test installation from PyPI
users -> pypi : pip install playlist-downloader
pypi -> users : download package
@enduml
상태 다이어그램 - 패키지 생명주기
@startuml
!theme plain
title Python 패키지 상태
[*] --> Development
Development --> Testing : commit/PR
Testing --> Development : tests fail
Testing --> Built : tests pass
Built --> TestPyPI : pre-release tag
Built --> PyPI : stable tag
TestPyPI --> PyPI : validation OK
PyPI --> Published
Published --> [*]
state Development {
[*] --> Coding
Coding --> LocalTesting
LocalTesting --> Coding : fix issues
LocalTesting --> ReadyForCI : all tests pass
}
state Testing {
[*] --> CITests
CITests --> SecurityScan
SecurityScan --> QualityCheck
QualityCheck --> BuildValidation
}
@enduml
배포 다이어그램 - CI/CD 인프라스트럭처
@startuml
!theme plain
title 배포 인프라스트럭처
node "깃허브" {
component "저장소" as repo
component "액션 러너" as runner
component "비밀 저장소" as secrets
}
node "PyPI 인프라" {
component "PyPI" as pypi
component "테스트 PyPI" as testpypi
database "패키지 인덱스" as index
}
node "개발자 머신" {
component "Git 클라이언트" as git
component "파이썬 환경" as python
component "통합 개발 환경" as ide
}
node "사용자 환경" {
component "피프" as pip_client
component "Python 런타임" as py_runtime
}
git --> repo : push code/tags
repo --> runner : trigger workflows
runner --> secrets : read API tokens
runner --> testpypi : upload pre-release
runner --> pypi : upload release
pypi --> index : store package
pip_client --> pypi : download package
py_runtime <-- pip_client : install package
@enduml
파이프라인 객체 및 모델
클래스 다이어그램 - CI/CD 모델
@startuml
!theme plain
title CI/CD 파이프라인 템플릿
class PipelineConfig {
+python_versions: List[str]
+test_environments: List[str]
+security_checks: bool
+coverage_threshold: float
+validate()
}
class BuildArtifact {
+name: str
+version: str
+wheel_path: str
+sdist_path: str
+checksums: Dict[str, str]
+validate_integrity()
}
class TestResult {
+test_suite: str
+python_version: str
+passed: int
+failed: int
+coverage: float
+duration: float
+is_success(): bool
}
class SecurityReport {
+bandit_issues: List[Issue]
+safety_vulnerabilities: List[Vulnerability]
+severity_level: str
+is_secure(): bool
}
class DeploymentTarget {
+name: str
+url: str
+api_token: str
+environment: str
+deploy(artifact: BuildArtifact)
}
class ReleaseManager {
+version: str
+changelog: str
+artifacts: List[BuildArtifact]
+test_results: List[TestResult]
+security_report: SecurityReport
+deploy_to_test()
+deploy_to_production()
+create_github_release()
}
PipelineConfig ||--o{ TestResult
ReleaseManager *-- BuildArtifact
ReleaseManager *-- SecurityReport
ReleaseManager o-- DeploymentTarget
DeploymentTarget ..> BuildArtifact : uses
@enduml
시크릿 구성
파이프라인이 작동하려면 GitHub에 다음 비밀을 구성해야 합니다:
GitHub Actions 시크릿
# Dans Settings > Secrets and variables > Actions
# Token PyPI pour la production
PYPI_API_TOKEN=pypi-...
# Token Test PyPI pour les pré-releases
TEST_PYPI_API_TOKEN=pypi-...
# Token GitHub pour créer les releases
GITHUB_TOKEN=(automatiquement fourni)
# Token Codecov (optionnel)
CODECOV_TOKEN=...
PyPI 토큰 생성
# 1. Créer un compte sur PyPI et Test PyPI
# 2. Aller dans Account Settings > API tokens
# 3. Créer un token avec scope "Entire account" ou spécifique au projet
# 4. Format du token : pypi-AgEIcHlwaS5vcmc...
로컬 개발 스크립트
개발을 용이하게 하기 위해 유용한 스크립트를 만드세요 :
메이크파일
.PHONY: install test lint format security build clean release-test release-prod
install:
pip install -e ".[dev]"
test:
pytest tests/ -v --cov=src/playlist_downloader
lint:
flake8 src tests
mypy src
format:
black src tests
security:
bandit -r src/
safety check
build:
python -m build
twine check dist/*
clean:
rm -rf build/ dist/ *.egg-info/
find . -type d -name __pycache__ -delete
find . -name "*.pyc" -delete
release-test: clean build
twine upload --repository testpypi dist/*
release-prod: clean build
twine upload dist/*
pre-commit: format lint test security
@echo "✅ Prêt pour commit"
버전 스크립트
#!/usr/bin/env python3
"""Script pour gérer les versions du projet."""
import sys
import subprocess
from pathlib import Path
def get_current_version():
"""Récupère la version actuelle depuis git."""
try:
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return "0.0.0"
def create_version_tag(version, message=None):
"""Crée un tag de version."""
if not version.startswith('v'):
version = f'v{version}'
tag_message = message or f"Release {version}"
subprocess.run(["git", "tag", "-a", version, "-m", tag_message], check=True)
print(f"✅ Tag {version} créé")
# Push le tag
subprocess.run(["git", "push", "origin", version], check=True)
print(f"✅ Tag {version} poussé vers origin")
if __name__ == "__main__":
if len(sys.argv) < 2:
current = get_current_version()
print(f"Version actuelle: {current}")
print("Usage: python version.py <new_version> [message]")
sys.exit(1)
new_version = sys.argv[1]
message = sys.argv[2] if len(sys.argv) > 2 else None
create_version_tag(new_version, message)
모범 사례 및 권고안
시맨틱 버저닝
의미론적 버전 관리(SemVer)를 사용하세요:
-
MAJOR.MINOR.PATCH(ex: 1.2.3) -
MAJOR: 호환되지 않는 변경 -
MINOR: 새로운 호환 가능한 기능 -
PATCH: 호환 가능한 버그 수정
전략 브랜칭
main ──●──●──●──●──●────●── (releases stables)
/ / /
develop ──●──●──●──●──●──●──●──●── (développement)
/ / /
feature/xxx ●──●──●──●──●──/ (fonctionnalités)
테스트 및 커버리지
-
최소 코드 커버리지: 85%
-
비즈니스 로직을 위한 단위 테스트
-
어댑터를 위한 통합 테스트
-
CLI에 대한 엔드 투 엔드 테스트
안전
-
자동 의존성 스캔 (Safety)
-
정적 코드 분석 (Bandit)
-
비밀 assoluto 코드에
-
특정 PyPI 토큰 사용
모니터링 및 관찰 가능성
파이프라인 메트릭스
# .github/workflows/metrics.yml
name: Pipeline Metrics
on:
workflow_run:
workflows: ["CI", "Release"]
types: [completed]
jobs:
metrics:
runs-on: ubuntu-latest
steps:
- name: Collect metrics
run: |
echo "Pipeline: ${{ github.event.workflow_run.name }}"
echo "Status: ${{ github.event.workflow_run.conclusion }}"
echo "Duration: ${{ github.event.workflow_run.updated_at - github.event.workflow_run.created_at }}"
# Envoyer vers système de monitoring
알림
# Ajout dans les workflows pour notifications
- name: Notify on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "❌ Pipeline failed for ${{ github.repository }}"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
결론
이 완전한 Python용 CI/CD 파이프라인은 다음을 제공합니다:
-
완전 자동화: 코드 검증에서 게시까지
-
보안자동 스캔 및 보안 비밀 관리
-
품질: 멀티 버전 테스트, 린팅 및 코드 커버리지
-
신뢰성: Test PyPI를 통한 점진적 배포
-
추적성: 아티팩트, 보고서 및 GitHub 릴리스
이러한 관행을 채택하면 Python CLI 애플리케이션에 대한 견고하고 전문적인 배포 프로세스가 보장되어 프로젝트의 유지 관리와 장기적인 발전을 용이하게 합니다.
관련 기사
31 May 2026
14 May 2026