Beispiel #1
0
    def __init__(
        self,
        bento_service=None,
        kind="BentoService",
        bentoml_deployment_version: str = Provide[
            BentoMLContainer.bento_bundle_deployment_version
        ],
    ):
        self.kind = kind
        self._yaml = YAML()
        self._yaml.default_flow_style = False
        self.config = self._yaml.load(
            BENTOML_CONFIG_YAML_TEMPLATE.format(
                kind=self.kind,
                bentoml_version=bentoml_deployment_version,
                created_at=str(datetime.utcnow()),
            )
        )

        if bento_service is not None:
            self.config["metadata"].update(
                {
                    "service_name": bento_service.name,
                    "service_version": bento_service.version,
                }
            )
            self.config["env"] = bento_service.env.to_dict()
            self.config['apis'] = _get_apis_list(bento_service)
            self.config['artifacts'] = _get_artifacts_list(bento_service)
Beispiel #2
0
    def __init__(
        self,
        name: str = None,
        channels: List[str] = None,
        dependencies: List[str] = None,
        default_env_yaml_file: str = None,
        override_channels: bool = False,
    ):
        self._yaml = YAML()
        self._yaml.default_flow_style = False

        if default_env_yaml_file:
            env_yml_file = Path(default_env_yaml_file)
            if not env_yml_file.is_file():
                raise BentoMLException(
                    f"Can not find conda environment config yaml file at: "
                    f"`{default_env_yaml_file}`")
            self._conda_env = self._yaml.load(env_yml_file)
        else:
            self._conda_env = self._yaml.load(DEFAULT_CONDA_ENV_BASE_YAML)

        if name:
            self.set_name(name)

        if override_channels and channels is None:
            raise BentoMLException(
                "No `conda_channels` provided while override_channels=True")

        if channels:
            self.set_channels(channels, override_channels)

        if dependencies:
            self.add_conda_dependencies(dependencies)
Beispiel #3
0
def dump_to_yaml_str(yaml_dict):
    from bentoml.utils.ruamel_yaml import YAML

    yaml = YAML()
    string_io = StringIO()
    yaml.dump(yaml_dict, string_io)
    return string_io.getvalue()
Beispiel #4
0
def test_conda_env_yml_file_option(tmpdir):
    conda_env_yml_file = os.path.join(tmpdir, 'environment.yml')
    with open(conda_env_yml_file, 'wb') as f:
        f.write("""
name: bentoml-test-conda-env
channels:
  - test-ch-1
  - test-ch-2
dependencies:
  - test-dep-1
""".encode())

    @bentoml.env(conda_env_yml_file=conda_env_yml_file)
    class ServiceWithCondaDeps(bentoml.BentoService):
        @bentoml.api(input=DataframeInput(), batch=True)
        def predict(self, df):
            return df

    service_with_string = ServiceWithCondaDeps()
    service_with_string.save_to_dir(str(tmpdir))

    from pathlib import Path

    from bentoml.utils.ruamel_yaml import YAML

    yaml = YAML()
    env_yml = yaml.load(Path(os.path.join(tmpdir, 'environment.yml')))
    assert 'test-ch-1' in env_yml['channels']
    assert 'test-ch-2' in env_yml['channels']

    assert 'test-dep-1' in env_yml['dependencies']
Beispiel #5
0
def parse_yaml_file_callback(ctx, param, value):  # pylint: disable=unused-argument
    yaml = YAML()
    yml_content = value.read()
    try:
        return yaml.load(yml_content)
    except Exception:
        raise click.BadParameter(
            'Input value is not recognizable yaml file or yaml content')
Beispiel #6
0
class CondaEnv(object):
    """A wrapper around conda environment settings file, allows adding/removing
    conda or pip dependencies to env configuration, and supports load/export those
    settings from/to yaml files. The generated file is the same format as yaml file
    generated from `conda env export` command.
    """
    def __init__(
        self,
        name: str = None,
        channels: List[str] = None,
        dependencies: List[str] = None,
        default_env_yaml_file: str = None,
        override_channels: bool = False,
    ):
        self._yaml = YAML()
        self._yaml.default_flow_style = False

        if default_env_yaml_file:
            env_yml_file = Path(default_env_yaml_file)
            if not env_yml_file.is_file():
                raise BentoMLException(
                    f"Can not find conda environment config yaml file at: "
                    f"`{default_env_yaml_file}`")
            self._conda_env = self._yaml.load(env_yml_file)
        else:
            self._conda_env = self._yaml.load(DEFAULT_CONDA_ENV_BASE_YAML)

        if name:
            self.set_name(name)

        if override_channels and channels is None:
            raise BentoMLException(
                "No `conda_channels` provided while override_channels=True")

        if channels:
            self.set_channels(channels, override_channels)

        if dependencies:
            self.add_conda_dependencies(dependencies)

    def set_name(self, name):
        self._conda_env["name"] = name

    def add_conda_dependencies(self, conda_dependencies: List[str]):
        # BentoML uses conda's channel_priority=strict option by default
        # Adding `dependencies` to beginning of the list to take priority over the
        # existing conda channels
        self._conda_env["dependencies"] = (conda_dependencies +
                                           self._conda_env["dependencies"])

    def set_channels(self, channels: List[str], override_channels=False):
        if override_channels and "nodefaults" not in channels:
            channels.append("nodefaults")
        self._conda_env["channels"] = channels

    def write_to_yaml_file(self, filepath):
        with open(filepath, 'wb') as output_yaml:
            self._yaml.dump(self._conda_env, output_yaml)
Beispiel #7
0
class CondaEnv(object):
    """A wrapper around conda environment settings file, allows adding/removing
    conda or pip dependencies to env configuration, and supports load/export those
    settings from/to yaml files. The generated file is the same format as yaml file
    generated from `conda env export` command.
    """
    def __init__(
        self,
        name: str,
        python_version: str,
        conda_channels: List[str] = None,
        conda_dependencies: List[str] = None,
    ):
        self._yaml = YAML()
        self._yaml.default_flow_style = False

        self._conda_env = self._yaml.load(
            CONDA_ENV_BASE_YAML.format(name=name,
                                       python_version=python_version))

        if conda_channels:
            self.add_channels(conda_channels)

        if conda_dependencies:
            self.add_conda_dependencies(conda_dependencies)

    def set_name(self, name):
        self._conda_env["name"] = name

    def get_name(self):
        return self._conda_env["name"]

    def add_conda_dependencies(self, conda_dependencies: List[str]):
        # BentoML uses conda's channel_priority=strict option by default
        # Adding `conda_dependencies` to beginning of the list to take priority over the
        # existing conda channels
        self._conda_env["dependencies"] = (conda_dependencies +
                                           self._conda_env["dependencies"])

    def add_channels(self, channels: List[str]):
        for channel_name in channels:
            if channel_name not in self._conda_env["channels"]:
                self._conda_env["channels"] += channels
            else:
                logger.debug(f"Conda channel {channel_name} already added")

    def write_to_yaml_file(self, filepath):
        with open(filepath, 'wb') as output_yaml:
            self._yaml.dump(self._conda_env, output_yaml)
Beispiel #8
0
            def wrapped_save(*args, **kwargs):
                if not self.is_ready:
                    raise FailedPrecondition(
                        "Trying to save empty artifact. An artifact needs to be `pack` "
                        "with model instance or `load` from saved path before saving"
                    )

                # save metadata
                dst = args[0]  # save(self, dst)
                if self.metadata:
                    yaml = YAML()
                    yaml.dump(self.metadata, Path(self._metadata_path(dst)))

                original = object.__getattribute__(self, item)
                return original(*args, **kwargs)
Beispiel #9
0
def configure_logging(logging_level=None):
    base_log_dir = os.path.expanduser(config("logging").get("BASE_LOG_DIR"))
    Path(base_log_dir).mkdir(parents=True, exist_ok=True)
    if os.path.exists(config("logging").get("logging_config")):
        logging_config_path = config("logging").get("logging_config")
        with open(logging_config_path, "rb") as f:
            logging_config = YAML().load(f.read())
        logging.config.dictConfig(logging_config)
        logging.getLogger(__name__).debug(
            "Loaded logging configuration from %s." % logging_config_path)
    else:
        if logging_level is None:
            logging_level = config("logging").get("LEVEL").upper()
            if "LOGGING_LEVEL" in config("logging"):
                # Support legacy config name e.g. BENTOML__LOGGING__LOGGING_LEVEL=debug
                logging_level = config("logging").get("LOGGING_LEVEL").upper()

        if get_debug_mode():
            logging_level = logging.getLevelName(logging.DEBUG)

        logging_config = get_logging_config_dict(logging_level, base_log_dir)
        logging.config.dictConfig(logging_config)
        logging.getLogger(__name__).debug(
            "Loaded logging configuration from default configuration " +
            "and environment variables.")
Beispiel #10
0
def _get_bento_service_event_properties(bento_service, properties=None):
    bento_service_metadata = bento_service.get_bento_service_metadata_pb()

    if properties is None:
        properties = {}

    artifact_types = set()
    input_types = set()
    output_types = set()

    for artifact in bento_service_metadata.artifacts:
        artifact_types.add(artifact.artifact_type)

    for api in bento_service_metadata.apis:
        input_types.add(api.input_type)
        output_types.add(api.output_type)

    if input_types:
        properties["input_types"] = list(input_types)
    if output_types:
        properties["output_types"] = list(output_types)

    if artifact_types:
        properties["artifact_types"] = list(artifact_types)
    else:
        properties["artifact_types"] = ["NO_ARTIFACT"]

    env_dict = ProtoMessageToDict(bento_service_metadata.env)
    if 'conda_env' in env_dict:
        env_dict['conda_env'] = YAML().load(env_dict['conda_env'])
    properties['env'] = env_dict

    return properties
Beispiel #11
0
    def __init__(
        self,
        default_config_file: str = None,
        override_config_file: str = None,
        validate_schema: bool = True,
    ):
        # Default configuraiton
        if default_config_file is None:
            default_config_file = os.path.join(
                os.path.dirname(__file__), "default_bentoml.yml"
            )

        with open(default_config_file, "rb") as f:
            self.config = YAML().load(f.read())

        if validate_schema:
            try:
                SCHEMA.validate(self.config)
            except SchemaError as e:
                raise BentoMLConfigException(
                    "Default configuration 'default_bentoml.yml' does not"
                    " conform to the required schema."
                ) from e

        # User override configuration
        if override_config_file is not None:
            LOGGER.info("Applying user config override from %s" % override_config_file)
            if not os.path.exists(override_config_file):
                raise BentoMLConfigException(
                    f"Config file {override_config_file} not found"
                )

            with open(override_config_file, "rb") as f:
                override_config = YAML().load(f.read())
            always_merger.merge(self.config, override_config)

            if validate_schema:
                try:
                    SCHEMA.validate(self.config)
                except SchemaError as e:
                    raise BentoMLConfigException(
                        "Configuration after user override does not conform to"
                        " the required schema."
                    ) from e
Beispiel #12
0
    def __init__(
        self,
        name: str,
        python_version: str,
        conda_channels: List[str] = None,
        conda_dependencies: List[str] = None,
    ):
        self._yaml = YAML()
        self._yaml.default_flow_style = False

        self._conda_env = self._yaml.load(
            CONDA_ENV_BASE_YAML.format(name=name,
                                       python_version=python_version))

        if conda_channels:
            self.add_channels(conda_channels)

        if conda_dependencies:
            self.add_conda_dependencies(conda_dependencies)
Beispiel #13
0
def test_conda_channels_n_dependencies(tmpdir):
    @bentoml.env(conda_channels=["bentoml-test-channel"],
                 conda_dependencies=["bentoml-test-lib"])
    class ServiceWithCondaDeps(bentoml.BentoService):
        @bentoml.api(input=DataframeInput(), batch=True)
        def predict(self, df):
            return df

    service_with_string = ServiceWithCondaDeps()
    service_with_string.save_to_dir(str(tmpdir))

    from pathlib import Path

    from bentoml.utils.ruamel_yaml import YAML

    yaml = YAML()
    env_yml = yaml.load(Path(os.path.join(tmpdir, 'environment.yml')))
    assert 'bentoml-test-channel' in env_yml['channels']

    assert 'bentoml-test-lib' in env_yml['dependencies']
Beispiel #14
0
            def wrapped_load(*args, **kwargs):
                if self.packed:
                    logger.warning(
                        "`load` on a 'packed' artifact may lead to unexpected behaviors"
                    )
                if self.loaded:
                    logger.warning(
                        "`load` an artifact multiple times may lead to unexpected "
                        "behaviors")

                # load metadata if exists
                path = args[0]  # load(self, path)
                meta_path = self._metadata_path(path)
                if os.path.isfile(meta_path):
                    with open(meta_path) as file:
                        yaml = YAML()
                        yaml_content = file.read()
                        self._metadata = yaml.load(yaml_content)

                ret = original(*args, **kwargs)
                # do not set self._loaded if `load` has failed with an exception raised
                self._loaded = True
                return ret
Beispiel #15
0
    def __init__(
        self,
        name: str = None,
        channels: List[str] = None,
        dependencies: List[str] = None,
        default_env_yaml_file: str = None,
        override_channels: bool = False,
    ):
        self._yaml = YAML()
        self._yaml.default_flow_style = False
        self.default_env_yaml_file = default_env_yaml_file

        if name:
            self.set_name(name)

        if override_channels and channels is None:
            raise BentoMLException(
                "No `conda_channels` provided while override_channels=True")

        if channels:
            self.set_channels(channels, override_channels)

        if dependencies:
            self.add_conda_dependencies(dependencies)
Beispiel #16
0
class SavedBundleConfig(object):
    def __init__(self, bento_service=None, kind="BentoService"):
        self.kind = kind
        self._yaml = YAML()
        self._yaml.default_flow_style = False
        self.config = self._yaml.load(
            BENTOML_CONFIG_YAML_TEMPLATE.format(
                kind=self.kind,
                bentoml_version=get_bentoml_deploy_version(),
                created_at=str(datetime.utcnow()),
            ))

        if bento_service is not None:
            self.config["metadata"].update({
                "service_name":
                bento_service.name,
                "service_version":
                bento_service.version,
            })
            self.config["env"] = bento_service.env.to_dict()
            self.config['apis'] = _get_apis_list(bento_service)
            self.config['artifacts'] = _get_artifacts_list(bento_service)

    def write_to_path(self, path, filename="bentoml.yml"):
        return self._yaml.dump(self.config, Path(os.path.join(path, filename)))

    @classmethod
    def load(cls, filepath):
        conf = cls()
        with open(filepath, "rb") as config_file:
            yml_content = config_file.read()
        conf.config = conf._yaml.load(yml_content)
        ver = str(conf["version"])
        py_ver = conf.config["env"]["python_version"]

        if ver != BENTOML_VERSION:
            msg = (
                "Saved BentoService bundle version mismatch: loading BentoService "
                "bundle create with BentoML version {}, but loading from BentoML "
                "version {}".format(conf["version"], BENTOML_VERSION))

            # If major version is different, then there could be incompatible API
            # changes. Raise error in this case.
            if ver.split(".")[0] != BENTOML_VERSION.split(".")[0]:
                if not BENTOML_VERSION.startswith('0+untagged'):
                    raise BentoMLConfigException(msg)
                else:
                    logger.warning(msg)
            else:  # Otherwise just show a warning.
                logger.warning(msg)

        if py_ver != PYTHON_VERSION:
            logger.warning(
                f"Saved BentoService Python version mismatch: loading "
                f"BentoService bundle created with Python version {py_ver}, "
                f"but current environment version is {PYTHON_VERSION}.")

        return conf

    def get_bento_service_metadata_pb(self):
        from bentoml.yatai.proto.repository_pb2 import BentoServiceMetadata

        bento_service_metadata = BentoServiceMetadata()
        bento_service_metadata.name = self.config["metadata"]["service_name"]
        bento_service_metadata.version = self.config["metadata"][
            "service_version"]
        bento_service_metadata.created_at.FromDatetime(
            self.config["metadata"]["created_at"])

        if "env" in self.config:
            if "setup_sh" in self.config["env"]:
                bento_service_metadata.env.setup_sh = self.config["env"][
                    "setup_sh"]

            if "conda_env" in self.config["env"]:
                bento_service_metadata.env.conda_env = dump_to_yaml_str(
                    self.config["env"]["conda_env"])

            if "pip_packages" in self.config["env"]:
                for pip_package in self.config["env"]["pip_packages"]:
                    bento_service_metadata.env.pip_packages.append(pip_package)
            if "python_version" in self.config["env"]:
                bento_service_metadata.env.python_version = self.config["env"][
                    "python_version"]
            if "docker_base_image" in self.config["env"]:
                bento_service_metadata.env.docker_base_image = self.config[
                    "env"]["docker_base_image"]

        if "apis" in self.config:
            for api_config in self.config["apis"]:
                if 'handler_type' in api_config:
                    # Convert handler type to input type for saved bundle created
                    # before version 0.8.0
                    input_type = api_config.get('handler_type')
                elif 'input_type' in api_config:
                    input_type = api_config.get('input_type')
                else:
                    input_type = "unknown"

                if 'output_type' in api_config:
                    output_type = api_config.get('output_type')
                else:
                    output_type = "DefaultOutput"

                api_metadata = BentoServiceMetadata.BentoServiceApi(
                    name=api_config["name"],
                    docs=api_config["docs"],
                    input_type=input_type,
                    output_type=output_type,
                )
                if "handler_config" in api_config:
                    # Supports viewing API input config info for saved bundle created
                    # before version 0.8.0
                    for k, v in api_config["handler_config"].items():
                        if k in {'mb_max_latency', 'mb_max_batch_size'}:
                            setattr(api_metadata, k, v)
                        else:
                            api_metadata.input_config[k] = v
                else:
                    if 'mb_max_latency' in api_config:
                        api_metadata.mb_max_latency = api_config[
                            "mb_max_latency"]
                    else:
                        api_metadata.mb_max_latency = DEFAULT_MAX_LATENCY

                    if 'mb_max_batch_size' in api_config:
                        api_metadata.mb_max_batch_size = api_config[
                            "mb_max_batch_size"]
                    else:
                        api_metadata.mb_max_batch_size = DEFAULT_MAX_BATCH_SIZE

                if "input_config" in api_config:
                    for k, v in api_config["input_config"].items():
                        api_metadata.input_config[k] = v

                if "output_config" in api_config:
                    for k, v in api_config["output_config"].items():
                        api_metadata.output_config[k] = v
                api_metadata.batch = api_config.get("batch", False)
                bento_service_metadata.apis.extend([api_metadata])

        if "artifacts" in self.config:
            for artifact_config in self.config["artifacts"]:
                artifact_metadata = BentoServiceMetadata.BentoArtifact()
                if "name" in artifact_config:
                    artifact_metadata.name = artifact_config["name"]
                if "artifact_type" in artifact_config:
                    artifact_metadata.artifact_type = artifact_config[
                        "artifact_type"]
                if "metadata" in artifact_config:
                    if isinstance(artifact_config["metadata"], dict):
                        s = Struct()
                        s.update(artifact_config["metadata"])
                        artifact_metadata.metadata.CopyFrom(s)
                    else:
                        logger.warning(
                            "Tried to get non-dictionary metadata for artifact "
                            f"{artifact_metadata.name}. Ignoring metadata...")
                bento_service_metadata.artifacts.extend([artifact_metadata])

        return bento_service_metadata

    def __getitem__(self, item):
        return self.config[item]

    def __setitem__(self, key, value):
        self.config[key] = value

    def __contains__(self, item):
        return item in self.config
Beispiel #17
0
    def __init__(
        self,
        default_config_file: str = None,
        override_config_file: str = None,
        validate_schema: bool = True,
        legacy_compatibility: bool = True,
    ):
        # Default configuraiton
        if default_config_file is None:
            default_config_file = os.path.join(os.path.dirname(__file__),
                                               "default_bentoml.yml")

        with open(default_config_file, "rb") as f:
            self.config = YAML().load(f.read())

        if validate_schema:
            try:
                SCHEMA.validate(self.config)
            except SchemaError as e:
                raise BentoMLConfigException(
                    "Default configuration 'default_bentoml.yml' does not"
                    " conform to the required schema.") from e

        # Legacy configuration compatibility
        if legacy_compatibility:
            try:
                self.config["api_server"]["port"] = config("apiserver").getint(
                    "default_port")
                self.config["api_server"]["max_request_size"] = config(
                    "apiserver").getint("default_max_request_size")
                self.config["marshal_server"]["max_batch_size"] = config(
                    "marshal_server").getint("default_max_batch_size")
                self.config["marshal_server"]["max_latency"] = config(
                    "marshal_server").getint("default_max_latency")
                self.config["marshal_server"]["request_header_flag"] = config(
                    "marshal_server").get("marshal_request_header_flag")
                self.config["yatai"]["url"] = config("yatai_service").get(
                    "url")
                self.config["tracing"]["zipkin_api_url"] = config(
                    "tracing").get("zipkin_api_url")
                self.config["instrument"]["namespace"] = config(
                    "instrument").get("default_namespace")
            except KeyError as e:
                raise BentoMLConfigException(
                    "Overriding a non-existent configuration key in compatibility mode."
                ) from e

            if validate_schema:
                try:
                    SCHEMA.validate(self.config)
                except SchemaError as e:
                    raise BentoMLConfigException(
                        "Configuration after applying legacy compatibility"
                        " does not conform to the required schema.") from e

        # User override configuration
        if override_config_file is not None and os.path.exists(
                override_config_file):
            LOGGER.info("Applying user config override from %s" %
                        override_config_file)
            with open(override_config_file, "rb") as f:
                override_config = YAML().load(f.read())
            always_merger.merge(self.config, override_config)

            if validate_schema:
                try:
                    SCHEMA.validate(self.config)
                except SchemaError as e:
                    raise BentoMLConfigException(
                        "Configuration after user override does not conform to"
                        " the required schema.") from e
def deployment_yaml_string_to_pb(deployment_yaml_string):
    yaml = YAML()
    deployment_yaml = yaml.load(deployment_yaml_string)
    return deployment_dict_to_pb(deployment_yaml)
Beispiel #19
0
    def __init__(
        self,
        default_config_file: str = None,
        override_config_file: str = None,
        validate_schema: bool = True,
        legacy_compatibility: bool = True,
    ):
        # Default configuraiton
        if default_config_file is None:
            default_config_file = os.path.join(os.path.dirname(__file__),
                                               "default_bentoml.yml")

        with open(default_config_file, "rb") as f:
            self.config = YAML().load(f.read())

        if validate_schema:
            try:
                SCHEMA.validate(self.config)
            except SchemaError as e:
                raise BentoMLConfigException(
                    "Default configuration 'default_bentoml.yml' does not"
                    " conform to the required schema.") from e

        # Legacy configuration compatibility
        if legacy_compatibility:
            try:
                self.config["bento_server"]["port"] = config(
                    "apiserver").getint("default_port")
                self.config["bento_server"]["workers"] = config(
                    "apiserver").getint("default_gunicorn_workers_count")
                self.config["bento_server"]["max_request_size"] = config(
                    "apiserver").getint("default_max_request_size")

                if "default_max_batch_size" in config("marshal_server"):
                    self.config["bento_server"]["microbatch"][
                        "max_batch_size"] = config("marshal_server").getint(
                            "default_max_batch_size")

                if "default_max_latency" in config("marshal_server"):
                    self.config["bento_server"]["microbatch"][
                        "max_latency"] = config("marshal_server").getint(
                            "default_max_latency")

                self.config["bento_server"]["metrics"]["namespace"] = config(
                    "instrument").get("default_namespace")

                self.config["adapters"]["image_input"][
                    "default_extensions"] = [
                        extension.strip()
                        for extension in config("apiserver").get(
                            "default_image_input_accept_file_extensions").
                        split(",")
                    ]
            except KeyError as e:
                raise BentoMLConfigException(
                    "Overriding a non-existent configuration key in compatibility mode."
                ) from e

            if validate_schema:
                try:
                    SCHEMA.validate(self.config)
                except SchemaError as e:
                    raise BentoMLConfigException(
                        "Configuration after applying legacy compatibility"
                        " does not conform to the required schema.") from e

        # User override configuration
        if override_config_file is not None:
            LOGGER.info("Applying user config override from %s" %
                        override_config_file)
            if not os.path.exists(override_config_file):
                raise BentoMLConfigException(
                    f"Config file {override_config_file} not found")

            with open(override_config_file, "rb") as f:
                override_config = YAML().load(f.read())
            always_merger.merge(self.config, override_config)

            if validate_schema:
                try:
                    SCHEMA.validate(self.config)
                except SchemaError as e:
                    raise BentoMLConfigException(
                        "Configuration after user override does not conform to"
                        " the required schema.") from e
Beispiel #20
0
def _create_aws_lambda_cloudformation_template_file(
    project_dir,
    namespace,
    deployment_name,
    deployment_path_prefix,
    api_names,
    bento_service_name,
    s3_bucket_name,
    py_runtime,
    memory_size,
    timeout,
):
    template_file_path = os.path.join(project_dir, 'template.yaml')
    yaml = YAML()
    sam_config = {
        'AWSTemplateFormatVersion': '2010-09-09',
        'Transform': 'AWS::Serverless-2016-10-31',
        'Globals': {
            'Function': {
                'Timeout': timeout,
                'Runtime': py_runtime
            },
            'Api': {
                'BinaryMediaTypes': ['image~1*'],
                'Cors': "'*'",
                'Auth': {
                    'ApiKeyRequired': False,
                    'DefaultAuthorizer': 'NONE',
                    'AddDefaultAuthorizerToCorsPreflight': False,
                },
            },
        },
        'Resources': {},
        'Outputs': {
            'S3Bucket': {
                'Value': s3_bucket_name,
                'Description':
                'S3 Bucket for saving artifacts and lambda bundle',
            }
        },
    }
    for api_name in api_names:
        sam_config['Resources'][api_name] = {
            'Type': 'AWS::Serverless::Function',
            'Properties': {
                'Runtime': py_runtime,
                'CodeUri': deployment_name + '/',
                'Handler': 'app.{}'.format(api_name),
                'FunctionName': f'{namespace}-{deployment_name}-{api_name}',
                'Timeout': timeout,
                'MemorySize': memory_size,
                'Events': {
                    'Api': {
                        'Type': 'Api',
                        'Properties': {
                            'Path': '/{}'.format(api_name),
                            'Method': 'post',
                        },
                    }
                },
                'Policies': [{
                    'S3ReadPolicy': {
                        'BucketName': s3_bucket_name
                    }
                }],
                'Environment': {
                    'Variables': {
                        'BENTOML_BENTO_SERVICE_NAME': bento_service_name,
                        'BENTOML_API_NAME': api_name,
                        'BENTOML_S3_BUCKET': s3_bucket_name,
                        'BENTOML_DEPLOYMENT_PATH_PREFIX':
                        deployment_path_prefix,
                    }
                },
            },
        }

    yaml.dump(sam_config, Path(template_file_path))

    # We add Outputs section separately, because the value should not
    # have "'" around !Sub
    with open(template_file_path, 'a') as f:
        f.write("""\
  EndpointUrl:
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.\
amazonaws.com/Prod"
    Description: URL for endpoint
""")
    return template_file_path