AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article파이썬 코드를 다른 사람도 사용할 수 있게 공유하거나, 불필요한 파일들을 제거하고 프로젝트를 배포하려면 패키징을 하는 것이 가장 효율적입니다. 파이썬은 나름 긴 역사와 함께 패키징 또한 여러 방식을 거쳐 발전하고 있습니다. 이 블로그에서는 파이선 패키징의 문제점 2개를 설명하고, 작성 시점에서 가장 권장되고 표준인 패키징 방식을 설명하려고 합니다.
제가 알고 있던 역사를 정리하려고 찾아보니 좋은 글이 있어서 그 글을 소개하는 것으로 대체하려고 합니다.
https://ryanking13.github.io/2021/07/11/python-packaging.html
다음 사항에 포함된다면 legacy를 사용하고 있다고 볼 수 있습니다.
Poetry를 사용하고 있다면 legacy가 아닙니다. 다만 아래 부록을 참고하면 배포를 좀 더 쉽게 만들 수 있습니다.
파이썬에서 패키지 포맷은 Binary Distribution인 wheel과 Source Distribution으로 이미 표준이 있습니다. 파일 이름과 위치에 맞춰서 넣기만 하면 setuptools나 Poetry로 패키징한 패키지와 동일하게 설치할 수 있습니다. 하지만 패키징 자체는 표준이 없고 대신 setuptools와 setup.py가 사실상 표준으로 사용되고 있었고, setuptools는 third party library입니다. setup.py를 읽기 위해서는 setuptools 패키지가 필요하기 때문에 무슨 패키징 툴을 개발하든 setuptools를 의존해야 합니다.
runtime dependency는 패키지에 적혀있기 때문에 pip이 알아서 같이 설치를 해줍니다.
만약 패키징할 때 setup.py에서 특정 라이브러리를 import 해서 버전 관리를 따로 한다면?
해당 패키지 설치 없이 python setup.py
는 당연히 ImportError
가 발생할 것이고, 파일을 열어 dependency를 확인한 후 직접 설치해야 합니다.
setup.py 스크립트를 사용했을 때 코드 실행 때문에 위와 같은 의존성 문제가 발생한다면, 스크립트를 없애자!
pyproject.toml
PEP-518 에서 build-time dependency를 선언적으로 관리하기 위해 추가된 파일.
최근에는 용도가 확장돼서 pytest, mypy, 다양한 linter 등 다양한 툴들의 설정 파일과 project metadata를 저장하는 용도로 사용되고 있습니다.
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
위의 예시 내용이 pyproject.toml
에 적혀있고 PEP-517 를 만족하는 패키징 툴이 있다면,
setuptools나 poetry가 없는 환경에서도 필요한 build system과 dependency들을 설치하고 패키징하는 것을 자동화할 수 있습니다.
찾아보기론 현재 PEP-517 만족하는 툴은 build 와 pep517 인데, 후자는 deprecate 되었고 전자를 대부분 사용하는 것 같습니다.
Poetry 또한 PEP-517을 지원하는데, 테스트해보니 backend가 poetry인 경우만 동작하는 것으로 보입니다.
build 의 경우 아래의 두 줄로 패키징할 수 있습니다. (poetry 또는 setuptools 등 backend의 사전 설치 불필요)
pip install build
python -m build .
위의 설정만으로 패키징이 끝나는 것은 아닙니다. 필수 메타데이터인 패키지 이름, 버전, dependency 이외에도 파이썬 패키지에는 readme, tag, entrypoint 등 다양한 메타데이터를 심을 수 있습니다. 이 부분 또한 표준화되어있지만, 아쉽게도 비교적 최근에 표준화되어서 (PEP-621: 2021/3/1에 Final로 변경) 아직은 주요 빌드툴에서 지원되지 않습니다.
build system | status | link |
---|---|---|
setuptools | 논의 중 | https://github.com/pypa/setuptools/issues/1688 |
Poetry | 논의 중 | https://github.com/python-poetry/poetry/issues/3332 |
Flit | 지원 | https://flit.readthedocs.io/en/latest/pyproject_toml.html#pyproject-toml-project |
그 때문에 각 빌드툴 specific한 포맷을 따라서 메타데이터를 작성해야 합니다.
(setuptools - setup.cfg
, Poetry - pyproject.toml
의 [tool.poetry]
)
아래는 각 독자 규격 샘플과 문서를 나열했고, 마지막은 미래에 모든 빌드툴 공통으로 쓰일 표준 포맷 샘플을 작성했습니다.
공식 문서: https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html#declarative-config
setup.py
에 있던 모든 필드를 ini 포맷의 setup.cfg
에 선언적인 방식으로 기록합니다.
[metadata]
name = my_package
version = attr: src.VERSION
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
license = BSD 3-Clause License
classifiers =
Framework :: Django
License :: OSI Approved :: BSD License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[options]
zip_safe = False
include_package_data = True
packages = find:
scripts =
bin/first.py
bin/second.py
install_requires =
requests
importlib; python_version == "2.6"
[options.package_data]
* = *.txt, *.rst
hello = *.msg
[options.entry_points]
console_scripts =
executable-name = package.module:function
[options.extras_require]
pdf = ReportLab>=1.2; RXP
rest = docutils>=0.3; pack ==1.1, ==1.3
[options.packages.find]
exclude =
src.subpackage1
src.subpackage2
[options.data_files]
/etc/my_package =
site.d/00_default.conf
host.d/00_default.conf
data = data/img/logo.png, data/svg/icon.svg
fonts = data/fonts/*.ttf, data/fonts/*.otf
공식 문서: https://python-poetry.org/docs/pyproject/
setuptools와 다르게 최근에 개발됐기 때문에 처음부터 setup.py
대신 pyproject.toml
을 사용했습니다.
다만 PEP-621가 accept 되기 전에 개발됐기 때문에 [project]
대신 [tool.poetry]
에 PEP-621과는 약간 다른 독자 포맷으로 기록합니다.
[tool.poetry]
name = "poetry"
version = "1.2.0a2"
description = "Python dependency management and packaging made easy."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.md"
include = [
{ path = "tests", format = "sdist" }
]
homepage = "https://python-poetry.org/"
repository = "https://github.com/python-poetry/poetry"
documentation = "https://python-poetry.org/docs"
keywords = ["packaging", "dependency", "poetry"]
classifiers = [
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules"
]
[tool.poetry.build]
generate-setup-file = false
# Requirements
[tool.poetry.dependencies]
python = "^3.6"
poetry-core = "^1.1.0a6"
cleo = "^1.0.0a4"
crashtest = "^0.3.0"
requests = "^2.18"
cachy = "^0.3.0"
requests-toolbelt = "^0.9.1"
cachecontrol = { version = "^0.12.4", extras = ["filecache"] }
pkginfo = "^1.5"
html5lib = "^1.0"
shellingham = "^1.1"
tomlkit = ">=0.7.0,<1.0.0"
pexpect = "^4.7.0"
packaging = "^20.4"
# temporarily clamped due to https://github.com/pypa/pip/issues/9953
virtualenv = ">=20.4.3,<20.4.5"
keyring = ">=21.2.0"
entrypoints = "^0.3"
importlib-metadata = {version = "^1.6.0", python = "<3.8"}
dataclasses = {version = "^0.8", python = "~3.6"}
[tool.poetry.dev-dependencies]
pytest = "^6.2"
pytest-cov = "^2.8"
pytest-mock = "^3.5"
pre-commit = { version = "^2.6", python = "^3.6.1" }
tox = "^3.0"
pytest-sugar = "^0.9"
httpretty = "^1.0"
zipp = { version = "^3.4", python = "<3.8"}
deepdiff = "^5.0"
[tool.poetry.scripts]
poetry = "poetry.console.application:main"
공식 문서
setuptools와 Poetry가 PEP-621를 지원하면 아래 포맷으로 메타데이터를 작성하면 어떤 툴을 쓰더라도 상호 교차적으로 사용할 수 있습니다. 하지만 위의 표처럼 setuptools와 Poetry는 아직 표준을 따르도록 구현되지 않았기 때문에 아쉽게도 지금은 사용할 수 없습니다. 지금 아래의 포맷으로 패키징을 하고싶다면 유일한 선택지는 Flit 입니다.
[project]
name = "spam"
version = "2020.0.0"
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
requires-python = ">=3.8"
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
authors = [
{email = "hi@pradyunsg.me"},
{name = "Tzu-Ping Chung"}
]
maintainers = [
{name = "Brett Cannon", email = "brett@python.org"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python"
]
dependencies = [
"httpx",
"gidgethub[httpx]>4.0.0",
"django>2.1; os_name != 'nt'",
"django>2.0; os_name == 'nt'"
]
[project.optional-dependencies]
test = [
"pytest < 5.0.0",
"pytest-cov[all]"
]
[project.urls]
homepage = "example.com"
documentation = "readthedocs.org"
repository = "github.com"
changelog = "github.com/me/spam/blob/master/CHANGELOG.md"
[project.scripts]
spam-cli = "spam:main_cli"
[project.gui-scripts]
spam-gui = "spam:main_gui"
[project.entry-points."spam.magical"]
tomatoes = "spam:main_tomatoes"
Dockerfile이나 CI pipeline에서는 potery
커맨드 대신 python -m build
를 사용하자
Poetry 를 사용하면 패키징 이외에도 virtual env 관리, dependency resolving, 패키지 업로드 등
개발에 필요한 기능들을 한 번에 해결할 수 있습니다.
다만 Dockerfile이나 CI pipeline에서 패키징을 위해 poetry
커맨드를 사용하려고 하면 위의 기능을 모두 설치해야 합니다.
여기서 사소한 문제는 poetry
패키지의 dependency가 많아서 poetry
패키지를 설치하는 데만 시간이 좀 걸린다는 것입니다.
특히 alpine 이미지를 사용할 경우 복잡해지는데,
dependency 중에는 C 코드를 포함하는 패키지가 (cryptography) 있어서 각종 빌드 프로그램들을 설치하고 컴파일도 해줘야 합니다
(현재 alpine은 wheel 포맷을 지원하지 않기 때문).
그 때문에 로컬의 호스트 머신에서 사용할 게 아니라면 Poetry로 관리한다고 하더라도 위에 설명한 build
커맨드를 사용하는 것이 가장 편리합니다.
아래는 Dockerfile을 예시로 작성했습니다.
FROM python:3.9-alpine
WORKDIR /src
COPY . .
RUN apk --update add gcc musl-dev openssl-dev libffi-dev cargo
# 아래가 rust, c 코드 컴파일로 몇분이 소요됨
RUN pip install poetry
RUN poetry build
FROM python:3.9-alpine
WORKDIR /src
COPY . .
RUN pip install build
RUN python -m build
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article들어가며 안녕하세요, 버즈빌 데이터 엔지니어 Abel 입니다. 이번 포스팅에서는 데이터 파이프라인 CI 테스트에 소요되는 시간을 어떻게 7분대에서 3분대로 개선하였는지에 대해 소개하려 합니다. 배경 이전에 버즈빌의 데이터 플랫폼 팀에서 ‘셀프 서빙 데이터 …
Read Article