Skip to content
Permalink
Browse files

Enhance serverless deployment (#321)

* check docker available or not
* make sure check docker exist or not, before lambda deployment
* remove bentoml entry, if bundled one exists.
* if Bentoml is not in bundled direct, will not remove bentoml entry
  • Loading branch information...
yubozhao committed Oct 2, 2019
1 parent 5a7d2a4 commit 626d83574baebbf6999938afd65695172dfd152b
@@ -115,6 +115,7 @@ package.json
node_modules/
yarn-error.log
yarn.lock
package-lock.json

# docs
built-docs
@@ -39,6 +39,7 @@
TemporaryServerlessContent,
TemporaryServerlessConfig,
parse_serverless_info_response_to_json_string,
ensure_docker_available_or_raise,
)
from bentoml.archive.loader import load_bentoml_config

@@ -131,6 +132,7 @@ def generate_handler_py(bento_name, apis, output_path):

class AwsLambdaDeploymentOperator(DeploymentOperatorBase):
def apply(self, deployment_pb, repo, prev_deployment=None):
ensure_docker_available_or_raise()
deployment_spec = deployment_pb.spec
aws_config = deployment_spec.aws_lambda_operator_config

@@ -166,7 +168,7 @@ def apply(self, deployment_pb, repo, prev_deployment=None):
install_serverless_plugin("serverless-python-requirements", output_path)
install_serverless_plugin("serverless-apigw-binary", output_path)
logger.info('Deploying to AWS Lambda')
call_serverless_command(["serverless", "deploy"], output_path)
call_serverless_command(["deploy"], output_path)

res_deployment_pb = Deployment(state=DeploymentState())
res_deployment_pb.CopyFrom(deployment_pb)
@@ -200,7 +202,7 @@ def delete(self, deployment_pb, repo=None):
provider_name='aws',
functions=generate_aws_handler_functions_config(bento_config['apis']),
) as tempdir:
response = call_serverless_command(['serverless', 'remove'], tempdir)
response = call_serverless_command(['remove'], tempdir)
stack_name = '{name}-{namespace}'.format(
name=deployment_pb.name, namespace=deployment_pb.namespace
)
@@ -228,7 +230,7 @@ def describe(self, deployment_pb, repo=None):
functions=generate_aws_handler_functions_config(bento_config['apis']),
) as tempdir:
try:
response = call_serverless_command(["serverless", "info"], tempdir)
response = call_serverless_command(["info"], tempdir)
info_json = parse_serverless_info_response_to_json_string(response)
state = DeploymentState(
state=DeploymentState.RUNNING, info_json=info_json
@@ -122,7 +122,7 @@ def apply(self, deployment_pb, repo, prev_deployment=None):
region=gcp_config.region,
stage=deployment_pb.namespace,
)
call_serverless_command(["serverless", "deploy"], output_path)
call_serverless_command(["deploy"], output_path)

res_deployment_pb = Deployment(state=DeploymentState())
res_deployment_pb.CopyFrom(deployment_pb)
@@ -146,7 +146,7 @@ def describe(self, deployment_pb, repo=None):
functions=generate_gcp_handler_functions_config(bento_config['apis']),
) as tempdir:
try:
response = call_serverless_command(["serverless", "info"], tempdir)
response = call_serverless_command(["info"], tempdir)
info_json = parse_serverless_info_response_to_json_string(response)
state = DeploymentState(
state=DeploymentState.RUNNING, info_json=info_json
@@ -184,7 +184,7 @@ def delete(self, deployment_pb, repo=None):
functions=generate_gcp_handler_functions_config(bento_config['apis']),
) as tempdir:
try:
response = call_serverless_command(['serverless', 'remove'], tempdir)
response = call_serverless_command(['remove'], tempdir)
if "Serverless: Stack removal finished..." in response:
status = Status.OK()
else:
@@ -22,49 +22,60 @@
import subprocess
import json
from subprocess import PIPE
from packaging import version

from ruamel.yaml import YAML
from packaging import version

from bentoml.configuration import _get_bentoml_home
from bentoml.utils import Path
from bentoml.utils.tempdir import TempDirectory
from bentoml.utils.whichcraft import which
from bentoml.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException, BentoMLMissingDepdencyException

logger = logging.getLogger(__name__)

MINIMUM_SERVERLESS_VERSION = '1.40.0'

SERVERLESS_VERSION = '1.53.0'
BENTOML_HOME = _get_bentoml_home()

def check_serverless_compatiable_version():
if which("serverless") is None:
raise ValueError(
"Serverless framework is not installed, please visit "
+ "www.serverless.com for install instructions."
)
# We will install serverless package and use the installed one, instead
# of user's installation
SERVERLESS_BIN_COMMAND = '{}/node_modules/.bin/serverless'.format(BENTOML_HOME)

version_result = (
subprocess.check_output(["serverless", "-v"]).decode("utf-8").strip()
)
if "(Enterprise Plugin:" in version_result:
slice_end_index = version_result.find(" (Enterprise")
version_result = version_result[0:slice_end_index]

def check_nodejs_comptaible_version():
if which('npm') is None:
raise BentoMLMissingDepdencyException(
'NPM is not installed. Please visit www.nodejs.org for instructions'
)
if which("node") is None:
raise BentoMLMissingDepdencyException(
"NodeJs is not installed, please visit www.nodejs.org for install "
"instructions."
)
version_result = subprocess.check_output(["node", "-v"]).decode("utf-8").strip()
parsed_version = version.parse(version_result)

if parsed_version >= version.parse(MINIMUM_SERVERLESS_VERSION):
return
else:
if not parsed_version >= version.parse('v8.10.0'):
raise ValueError(
"Incompatiable serverless version, please install version 1.40.0 or greater"
"Incompatible Nodejs version, please install version v8.10.0 " "or greater"
)


def install_serverless_package():
check_nodejs_comptaible_version()
install_command = ['npm', 'install', 'serverless@{}'.format(SERVERLESS_VERSION)]
subprocess.call(install_command, cwd=BENTOML_HOME)


def install_serverless_plugin(plugin_name, install_dir_path):
command = ["serverless", "plugin", "install", "-n", plugin_name]
command = ["plugin", "install", "-n", plugin_name]
call_serverless_command(command, install_dir_path)


def call_serverless_command(command, cwd_path):
command = [SERVERLESS_BIN_COMMAND] + command

with subprocess.Popen(command, cwd=cwd_path, stdout=PIPE, stderr=PIPE) as proc:
response = parse_serverless_response(proc.stdout.read().decode("utf-8"))
logger.debug("Serverless response: %s", "\n".join(response))
@@ -79,7 +90,7 @@ def parse_serverless_response(serverless_response):

# Parsing serverless response brutally. The current serverless
# response format is:
# ServerlessError|Error -----{fill dash to 56 line lenght}
# ServerlessError|Error -----{fill dash to 56 line length}
# empty space
# Error Message
# empty space
@@ -126,11 +137,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()

def generate(self):
install_serverless_package()
self.temp_directory.create()
tempdir = self.temp_directory.path
call_serverless_command(
[
"serverless",
"create",
"--template",
self.template_type,
@@ -149,18 +160,35 @@ def generate(self):
self.archive_path, 'bundled_pip_dependencies'
)
# If bundled_pip_dependencies directory exists, we copy over and update
# requirements.txt
# requirements.txt. We need to remove the bentoml entry in the file, because
# when pip install, it will NOT override the pypi released version.
if os.path.isdir(bundled_dependencies_path):
dest_bundle_path = os.path.join(tempdir, 'bundled_pip_dependencies')
shutil.copytree(bundled_dependencies_path, dest_bundle_path)
requirement_txt_path = os.path.join(tempdir, 'requirements.txt')

with open(requirement_txt_path, 'a+') as requirement_file:
bundled_files = os.listdir(dest_bundle_path)
for bundled_module_name in bundled_files:
requirement_file.write(
'\n./bundled_pip_dependencies/{}'.format(bundled_module_name)
)
bundled_files = os.listdir(dest_bundle_path)
has_bentoml_bundle = False
for index, bundled_file_name in enumerate(bundled_files):
bundled_files[index] = './bundled_pip_dependencies/{}\n'.format(
bundled_file_name
)
# If file name start with `BentoML-`, assuming it is a
# bentoml targz bundle
if bundled_file_name.startswith('BentoML-'):
has_bentoml_bundle = True

with open(
os.path.join(tempdir, 'requirements.txt'), 'r+'
) as requirement_file:
required_modules = requirement_file.readlines()
if has_bentoml_bundle:
# Assuming bentoml is always the first one in
# requirements.txt. We are removing it
required_modules = required_modules[1:]
required_modules = required_modules + bundled_files
# Write from beginning of the file, instead of appending to
# the end.
requirement_file.seek(0)
requirement_file.writelines(required_modules)

self.path = tempdir

@@ -199,6 +227,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()

def generate(self):
install_serverless_package()
serverless_config = {
"service": self.deployment_name,
"provider": {
@@ -227,3 +256,17 @@ def generate(self):
def cleanup(self):
self.temp_directory.cleanup()
self.path = None


def ensure_docker_available_or_raise():
try:
subprocess.check_call(['docker', 'info'])
except subprocess.CalledProcessError as error:
raise BentoMLException(
'Error executing docker command: {}'.format(error.output)
)
except FileNotFoundError:
raise BentoMLMissingDepdencyException(
'Docker is required for AWS Lambda deployment. Please visit '
'www.docker.come for instructions'
)
@@ -51,7 +51,7 @@ def process_docker_api_line(payload):
logger.info(line_payload['stream'])


def add_local_bentoml_package_to_repo(archive_path):
def _find_bentoml_module_location():
try:
module_location, = importlib.util.find_spec(
'bentoml'
@@ -61,6 +61,11 @@ def add_local_bentoml_package_to_repo(archive_path):
import imp

_, module_location, _ = imp.find_module('bentoml')
return module_location


def add_local_bentoml_package_to_repo(archive_path):
module_location = _find_bentoml_module_location()
bentoml_setup_py = os.path.abspath(os.path.join(module_location, '..', 'setup.py'))

assert os.path.isfile(bentoml_setup_py), '"setup.py" for Bentoml module not found'
@@ -36,3 +36,7 @@ class BentoMLDeploymentException(BentoMLException):

class BentoMLRepositoryException(BentoMLException):
pass


class BentoMLMissingDepdencyException(BentoMLException):
pass

0 comments on commit 626d835

Please sign in to comment.
You can’t perform that action at this time.