Hey everyone, I'm evaluating Pants as a potential...
# general
r
Hey everyone, I'm evaluating Pants as a potential solution to some monorepo pain points we've been having at my job. I've done a quick search and can't find exactly what I'm looking for. If possible, I'd love to see the following if anyone has links to a public repo I can check out: • A repo that deploys a Flask or FastAPI app with docker and gunicorn/uvicorn. I've seen some basic docker examples but nothing deploying using those. • A repo that packages javascript (sveltekit) or rust in a docker image. I know these are experimental still, but if anyone has a setup that they are happy with, I'd love to see it. • A repo that uses Helm. We currently deploy all of our services with Helm, and if Pants is a good solution for that as well, then I'd be interested. I've seen it in the docs but no examples. Also, if anyone has some knowledge/experience on the effort required to adopt this on a small/medium size repo (10-12 independent services with 2-3 folders of shared code), I'd love to hear about your experience. Thanks in advance! (and please let me know if this isn't the place for that, and I'm happy to move the convo elsewhere)
b
While I don’t have a public repo to show you, I have set up these things:
A repo that deploys a Flask or FastAPI app with docker and gunicorn/uvicorn. I’ve seen some basic docker examples but nothing deploying using those.
A repo that uses Helm. We currently deploy all of our services with Helm, and if Pants is a good solution for that as well, then I’d be interested. I’ve seen it in the docs but no examples.
Are there specific issues you’ve run into with those use cases?
r
Wow, thanks for the quick response. I'm still very early into the evaluating phase and just trying to get a feel for how several different options work, so I was hoping to see examples. In particular, I'm curious about Pex files as I've never used them (or even heard of them before Pants). Right, we use something similar to the following for an entrypoint in our containers:
Copy code
[
  "gunicorn",
  "-b",
  "0.0.0.0:80",
  "--max-requests",
  "0",
  "--threads",
  "10",
  "-k",
  "uvicorn.workers.UvicornWorker",
  "--preload",
  "<my.app.path>:main()",
]
I'm curious if you could share what the entry point looks like for your service. Do you use Pex for these? Additionally, what does your folder structure look like (if that's not asking too much)? After looking through some examples I was considering the following:
3rdparty/
| -- python/
src/
| -- docker/
| -- service_a/
| -- service_b/
| -- python/
| -- services/
| -- service_a/
| -- service_b/
| -- libs/
| -- lib_a/
| -- javascript/
tests/
w
This isn't exactly what you're asking for, and I keep meaning to see if I can make sample apps for some of these projects - but what you're asking for is pretty par for the course on the SvelteKit/FastAPI side (I have a multitude of projects with that stack). I don't believe there is yet TS support, so I'm currently sticking with
adhoc_tool
to run my SvelteKit stuff: https://gist.github.com/sureshjoshi/98fb09f2a340f7c1dad270c4887865a0 For FastAPI, I'm using
scies
rather than Docker, but it's not particularly hard to deploy to Docker instead (just put a
pex
into the docker container and that's already most of the work). There are already docs on some what you've asked for (e.g. package your Flask/FastAPI app into a pex, then deploy it into Docker): https://pex.readthedocs.io/en/v2.1.140/recipes.html Here's a bit of a throwaway example that needs some updates involving the main steps in using FastAPI: https://github.com/sureshjoshi/pants-plugins/tree/main/examples/python/hellofastapi And here's another example I use to make `scie`s for one of my projects (private, unfortunately):
Copy code
pex_binary(
  name="apigateway-pex",
  dependencies=[
    ":libapigateway", 
    "//:reqs#uvicorn",
    "//:reqs#gunicorn",
  ],
  include_tools=True,
  platforms=["linux-x86_64-cp-311-cp311", "macosx-13.3-arm64-cp-311-cp311",]
)

scie_binary(
 name="apigateway",
  dependencies=[":apigateway-pex"],
  platforms=["linux-x86_64", "macos-aarch64"],
  lift="lift.toml",
)
And then the
lift.toml
is similar to the
hellofastapi.toml
in the project above. Finally, one of my projects it's sitting in an Azure App Service (because of reasons outside of my control), so in that case, I'm manually unwrapping my pex on deployment and running it with these commands (e.g. using the packaged gunicorn + uvicorn):
PEX_TOOLS=1 python ./apigateway.pex venv --bin-path prepend --compile --rm all venv
venv/bin/gunicorn apigateway.main:app --bind=0.0.0.0  --timeout 600 --forwarded-allow-ips="*" -k uvicorn.workers.UvicornWorker -w $((($NUM_CORES*2)+1))
b
I use Uvicorn to deploy our FastAPI server - happy to share our BUILD file for doing so:
Copy code
python_sources(
    name="lib",
    dependencies=[
        "//:poetry#grpcio",
        "//:poetry#openai",
        "//:poetry#anthropic",
        "//:poetry#weaviate-client",
    ],
)

pex_binary(
    name="binary-deps",
    environment="linux_docker",
    layout="packed",
    execution_mode="venv",
    include_sources=False,
    include_tools=True,
)

pex_binary(
    name="binary-srcs",
    entry_point="main.py",
    dependencies=[
        ":lib",
        "api/src/py/api/scripts:lib",
        "api/src/py/api/weaviate:schema",
        "graph/src/py/graph:lib",
    ],
    environment="linux_docker",
    layout="packed",
    execution_mode="venv",
    include_requirements=False,
    include_tools=True,
)
And in
main.py
I have this at the bottom:
Copy code
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=80)
h
There is the official example-django repo, which demonstrates solutions to some of the same issues you'd presumably hit with Flask.
If this all works out, we'd welcome a Flask equivalent of this example repo!
r
Thanks for all the info! I'm going to give pants a run this week and if I end up going with it I'd be happy to submit an example repo.
w
@better-van-82973 I realize this was 6 months ago, but if you're reading this could you share what the Dockerfile looks like that uses the generated pex?
Here's a pretty barebones Dockerfile that pulls in a pex and gunicorns it
Copy code
# Following instructions here for reduced startup time and smaller footprint
# <https://pex.readthedocs.io/en/latest/recipes.html?ref=blog.pantsbuild.org#pex-app-in-a-container>
# There are still some more optimizations that could be done, see below:
# <https://blog.pantsbuild.org/optimizing-python-docker-deploys-using-pants/>

FROM python:3.11-slim as deps
COPY backend.admin/adminapi-pex.pex /api.pex
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /api.pex venv --scope=deps --compile /bin/app

FROM python:3.11-slim as srcs
COPY backend.admin/adminapi-pex.pex /api.pex
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /api.pex venv --scope=srcs --compile /bin/app

FROM python:3.11-slim
COPY --from=deps /bin/app /bin/app
COPY --from=srcs /bin/app /bin/app

# Using the same entrypoint as the Azure App Service variant
# Look into the number of cores
EXPOSE 8000
ENTRYPOINT ["/bin/app/bin/gunicorn", "admin.main:app", "--bind=0.0.0.0", "--timeout", "600", "--forwarded-allow-ips=*", "-k", "uvicorn.workers.UvicornWorker", "-w", "1"]
b
Here’s mine, it’s quite similar:
Copy code
FROM python:3.11-slim

ENTRYPOINT ["/bin/app/pex"]
EXPOSE 80

# Install package dependencies
RUN \
    --mount=target=/var/lib/apt/lists,type=cache,sharing=locked \
    --mount=target=/var/cache/apt,type=cache,sharing=locked \
    apt-get update && apt-get install -y procps build-essential python3-dev libpq-dev

# Copy deps and compile
COPY dist/api.src.py.api/binary-deps.pex /binary-deps.pex
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /binary-deps.pex venv --scope=deps --compile /bin/app

# Copy sources and compile
COPY dist/api.src.py.api/binary-srcs.pex /binary-srcs.pex
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /binary-srcs.pex venv --scope=srcs --compile /bin/app
w
Thank you so much, guys. I tried something very similar but have not been able to get the srcs pex to compile.
Copy code
FROM python:3.11-slim-bookworm as deps
COPY src.python/api-binary-deps.pex /
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /api-binary-deps.pex venv --scope=deps --compile /api-binary-deps

FROM python:3.11-slim-bookworm as srcs
COPY src.python/api-binary-srcs.pex /
RUN PEX_TOOLS=1 /usr/local/bin/python3.11 /api-binary-srcs.pex venv --scope=srcs --compile /api-binary-srcs

ENTRYPOINT ["/app/pex"]
EXPOSE 80
The error on line 7:
Copy code
ERROR: failed to solve: process "/bin/sh -c PEX_TOOLS=1 /usr/local/bin/python3.11 /api-binary-srcs.pex venv --scope=srcs --compile /api-binary-srcs" did not complete successfully: exit code: 2
Honestly, I'm not even clear what "compile" means in this context, still less how to debug this failure.
b
It seems like a permissions / shell issue? https://www.redhat.com/sysadmin/exit-codes-demystified
Exit status 2 appears when there’s a permissions problem or a missing keyword in a command or script. A missing keyword example is forgetting to add a
done
in a script’s
do
loop. The best method for script debugging with this exit status is to issue your command in an interactive shell to see the errors you receive.
So if you can run that command manually inside a Docker container, you might find your answer
w
I was able to reproduce the error by running the command from the terminal:
Copy code
python: can't open file '/api-binary-srcs.pex': [Errno 2] No such file or directory
It's strange. From the error message the script seems to want to treat
api-binary-srcs.pex
as a file it can open when it's actually a directory (with a file-like name). Stranger still, the same command worked fine for the command on
api-binary-deps.pex
. I'm definitely confused. I can't seem to find anything in the docs for Pex that explain the usage of the --compile flag, only example code, which I've tried to follow without a lot of understanding. There's this from the source code explaining the --compile flag and the trade-off of using it:
Copy code
"Compiling means that the built pex will include .pyc files, which will result in "
"slightly faster startup performance. However, compiling means that the generated pex "
"likely will not be reproducible, meaning that if you were to run `./pex -o` with the "
"same inputs then the new pex would not be byte-for-byte identical to the original."
I rather appreciate that Pex (without compilation) affords a deterministic build artifact and wonder if it's worth the "slightly faster" startup performance to give that up. But I'd like to at least be able to try the compiled version.
h
Compiling means that Pex will pre-compile the .py files into .pyc files, and bundle the .pyc files into the pex. If you don't do that then the .pyc files will be created on-demand by the interpreter at runtime. Unless you can measure the difference, you're probably better off not compiling. It's unlikely to have impact outside the noise unless your app is pretty large.
👌 1
The on-demand compilation is the usual thing every non-pex python application does, and it's almost always fine, so I would not worry about it.
But wait, are you saying that
/api-binary-srcs.pex
is a directory?
But
/api-binary-deps.pex
is not?
w
They're both present as directories. But pex only tries to open the second one as a file (if I'm reading the error message correctly). It seems improbable that the script would behave differently on those two invocations so perhaps the error message is misleading.
The error happens even if I remove the --compile flags.
h
Yes, I would expect
--compile
to be a red herring here
w
My original thought, since the generated pex file works fine as a uvicorn/FastAPI server when directly executed, was to have an extremely simple Dockerfile that did only this:
Copy code
FROM python:3.11-slim-bookworm

ENTRYPOINT ["/bin/aerial-api.pex"]
COPY src.python/aerial-api.pex /bin
In this case I build the pex without
layout="packed"
and
execution_mode="venv"
to preserve the single file pex archive. Running that image in a container does in fact start up the server exactly as happens when executing the pex file directly, but the server isn't accessible from the browser. Some missing network config perhaps. I'm not the greatest Docker expert and haven't worked that out after a bunch of fiddling with ENTRYPOINT and CMD directives.
w
Can you post your BUILD file as well? Are you in venv mode?
w
Here's the BUILD file for the pex binaries. Even if I don't compile (and based on @happy-kitchen-89482’s advice I think I will not) I do like the idea of separate 3rd party and 1st party layers in the image.
Copy code
# 3rd party deps
pex_binary(
    name="api-binary-deps",
    environment="docker_linux",
    layout="packed",
    execution_mode="venv",
    include_sources=False,
    include_tools=True,
)

# 1st party srcs
pex_binary(
    name="api-binary-srcs",
    entry_point="api.main",
    environment="docker_linux",
    layout="packed",
    execution_mode="venv",
    include_requirements=False,
    include_tools=True,
)