Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/_bentoml_impl/server/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def create_dependency_watcher(

# Process environment variables from the service
for env_var in svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in env:
continue

Expand Down Expand Up @@ -207,6 +210,9 @@ def serve_http(

# Process environment variables from the service
for env_var in svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in env:
continue

Expand Down Expand Up @@ -240,6 +246,9 @@ def serve_http(
# Process environment variables for dependency services
dependency_env = env.copy()
for env_var in dep_svc.envs:
# Exclude Build Time environment variables
if env_var.stage == "build":
continue
if env_var.name in dependency_env:
continue

Expand Down
11 changes: 10 additions & 1 deletion src/_bentoml_sdk/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bentoml._internal.configuration import get_bentoml_requirement
from bentoml._internal.configuration import get_debug_mode
from bentoml._internal.configuration import get_quiet_mode
from bentoml._internal.container import split_envs_by_stage
from bentoml._internal.container.frontend.dockerfile import CONTAINER_METADATA
from bentoml._internal.container.frontend.dockerfile import CONTAINER_SUPPORTED_DISTROS
from bentoml.exceptions import BentoMLConfigException
Expand All @@ -25,6 +26,7 @@
if t.TYPE_CHECKING:
from bentoml._internal.bento.build_config import BentoEnvSchema


if sys.version_info >= (3, 11):
import tomllib
else:
Expand Down Expand Up @@ -196,8 +198,15 @@ def freeze(
docker_folder = bento_fs.joinpath("env", "docker")
docker_folder.mkdir(parents=True, exist_ok=True)
dockerfile_path = docker_folder.joinpath("Dockerfile")
runtime_envs, build_stage_envs = split_envs_by_stage(envs)
dockerfile_path.write_text(
generate_dockerfile(info, bento_fs, enable_buildkit=False, envs=envs),
generate_dockerfile(
info,
bento_fs,
enable_buildkit=bool(build_stage_envs),
envs=runtime_envs,
secret_envs=build_stage_envs,
),
)
for script_name, target_path in self.scripts.items():
shutil.copy(script_name, bento_fs / target_path)
Expand Down
12 changes: 9 additions & 3 deletions src/_bentoml_sdk/service/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ class PathMetadata(t.TypedDict):
mounted: bool


class ServiceEnvConfig(t.TypedDict, total=False):
name: str
value: str
stage: t.Literal["all", "build", "runtime"]


def with_config(
func: t.Callable[t.Concatenate["Service[t.Any]", P], R],
) -> t.Callable[t.Concatenate["Service[t.Any]", P], R]:
Expand All @@ -70,7 +76,7 @@ def wrapper(self: Service[t.Any], *args: P.args, **kwargs: P.kwargs) -> R:
return wrapper


def convert_envs(envs: t.List[t.Dict[str, t.Any]]) -> t.List[BentoEnvSchema]:
def convert_envs(envs: t.List[ServiceEnvConfig]) -> t.List[BentoEnvSchema]:
return [BentoEnvSchema(**env) for env in envs]


Expand Down Expand Up @@ -538,7 +544,7 @@ def service(
name: str | None = None,
image: Image | None = None,
description: str | None = None,
envs: list[dict[str, str]] | None = None,
envs: list[ServiceEnvConfig] | None = None,
labels: dict[str, str] | None = None,
cmd: list[str] | None = None,
service_class: type[Service[T]] = Service,
Expand All @@ -553,7 +559,7 @@ def service(
name: str | None = None,
image: Image | None = None,
description: str | None = None,
envs: list[dict[str, str]] | None = None,
envs: list[ServiceEnvConfig] | None = None,
labels: dict[str, str] | None = None,
cmd: list[str] | None = None,
service_class: type[Service[T]] = Service,
Expand Down
7 changes: 7 additions & 0 deletions src/bentoml/_internal/bento/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,11 +772,18 @@ def _model_spec_structure_hook(
return cls.from_item(d)


EnvStage = t.Literal["all", "build", "runtime"]


@attr.define(eq=True)
class BentoEnvSchema:
__forbid_extra_keys__ = False
name: str
value: str = ""
stage: EnvStage = attr.field(
default="all",
validator=attr.validators.in_(("all", "build", "runtime")),
)


bentoml_cattr.register_structure_hook(ModelSpec, _model_spec_structure_hook)
Expand Down
10 changes: 8 additions & 2 deletions src/bentoml/_internal/cloud/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@ def verify(
default_envs = [
{"name": env.name, "value": env.value}
for env in manifest.envs
if env.value and env.name not in existing_env_names
if env.value
and env.name not in existing_env_names
and env.stage != "build" # Exclude build time only env
]
if default_envs:
if self.envs is None:
Expand All @@ -206,7 +208,11 @@ def verify(
# these defaults to BentoCloud.
self.cfg_dict["envs"] = self.envs

required_envs = [env.name for env in manifest.envs if not env.value]
required_envs = [
env.name
for env in manifest.envs
if env.stage != "build" and not env.value
]
provided_envs: list[str] = [env["name"] for env in (self.envs or [])]
if self.secrets:
secret_api = SecretAPI(_client)
Expand Down
58 changes: 56 additions & 2 deletions src/bentoml/_internal/container/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from simple_di import Provide
from simple_di import inject

from ...exceptions import BentoMLException
from ...exceptions import InvalidArgument
from ..configuration.containers import BentoMLContainer
from ..utils.cattr import bentoml_cattr
Expand All @@ -24,6 +25,7 @@
if TYPE_CHECKING:
from ..bento import Bento
from ..bento import BentoStore
from ..bento.build_config import BentoEnvSchema
from ..tag import Tag
from .base import Arguments

Expand Down Expand Up @@ -112,6 +114,19 @@ def enable_buildkit(
raise ValueError("Either backend or builder must be provided.")


def split_envs_by_stage(
envs: t.Sequence[BentoEnvSchema],
) -> tuple[list[BentoEnvSchema], list[BentoEnvSchema]]:
runtime_envs: list[BentoEnvSchema] = []
build_envs: list[BentoEnvSchema] = []
for env in envs:
if env.stage == "build":
build_envs.append(env)
else:
runtime_envs.append(env)
return runtime_envs, build_envs


# XXX: Sync with BentoML extra dependencies found in pyproject.toml
FEATURES = frozenset(
{
Expand Down Expand Up @@ -216,12 +231,19 @@ def construct_containerfile(
from _bentoml_impl.docker import generate_dockerfile

assert isinstance(options, BentoInfoV2)
runtime_envs, build_env = split_envs_by_stage(options.envs)
if build_env and not enable_buildkit:
raise BentoMLException(
"stage='build' environment variables require BuildKit. "
"Enable BuildKit for your backend (e.g. set DOCKER_BUILDKIT=1)."
)
dockerfile = generate_dockerfile(
options.image,
Path(tempdir),
enable_buildkit=enable_buildkit,
add_header=add_header,
envs=options.envs,
envs=runtime_envs,
secret_envs=build_env,
)
instruction.append(dockerfile)
Path(tempdir, dockerfile_path).write_text("\n".join(instruction))
Expand All @@ -241,14 +263,45 @@ def build(
bento = _bento_store.get(bento_tag)

builder = get_backend(backend)
buildkit_enabled = enable_buildkit(builder=builder)
_, build_stage_envs = split_envs_by_stage(bento.info.envs)
secret_specs: list[str] = []
if build_stage_envs:
if not buildkit_enabled:
raise BentoMLException(
"stage='build' environment variables require BuildKit. "
"Enable BuildKit for your backend (e.g. set DOCKER_BUILDKIT=1)."
)
for env in build_stage_envs:
secret_value = os.getenv(env.name, env.value)
if secret_value is None:
raise BentoMLException(
f"Environment variable '{env.name}' (stage='build') is required during image build."
)
secret_file = tempfile.NamedTemporaryFile("w", delete=False)
secret_file.write(secret_value)
secret_file.flush()
secret_file.close()
clean_context.callback(lambda path=secret_file.name: os.remove(path))
secret_specs.append(f"id={env.name},src={secret_file.name}")

context_path, dockerfile = clean_context.enter_context(
construct_containerfile(
bento,
features=features,
enable_buildkit=enable_buildkit(builder=builder),
enable_buildkit=buildkit_enabled,
)
)
try:
if secret_specs:
existing_secret = kwargs.get("secret")
if existing_secret is None:
merged_secret: tuple[str, ...] = tuple(secret_specs)
elif isinstance(existing_secret, (tuple, list)):
merged_secret = (*existing_secret, *secret_specs)
else:
merged_secret = (existing_secret, *secret_specs)
kwargs["secret"] = merged_secret
kwargs.update({"file": dockerfile, "context_path": context_path})
return builder.build(**kwargs)
except Exception as e: # pylint: disable=broad-except
Expand Down Expand Up @@ -386,4 +439,5 @@ def get_backend(backend: str) -> OCIBuilder:
"register_backend",
"get_backend",
"REGISTERED_BACKENDS",
"split_envs_by_stage",
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
{% set bento__entrypoint = expands_bento_path('env', 'docker', 'entrypoint.sh', bento_path=bento__path) %}
{% set __enable_buildkit__ = bento__enable_buildkit | default(False) -%}
{% set __bento_envs__ = bento__envs | default([]) %}
{% set __bento_secret_envs__ = bento__secret_envs | default([]) %}
{% set __secret_mounts__ = "" %}
{% set __secret_exports__ = "" %}
{% if __bento_secret_envs__ and __enable_buildkit__ %}
{% set mounts = [] %}
{% for env in __bento_secret_envs__ %}
{% do mounts.append("--mount=type=secret,id=%s" % env.name) %}
{% endfor %}
{% set __secret_mounts__ = " ".join(mounts) + " " %}
{% set secret_exports = [] %}
{% for env in __bento_secret_envs__ %}
{% do secret_exports.append('%s="$(cat /run/secrets/%s)"' % (env.name, env.name)) %}
{% endfor %}
{% set __secret_exports__ = " ".join(secret_exports) + " " %}
{% endif %}

{% if __enable_buildkit__ %}
# 1.2.1 is the current docker frontend that both buildkitd and kaniko supports.
# syntax = {{ bento__buildkit_frontend }}
Expand Down Expand Up @@ -50,9 +66,12 @@ ENV BENTOML_HF_CACHE_DIR={{ bento__path }}/hf-models
ENV BENTOML_CONTAINERIZED=true

{% for env in __bento_envs__ %}
ARG {{ env.name }}{% if env.value %}={{ env.value }}{% endif %}
{% set stage = env.stage | default("all") -%}
{% if stage != "runtime" -%}
ARG {{ env.name }}{% if env.value %}={{ env.value | bash_quote }}{% endif %}

ENV {{ env.name }}=${{ env.name }}
{% endif -%}
{% endfor %}

RUN mkdir $BENTO_PATH && chown {{ bento__user }}:{{ bento__user }} $BENTO_PATH -R
Expand All @@ -62,10 +81,10 @@ WORKDIR $BENTO_PATH
{% block SETUP_BENTO_COMPONENTS %}
COPY --chown={{ bento__user }}:{{ bento__user }} ./env/docker ./env/docker/
{% for command in __options__commands %}
RUN {{ command }}
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}{{ command }}
{% endfor %}
RUN command -v uv >/dev/null || pip install uv
RUN UV_PYTHON_INSTALL_DIR=/app/python/ uv venv --python {{ __options__python_version }} /app/.venv && \
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}command -v uv >/dev/null || pip install uv
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}UV_PYTHON_INSTALL_DIR=/app/python/ uv venv --python {{ __options__python_version }} /app/.venv && \
chown -R {{ bento__user }}:{{ bento__user }} /app/.venv
ENV VIRTUAL_ENV=/app/.venv
ENV UV_COMPILE_BYTECODE=1
Expand All @@ -75,10 +94,10 @@ ENV PATH=/app/.venv/bin:${PATH}

COPY --chown={{ bento__user }}:{{ bento__user }} ./env/python ./env/python/
# install python packages
{% call common.RUN(__enable_buildkit__) -%} {{ __pip_cache__ }} {% endcall -%} uv --directory ./env/python/ pip install -r requirements.txt
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {{ __pip_cache__ }} {% endcall -%} {{ __secret_exports__ }}uv --directory ./env/python/ pip install -r requirements.txt

{% for command in __options__post_commands %}
RUN {{ command }}
{% call common.RUN(__enable_buildkit__) -%} {{ __secret_mounts__ }} {% endcall -%} {{ __secret_exports__ }}{{ command }}
{% endfor %}

COPY --chown={{ bento__user }}:{{ bento__user }} . ./
Expand Down
Loading