Minimal example of docker oci_image with custom python toolchain in bazel

109 views Asked by At

I am trying to produce a docker image using the rules_oci bazel repo.

I am using a custom python toolchain I have registered in my WORKSPACE. The following code builds a docker image that contains the python toolchain and run_api_server.py and its relevant dependencies.

My questions are:

  1. How can I correctly set the python toolchain and run_api_server as entrypoint of the docker image such that all bazel paths work out of the box?
  2. How can I isolate the relevant code that goes on the bazel project to a separate folder /app on the docker image?

BUILD:

load("@rules_oci//oci:defs.bzl", "oci_tarball")
load("@rules_python//python:defs.bzl", "py_binary")
load(":py_image.bzl", "py_docker_image")

py_binary(
    name = "run_api_server",
    srcs = "run_api_server.py",
    deps = [
        "run_api_server.py",
        "//myproject:my_py_library",
    ],
)

py_docker_image(
    name = "run_api_server_docker",
    base = "@ubuntu_2204_base",
    binary = ":run_api_server",
)

py_image.bzl (taken from https://github.com/aspect-build/bazel-examples/blob/main/oci_python_image/py_layer.bzl):

load("@aspect_bazel_lib//lib:tar.bzl", "mtree_spec", "tar")
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball")

# match *only* external repositories that have the string "python"
# e.g. this will match
#   `/hello_world/hello_world_bin.runfiles/rules_python~0.21.0~python~python3_9_aarch64-unknown-linux-gnu/bin/python3`
# but not match
#   `/hello_world/hello_world_bin.runfiles/_main/python_app`
PY_INTERPRETER_REGEX = "\\.runfiles/.*python.*-.*"

# match *only* external pip like repositozries that contain the string "site-packages"
SITE_PACKAGES_REGEX = "\\.runfiles/.*/site-packages/.*"


def py_layers(name, binary):
    """
    Create three layers for a py_binary target: interpreter, third-party dependencies, and application code.

    This allows a container image to have smaller uploads, since the application layer usually changes more
    than the other two.

    Args:
        name: prefix for generated targets, to ensure they are unique within the package
        binary: a py_binary target
    Returns:
        a list of labels for the layers, which are tar files
    """

    # Produce layers in this order, as the app changes most often
    layers = ["interpreter", "packages", "app"]

    # Produce the manifest for a tar file of our py_binary, but don't tar it up yet, so we can split
    # into fine-grained layers for better docker performance.
    mtree_spec(
        name = name + ".mf",
        srcs = [binary],
    )

    native.genrule(
        name = name + ".interpreter_tar_manifest",
        srcs = [name + ".mf"],
        outs = [name + ".interpreter_tar_manifest.spec"],
        cmd = "grep '{}' $< >$@".format(PY_INTERPRETER_REGEX),
    )

    native.genrule(
        name = name + ".packages_tar_manifest",
        srcs = [name + ".mf"],
        outs = [name + ".packages_tar_manifest.spec"],
        cmd = "grep '{}' $< >$@".format(SITE_PACKAGES_REGEX),
    )

    # Any lines that didn't match one of the two grep above
    native.genrule(
        name = name + ".app_tar_manifest",
        srcs = [name + ".mf"],
        outs = [name + ".app_tar_manifest.spec"],
        cmd = "grep -v '{}' $< | grep -v '{}' >$@".format(SITE_PACKAGES_REGEX, PY_INTERPRETER_REGEX),
    )

    result = []
    for layer in layers:
        layer_target = "{}.{}_layer".format(name, layer)
        result.append(layer_target)
        tar(
            name = layer_target,
            srcs = [binary],
            mtree = "{}.{}_tar_manifest".format(name, layer),
        )

    return result


def py_oci_image(name, binary, tars = [], **kwargs):
    """
    Wrapper around oci_image that splits the py_binary into layers.
    Note you need to wrap the result of this rule in oci_tarball to produce
    an image that can be loaded by docker
    Args:
        name: name for the target
        binary: a py_binary target
        tars: extra docker layers, apart from `binary` dependencies
        kwargs: see oci_image https://github.com/bazel-contrib/rules_oci/blob/main/docs/image.md
    """
    oci_image(
        name = name,
        tars = tars + py_layers(name, binary),
        **kwargs
    )


def py_docker_image(name, binary, repo_tags = [], **kwargs):

    py_oci_image(
        name = name + ".image",
        binary = binary,
        **kwargs,
    )

    # Wrap in oci_tarball to produce an image that can be loaded by docker
    oci_tarball(
        name = name,
        image = ":" + name + ".image",
        repo_tags = ["my.repo/py_{}:latest".format(name)] + repo_tags,
    )

WORKSPACE:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

# -----------------------------------------------------------------------------
# Load rules_python repository - contains rules for building python code
# -----------------------------------------------------------------------------

http_archive(
    name = "rules_python",
    sha256 = "9acc0944c94adb23fba1c9988b48768b1bacc6583b52a2586895c5b7491e2e31",
    strip_prefix = "rules_python-0.27.0",
    url = "https://github.com/bazelbuild/rules_python/releases/download/0.27.0/rules_python-0.27.0.tar.gz",
)

load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")

py_repositories()

python_register_toolchains(name = "python_runtime", python_version = "3.10")

load("@python_runtime//:defs.bzl", python_interpreter = "interpreter")

# -----------------------------------------------------------------------------
# Load rules_oci repository - contains rules for (docker) containers
# -----------------------------------------------------------------------------

http_archive(
    name = "rules_oci",
    sha256 = "4a276e9566c03491649eef63f27c2816cc222f41ccdebd97d2c5159e84917c3b",
    strip_prefix = "rules_oci-1.7.4",
    url = "https://github.com/bazel-contrib/rules_oci/releases/download/v1.7.4/rules_oci-v1.7.4.tar.gz",
)

load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies")

rules_oci_dependencies()

load("@rules_oci//oci:repositories.bzl", "LATEST_CRANE_VERSION", "oci_register_toolchains")

oci_register_toolchains(name = "oci", crane_version = LATEST_CRANE_VERSION)

load("@rules_oci//oci:pull.bzl", "oci_pull")

oci_pull(
    name = "ubuntu_2204_base",
    digest = "sha256:81bba8d1dde7fc1883b6e95cd46d6c9f4874374f2b360c8db82620b33f6b5ca1",
    registry = "index.docker.io",
    repository = "library/ubuntu",
)

# -----------------------------------------------------------------------------
# Load aspect_bazel_lib repository - useful for constructing rulesets and BUILD files
# -----------------------------------------------------------------------------

http_archive(
    name = "aspect_bazel_lib",
    sha256 = "f5ea76682b209cc0bd90d0f5a3b26d2f7a6a2885f0c5f615e72913f4805dbb0d",
    strip_prefix = "bazel-lib-2.5.0",
    url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.5.0/bazel-lib-v2.5.0.tar.gz",
)

load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains")

aspect_bazel_lib_dependencies()  # Required bazel-lib dependencies
aspect_bazel_lib_register_toolchains()  # Register bazel-lib toolchains
0

There are 0 answers