Example #1
0
def apply_deployment(deployment_info, yatai_service=None):
    if yatai_service is None:
        from bentoml.yatai import get_yatai_service

        yatai_service = get_yatai_service()

    try:
        if isinstance(deployment_info, dict):
            deployment_pb = deployment_dict_to_pb(deployment_info)
        elif isinstance(deployment_info, str):
            deployment_pb = deployment_yaml_string_to_pb(deployment_info)
        else:
            raise YataiDeploymentException(
                'Unexpected argument type, expect deployment info to be str in yaml '
                'format or a dict, instead got: {}'.format(
                    str(type(deployment_info))))

        validation_errors = validate_deployment_pb_schema(deployment_pb)
        if validation_errors:
            return ApplyDeploymentResponse(status=Status.INVALID_ARGUMENT(
                'Failed to validate deployment: {errors}'.format(
                    errors=validation_errors)))

        return yatai_service.ApplyDeployment(
            ApplyDeploymentRequest(deployment=deployment_pb))
    except BentoMLException as error:
        return ApplyDeploymentResponse(status=Status.INTERNAL(str(error)))
Example #2
0
    def ApplyDeployment(self, request, context=None):
        try:
            # apply default namespace if not set
            request.deployment.namespace = (
                request.deployment.namespace or self.default_namespace
            )

            validation_errors = validate_deployment_pb_schema(request.deployment)
            if validation_errors:
                return ApplyDeploymentResponse(
                    status=Status.INVALID_ARGUMENT(
                        'Failed to validate deployment. {errors}'.format(
                            errors=validation_errors
                        )
                    )
                )

            previous_deployment = self.deployment_store.get(
                request.deployment.name, request.deployment.namespace
            )
            if previous_deployment:
                # check deployment platform
                if (
                    previous_deployment.spec.operator
                    != request.deployment.spec.operator
                ):
                    return ApplyDeploymentResponse(
                        status=Status.ABORTED(
                            'Can not change the target deploy platform of existing '
                            'active deployment. Try delete existing deployment and '
                            'deploy to new target platform again'
                        )
                    )
                request.deployment.state.state = DeploymentState.PENDING
            else:
                request.deployment.created_at.GetCurrentTime()

            request.deployment.last_updated_at.GetCurrentTime()

            self.deployment_store.insert_or_update(request.deployment)
            # find deployment operator based on deployment spec
            operator = get_deployment_operator(request.deployment)

            # deploying to target platform
            response = operator.apply(request.deployment, self, previous_deployment)

            # update deployment state
            self.deployment_store.insert_or_update(response.deployment)

            return response

        except BentoMLException as e:
            logger.error("INTERNAL ERROR: %s", e)
            return ApplyDeploymentResponse(status=Status.INTERNAL(str(e)))
Example #3
0
    def apply(self, deployment_pb, yatai_service, prev_deployment=None):
        try:
            deployment_spec = deployment_pb.spec
            gcp_config = deployment_spec.gcp_function_operator_config
            bento_pb = yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                ))
            if bento_pb.bento.uri.type != BentoUri.LOCAL:
                raise BentoMLException(
                    'BentoML currently only support local repository')
            else:
                bento_path = bento_pb.bento.uri.uri
            bento_service_metadata = bento_pb.bento.bento_service_metadata

            api_names = ([gcp_config.api_name] if gcp_config.api_name else
                         [api.name for api in bento_service_metadata.apis])
            ensure_deploy_api_name_exists_in_bento(
                [api.name for api in bento_service_metadata.apis], api_names)
            with TempDirectory() as serverless_project_dir:
                init_serverless_project_dir(
                    serverless_project_dir,
                    bento_path,
                    deployment_pb.name,
                    deployment_spec.bento_name,
                    'google-python',
                )
                generate_gcp_function_main_py(deployment_spec.bento_name,
                                              api_names,
                                              serverless_project_dir)
                generate_gcp_function_serverless_config(
                    deployment_pb.name,
                    api_names,
                    serverless_project_dir,
                    gcp_config.region,
                    # BentoML namespace is mapping to serverless stage.
                    stage=deployment_pb.namespace,
                )
                call_serverless_command(["deploy"], serverless_project_dir)

            res_deployment_pb = Deployment(state=DeploymentState())
            res_deployment_pb.CopyFrom(deployment_pb)
            state = self.describe(res_deployment_pb, yatai_service).state
            res_deployment_pb.state.CopyFrom(state)

            return ApplyDeploymentResponse(status=Status.OK(),
                                           deployment=res_deployment_pb)
        except BentoMLException as error:
            return ApplyDeploymentResponse(
                status=exception_to_return_status(error))
Example #4
0
    def apply(self, deployment_pb, repo, prev_deployment=None):
        deployment_spec = deployment_pb.spec
        gcp_config = deployment_spec.gcp_function_operator_config
        bento_path = repo.get(deployment_spec.bento_name,
                              deployment_spec.bento_version)

        bento_config = load_bentoml_config(bento_path)
        with TemporaryServerlessContent(
                archive_path=bento_path,
                deployment_name=deployment_pb.name,
                bento_name=deployment_spec.bento_name,
                template_type='google-python',
        ) as output_path:
            generate_main_py(bento_config['name'], bento_config['apis'],
                             output_path)
            generate_serverless_configuration_for_gcp_function(
                service_name=bento_config['name'],
                apis=bento_config['apis'],
                output_path=output_path,
                region=gcp_config.region,
                stage=deployment_pb.namespace,
            )
            call_serverless_command(["deploy"], output_path)

        res_deployment_pb = Deployment(state=DeploymentState())
        res_deployment_pb.CopyFrom(deployment_pb)
        state = self.describe(res_deployment_pb, repo).state
        res_deployment_pb.state.CopyFrom(state)

        return ApplyDeploymentResponse(status=Status.OK(),
                                       deployment=res_deployment_pb)
Example #5
0
    def apply(self, deployment_pb, yatai_service, prev_deployment=None):
        try:
            ensure_docker_available_or_raise()
            deployment_spec = deployment_pb.spec
            sagemaker_config = deployment_spec.sagemaker_operator_config
            if sagemaker_config is None:
                raise BentoMLDeploymentException('Sagemaker configuration is missing.')

            bento_pb = yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                )
            )
            if bento_pb.bento.uri.type not in (BentoUri.LOCAL, BentoUri.S3):
                raise BentoMLException(
                    'BentoML currently not support {} repository'.format(
                        bento_pb.bento.uri.type
                    )
                )

            return self._apply(
                deployment_pb,
                bento_pb,
                yatai_service,
                bento_pb.bento.uri.uri,
                prev_deployment,
            )

        except BentoMLException as error:
            return ApplyDeploymentResponse(status=exception_to_return_status(error))
Example #6
0
    def update(self, deployment_pb, previous_deployment):
        try:
            ensure_sam_available_or_raise()
            ensure_docker_available_or_raise()

            deployment_spec = deployment_pb.spec
            bento_pb = self.yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                )
            )
            if bento_pb.bento.uri.type not in (BentoUri.LOCAL, BentoUri.S3):
                raise BentoMLException(
                    'BentoML currently not support {} repository'.format(
                        BentoUri.StorageType.Name(bento_pb.bento.uri.type)
                    )
                )

            return self._update(
                deployment_pb, previous_deployment, bento_pb, bento_pb.bento.uri.uri
            )
        except BentoMLException as error:
            deployment_pb.state.state = DeploymentState.ERROR
            deployment_pb.state.error_message = f'Error: {str(error)}'
            return ApplyDeploymentResponse(
                status=error.status_code, deployment_pb=deployment_pb
            )
Example #7
0
    def _add(self, deployment_pb, bento_pb, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._add(deployment_pb, bento_pb, local_path)

        deployment_spec = deployment_pb.spec
        lambda_deployment_config = deployment_spec.aws_lambda_operator_config
        bento_service_metadata = bento_pb.bento.bento_service_metadata
        lambda_s3_bucket = generate_aws_compatible_string(
            'btml-{namespace}-{name}-{random_string}'.format(
                namespace=deployment_pb.namespace,
                name=deployment_pb.name,
                random_string=uuid.uuid4().hex[:6].lower(),
            )
        )
        try:
            create_s3_bucket_if_not_exists(
                lambda_s3_bucket, lambda_deployment_config.region
            )
            _deploy_lambda_function(
                deployment_pb=deployment_pb,
                bento_service_metadata=bento_service_metadata,
                deployment_spec=deployment_spec,
                lambda_s3_bucket=lambda_s3_bucket,
                lambda_deployment_config=lambda_deployment_config,
                bento_path=bento_path,
            )
            return ApplyDeploymentResponse(status=Status.OK(), deployment=deployment_pb)
        except BentoMLException as error:
            if lambda_s3_bucket and lambda_deployment_config:
                _cleanup_s3_bucket_if_exist(
                    lambda_s3_bucket, lambda_deployment_config.region
                )
            raise error
Example #8
0
    def ApplyDeployment(self, request, context=None):
        try:
            # apply default namespace if not set
            request.deployment.namespace = (request.deployment.namespace
                                            or self.default_namespace)

            # TODO: validate deployment config

            # create or update deployment spec record
            self.deployment_store.insert_or_update(request.deployment)

            # find deployment operator based on deployment spec
            operator = get_deployment_operator(request.deployment)

            # deploying to target platform
            response = operator.apply(request.deployment, self.repo)

            # update deployment state
            self.deployment_store.insert_or_update(response.deployment)

            return response

        except BentoMLException as e:
            logger.error("INTERNAL ERROR: %s", e)
            return ApplyDeploymentResponse(Status.INTERNAL(e))
Example #9
0
    def add(self, deployment_pb):
        try:
            deployment_spec = deployment_pb.spec
            deployment_spec.aws_lambda_operator_config.region = (
                deployment_spec.aws_lambda_operator_config.region
                or get_default_aws_region())
            if not deployment_spec.aws_lambda_operator_config.region:
                raise InvalidArgument('AWS region is missing')

            ensure_sam_available_or_raise()
            ensure_docker_available_or_raise()
            bento_pb = self.yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                ))
            if bento_pb.bento.uri.type not in (BentoUri.LOCAL, BentoUri.S3):
                raise BentoMLException(
                    'BentoML currently not support {} repository'.format(
                        BentoUri.StorageType.Name(bento_pb.bento.uri.type)))

            return self._add(deployment_pb, bento_pb, bento_pb.bento.uri.uri)
        except BentoMLException as error:
            deployment_pb.state.state = DeploymentState.ERROR
            deployment_pb.state.error_message = f'Error: {str(error)}'
            return ApplyDeploymentResponse(status=error.status_proto,
                                           deployment=deployment_pb)
Example #10
0
    def add(self, deployment_pb):
        try:
            ensure_docker_available_or_raise()
            deployment_spec = deployment_pb.spec
            sagemaker_config = deployment_spec.sagemaker_operator_config
            if sagemaker_config is None:
                raise YataiDeploymentException('Sagemaker configuration is missing.')

            bento_pb = self.yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                )
            )
            if bento_pb.bento.uri.type not in (BentoUri.LOCAL, BentoUri.S3):
                raise BentoMLException(
                    'BentoML currently not support {} repository'.format(
                        BentoUri.StorageType.Name(bento_pb.bento.uri.type)
                    )
                )

            return self._add(deployment_pb, bento_pb, bento_pb.bento.uri.uri)

        except BentoMLException as error:
            deployment_pb.state.state = DeploymentState.ERROR
            deployment_pb.state.error_message = (
                f'Error creating SageMaker deployment: {str(error)}'
            )
            return ApplyDeploymentResponse(
                status=error.status_proto, deployment=deployment_pb
            )
Example #11
0
def create_yatai_service_mock():
    yatai_service_mock = Mock()
    yatai_service_mock.ApplyDeployment.return_value = ApplyDeploymentResponse()
    yatai_service_mock.DeleteDeployment.return_value = DeleteDeploymentResponse()
    yatai_service_mock.DescribeDeployment.return_value = DescribeDeploymentResponse()
    yatai_service_mock.GetDeployment.return_value = GetDeploymentResponse(
        status=Status.NOT_FOUND()
    )
    yatai_service_mock.ListDeployments.return_value = ListDeploymentsResponse()
    return yatai_service_mock
Example #12
0
    def _apply(self, deployment_pb, bento_pb, yatai_service, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._apply(deployment_pb, bento_pb, yatai_service,
                                   local_path)

        deployment_spec = deployment_pb.spec
        aws_config = deployment_spec.aws_lambda_operator_config

        bento_service_metadata = bento_pb.bento.bento_service_metadata

        template = 'aws-python3'
        if version.parse(bento_service_metadata.env.python_version
                         ) < version.parse('3.0.0'):
            template = 'aws-python'

        api_names = ([aws_config.api_name] if aws_config.api_name else
                     [api.name for api in bento_service_metadata.apis])
        ensure_deploy_api_name_exists_in_bento(
            [api.name for api in bento_service_metadata.apis], api_names)

        with TempDirectory() as serverless_project_dir:
            init_serverless_project_dir(
                serverless_project_dir,
                bento_path,
                deployment_pb.name,
                deployment_spec.bento_name,
                template,
            )
            generate_aws_lambda_handler_py(deployment_spec.bento_name,
                                           api_names, serverless_project_dir)
            generate_aws_lambda_serverless_config(
                bento_service_metadata.env.python_version,
                deployment_pb.name,
                api_names,
                serverless_project_dir,
                aws_config.region,
                # BentoML deployment namespace is mapping to serverless `stage`
                # concept
                stage=deployment_pb.namespace,
            )
            logger.info(
                'Installing additional packages: serverless-python-requirements'
            )
            install_serverless_plugin("serverless-python-requirements",
                                      serverless_project_dir)
            logger.info('Deploying to AWS Lambda')
            call_serverless_command(["deploy"], serverless_project_dir)

        res_deployment_pb = Deployment(state=DeploymentState())
        res_deployment_pb.CopyFrom(deployment_pb)
        state = self.describe(res_deployment_pb, yatai_service).state
        res_deployment_pb.state.CopyFrom(state)
        return ApplyDeploymentResponse(status=Status.OK(),
                                       deployment=res_deployment_pb)
Example #13
0
    def _add(self, deployment_pb, bento_pb, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._add(deployment_pb, bento_pb, local_path)

        deployment_spec = deployment_pb.spec
        sagemaker_config = deployment_spec.sagemaker_operator_config

        raise_if_api_names_not_found_in_bento_service_metadata(
            bento_pb.bento.bento_service_metadata, [sagemaker_config.api_name])

        sagemaker_client = boto3.client('sagemaker', sagemaker_config.region)

        with TempDirectory() as temp_dir:
            sagemaker_project_dir = os.path.join(temp_dir,
                                                 deployment_spec.bento_name)
            _init_sagemaker_project(
                sagemaker_project_dir,
                bento_path,
                bento_pb.bento.bento_service_metadata.env.docker_base_image,
            )
            ecr_image_path = create_and_push_docker_image_to_ecr(
                sagemaker_config.region,
                deployment_spec.bento_name,
                deployment_spec.bento_version,
                sagemaker_project_dir,
            )

        try:
            (
                sagemaker_model_name,
                sagemaker_endpoint_config_name,
                sagemaker_endpoint_name,
            ) = _get_sagemaker_resource_names(deployment_pb)

            _create_sagemaker_model(sagemaker_client, sagemaker_model_name,
                                    ecr_image_path, sagemaker_config)
            _create_sagemaker_endpoint_config(
                sagemaker_client,
                sagemaker_model_name,
                sagemaker_endpoint_config_name,
                sagemaker_config,
            )
            _create_sagemaker_endpoint(
                sagemaker_client,
                sagemaker_endpoint_name,
                sagemaker_endpoint_config_name,
            )
        except AWSServiceError as e:
            delete_sagemaker_deployment_resources_if_exist(deployment_pb)
            raise e

        return ApplyDeploymentResponse(status=Status.OK(),
                                       deployment=deployment_pb)
Example #14
0
    def ApplyDeployment(self, request, context):
        try:
            deployment_pb = request.deployment
            operator = get_deployment_operator(deployment_pb)
            return operator.apply(request)

        except BentoMLException:
            response = ApplyDeploymentResponse()
            # response.status = ...
            # LOG.error(....)
            return response
Example #15
0
    def apply(self, deployment_pb, repo):
        # deploy code.....
        spec = deployment_pb.spec
        bento_path = repo.get(spec.bento_name, spec.bento_version)

        # config = load_bentoml_config(bento_path)...

        res_deployment_pb = Deployment()
        res_deployment_pb.CopyFrom(deployment_pb)
        # res_deployment_pb.state = ...
        return ApplyDeploymentResponse(status=Status.OK(), deployment=res_deployment_pb)
Example #16
0
    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

        bento_path = repo.get(deployment_spec.bento_name,
                              deployment_spec.bento_version)
        bento_config = load_bentoml_config(bento_path)

        template = 'aws-python3'
        minimum_python_version = version.parse('3.0.0')
        bento_python_version = version.parse(
            bento_config['env']['python_version'])
        if bento_python_version < minimum_python_version:
            template = 'aws-python'

        with TemporaryServerlessContent(
                archive_path=bento_path,
                deployment_name=deployment_pb.name,
                bento_name=deployment_spec.bento_name,
                template_type=template,
        ) as output_path:
            generate_handler_py(deployment_spec.bento_name,
                                bento_config['apis'], output_path)
            generate_serverless_configuration_for_aws_lambda(
                service_name=deployment_pb.name,
                apis=bento_config['apis'],
                output_path=output_path,
                region=aws_config.region,
                stage=deployment_pb.namespace,
            )
            logger.info(
                'Installing additional packages: serverless-python-requirements, '
                'serverless-apigw-binary')
            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(["deploy"], output_path)

        res_deployment_pb = Deployment(state=DeploymentState())
        res_deployment_pb.CopyFrom(deployment_pb)
        state = self.describe(res_deployment_pb, repo).state
        res_deployment_pb.state.CopyFrom(state)
        return ApplyDeploymentResponse(status=Status.OK(),
                                       deployment=res_deployment_pb)
Example #17
0
    def apply_deployment(self, deployment_info):
        if isinstance(deployment_info, dict):
            deployment_pb = deployment_dict_to_pb(deployment_info)
        elif isinstance(deployment_info, str):
            deployment_pb = deployment_yaml_string_to_pb(deployment_info)
        elif isinstance(deployment_info, Deployment):
            deployment_pb = deployment_info
        else:
            raise YataiDeploymentException(
                'Unexpected argument type, expect deployment info to be str in yaml '
                'format or a dict or a deployment protobuf obj, instead got: {}'
                .format(str(type(deployment_info))))

        validation_errors = validate_deployment_pb_schema(deployment_pb)
        if validation_errors:
            return ApplyDeploymentResponse(status=Status.INVALID_ARGUMENT(
                'Failed to validate deployment: {errors}'.format(
                    errors=validation_errors)))

        return self.yatai_service.ApplyDeployment(
            ApplyDeploymentRequest(deployment=deployment_pb))
Example #18
0
    def _update(self, deployment_pb, current_deployment, bento_pb, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._update(
                    deployment_pb, current_deployment, bento_pb, local_path
                )
        updated_deployment_spec = deployment_pb.spec
        updated_lambda_deployment_config = (
            updated_deployment_spec.aws_lambda_operator_config
        )
        updated_bento_service_metadata = bento_pb.bento.bento_service_metadata
        describe_result = self.describe(deployment_pb)
        if describe_result.status.status_code != status_pb2.Status.OK:
            error_code, error_message = status_pb_to_error_code_and_message(
                describe_result.status
            )
            raise YataiDeploymentException(
                f'Failed fetching Lambda deployment current status - '
                f'{error_code}:{error_message}'
            )
        latest_deployment_state = json.loads(describe_result.state.info_json)
        if 's3_bucket' in latest_deployment_state:
            lambda_s3_bucket = latest_deployment_state['s3_bucket']
        else:
            raise BentoMLException(
                'S3 Bucket is missing in the AWS Lambda deployment, please make sure '
                'it exists and try again'
            )

        _deploy_lambda_function(
            deployment_pb=deployment_pb,
            bento_service_metadata=updated_bento_service_metadata,
            deployment_spec=updated_deployment_spec,
            lambda_s3_bucket=lambda_s3_bucket,
            lambda_deployment_config=updated_lambda_deployment_config,
            bento_path=bento_path,
        )

        return ApplyDeploymentResponse(deployment=deployment_pb, status=Status.OK())
Example #19
0
    def apply(self, deployment_pb, repo, prev_deployment=None):
        deployment_spec = deployment_pb.spec
        sagemaker_config = deployment_spec.sagemaker_operator_config
        if sagemaker_config is None:
            raise BentoMLDeploymentException('Sagemaker configuration is missing.')

        archive_path = repo.get(
            deployment_spec.bento_name, deployment_spec.bento_version
        )

        # config = load_bentoml_config(bento_path)...

        sagemaker_client = boto3.client('sagemaker', sagemaker_config.region)

        with TemporarySageMakerContent(
            archive_path, deployment_spec.bento_name, deployment_spec.bento_version
        ) as temp_path:
            ecr_image_path = create_push_image_to_ecr(
                deployment_spec.bento_name, deployment_spec.bento_version, temp_path
            )

        execution_role_arn = get_arn_role_from_current_user()
        model_name = create_sagemaker_model_name(
            deployment_spec.bento_name, deployment_spec.bento_version
        )

        sagemaker_model_info = {
            "ModelName": model_name,
            "PrimaryContainer": {
                "ContainerHostname": model_name,
                "Image": ecr_image_path,
                "Environment": {
                    "API_NAME": sagemaker_config.api_name,
                    "BENTO_SERVER_TIMEOUT": config().get(
                        'apiserver', 'default_timeout'
                    ),
                    "BENTO_SERVER_WORKERS": config().get(
                        'apiserver', 'default_gunicorn_workers_count'
                    ),
                },
            },
            "ExecutionRoleArn": execution_role_arn,
        }
        logger.info("Creating sagemaker model %s", model_name)
        create_model_response = sagemaker_client.create_model(**sagemaker_model_info)
        logger.debug("AWS create model response: %s", create_model_response)

        production_variants = [
            {
                "VariantName": generate_aws_compatible_string(
                    deployment_spec.bento_name
                ),
                "ModelName": model_name,
                "InitialInstanceCount": sagemaker_config.instance_count,
                "InstanceType": sagemaker_config.instance_type,
            }
        ]
        endpoint_config_name = create_sagemaker_endpoint_config_name(
            deployment_spec.bento_name, deployment_spec.bento_version
        )
        logger.info(
            "Creating Sagemaker endpoint %s configuration", endpoint_config_name
        )
        create_endpoint_config_response = sagemaker_client.create_endpoint_config(
            EndpointConfigName=endpoint_config_name,
            ProductionVariants=production_variants,
        )
        logger.debug(
            "AWS create endpoint config response: %s", create_endpoint_config_response
        )

        endpoint_name = generate_aws_compatible_string(
            deployment_pb.namespace + '-' + deployment_spec.bento_name
        )
        if prev_deployment:
            logger.info("Updating sagemaker endpoint %s", endpoint_name)
            update_endpoint_response = sagemaker_client.update_endpoint(
                EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
            )
            logger.debug("AWS update endpoint response: %s", update_endpoint_response)
        else:
            logger.info("Creating sagemaker endpoint %s", endpoint_name)
            create_endpoint_response = sagemaker_client.create_endpoint(
                EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
            )
            logger.debug("AWS create endpoint response: %s", create_endpoint_response)

        res_deployment_pb = Deployment(state=DeploymentState())
        res_deployment_pb.CopyFrom(deployment_pb)

        return ApplyDeploymentResponse(status=Status.OK(), deployment=res_deployment_pb)
Example #20
0
def create_deployment(
    deployment_name,
    namespace,
    bento_name,
    bento_version,
    platform,
    operator_spec,
    labels=None,
    annotations=None,
    yatai_service=None,
):
    if yatai_service is None:
        from bentoml.yatai import get_yatai_service

        yatai_service = get_yatai_service()

    # Make sure there is no active deployment with the same deployment name
    get_deployment_pb = yatai_service.GetDeployment(
        GetDeploymentRequest(deployment_name=deployment_name,
                             namespace=namespace))
    if get_deployment_pb.status.status_code == status_pb2.Status.OK:
        raise YataiDeploymentException(
            'Deployment "{name}" already existed, use Update or Apply for updating '
            'existing deployment, delete the deployment, or use a different deployment '
            'name'.format(name=deployment_name))
    if get_deployment_pb.status.status_code != status_pb2.Status.NOT_FOUND:
        raise YataiDeploymentException(
            'Failed accesing YataiService deployment store. {error_code}:'
            '{error_message}'.format(
                error_code=Status.Name(get_deployment_pb.status.status_code),
                error_message=get_deployment_pb.status.error_message,
            ))

    deployment_dict = {
        "name": deployment_name,
        "namespace": namespace
        or config().get('deployment', 'default_namespace'),
        "labels": labels,
        "annotations": annotations,
        "spec": {
            "bento_name": bento_name,
            "bento_version": bento_version,
            "operator": platform,
        },
    }

    operator = platform.replace('-', '_').upper()
    try:
        operator_value = DeploymentSpec.DeploymentOperator.Value(operator)
    except ValueError:
        return ApplyDeploymentResponse(status=Status.INVALID_ARGUMENT(
            'Invalid platform "{}"'.format(platform)))
    if operator_value == DeploymentSpec.AWS_SAGEMAKER:
        deployment_dict['spec']['sagemaker_operator_config'] = {
            'region':
            operator_spec.get('region')
            or config().get('aws', 'default_region'),
            'instance_count':
            operator_spec.get('instance_count'),
            'instance_type':
            operator_spec.get('instance_type'),
            'api_name':
            operator_spec.get('api_name', ''),
        }
        if operator_spec.get('num_of_gunicorn_workers_per_instance'):
            deployment_dict['spec']['sagemaker_operator_config'][
                'num_of_gunicorn_workers_per_instance'] = operator_spec.get(
                    'num_of_gunicorn_workers_per_instance')
    elif operator_value == DeploymentSpec.AWS_LAMBDA:
        deployment_dict['spec']['aws_lambda_operator_config'] = {
            'region':
            operator_spec.get('region')
            or config().get('aws', 'default_region')
        }
        for field in ['api_name', 'memory_size', 'timeout']:
            if operator_spec.get(field):
                deployment_dict['spec']['aws_lambda_operator_config'][
                    field] = operator_spec[field]
    elif operator_value == DeploymentSpec.KUBERNETES:
        deployment_dict['spec']['kubernetes_operator_config'] = {
            'kube_namespace': operator_spec.get('kube_namespace', ''),
            'replicas': operator_spec.get('replicas', 0),
            'service_name': operator_spec.get('service_name', ''),
            'service_type': operator_spec.get('service_type', ''),
        }
    else:
        raise YataiDeploymentException(
            'Platform "{}" is not supported in the current version of '
            'BentoML'.format(platform))

    apply_response = apply_deployment(deployment_dict, yatai_service)

    if apply_response.status.status_code == status_pb2.Status.OK:
        describe_response = describe_deployment(deployment_name, namespace,
                                                yatai_service)
        if describe_response.status.status_code == status_pb2.Status.OK:
            deployment_state = describe_response.state
            apply_response.deployment.state.CopyFrom(deployment_state)
            return apply_response

    return apply_response
Example #21
0
    def apply(self, deployment_pb, yatai_service, prev_deployment=None):
        try:
            ensure_docker_available_or_raise()
            deployment_spec = deployment_pb.spec
            sagemaker_config = deployment_spec.sagemaker_operator_config
            if sagemaker_config is None:
                raise BentoMLDeploymentException('Sagemaker configuration is missing.')

            bento_pb = yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                )
            )
            if bento_pb.bento.uri.type != BentoUri.LOCAL:
                raise BentoMLException(
                    'BentoML currently only support local repository'
                )
            else:
                bento_path = bento_pb.bento.uri.uri

            ensure_deploy_api_name_exists_in_bento(
                [api.name for api in bento_pb.bento.bento_service_metadata.apis],
                [sagemaker_config.api_name],
            )

            sagemaker_client = boto3.client('sagemaker', sagemaker_config.region)

            with TempDirectory() as temp_dir:
                sagemaker_project_dir = os.path.jon(
                    temp_dir, deployment_spec.bento_name
                )
                init_sagemaker_project(sagemaker_project_dir, bento_path)
                ecr_image_path = create_push_docker_image_to_ecr(
                    deployment_spec.bento_name,
                    deployment_spec.bento_version,
                    sagemaker_project_dir,
                )

            execution_role_arn = get_arn_role_from_current_aws_user()
            model_name = create_sagemaker_model_name(
                deployment_spec.bento_name, deployment_spec.bento_version
            )

            sagemaker_model_info = {
                "ModelName": model_name,
                "PrimaryContainer": {
                    "ContainerHostname": model_name,
                    "Image": ecr_image_path,
                    "Environment": {
                        "API_NAME": sagemaker_config.api_name,
                        "BENTO_SERVER_TIMEOUT": config().get(
                            'apiserver', 'default_timeout'
                        ),
                        "BENTO_SERVER_WORKERS": config().get(
                            'apiserver', 'default_gunicorn_workers_count'
                        ),
                    },
                },
                "ExecutionRoleArn": execution_role_arn,
            }

            logger.info("Creating sagemaker model %s", model_name)
            try:
                create_model_response = sagemaker_client.create_model(
                    **sagemaker_model_info
                )
                logger.debug("AWS create model response: %s", create_model_response)
            except ClientError as e:
                status = _parse_aws_client_exception_or_raise(e)
                status.error_message = (
                    'Failed to create model for SageMaker Deployment: %s',
                    status.error_message,
                )
                return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

            production_variants = [
                {
                    "VariantName": generate_aws_compatible_string(
                        deployment_spec.bento_name
                    ),
                    "ModelName": model_name,
                    "InitialInstanceCount": sagemaker_config.instance_count,
                    "InstanceType": sagemaker_config.instance_type,
                }
            ]
            endpoint_config_name = create_sagemaker_endpoint_config_name(
                deployment_spec.bento_name, deployment_spec.bento_version
            )

            logger.info(
                "Creating Sagemaker endpoint %s configuration", endpoint_config_name
            )
            try:
                create_config_response = sagemaker_client.create_endpoint_config(
                    EndpointConfigName=endpoint_config_name,
                    ProductionVariants=production_variants,
                )
                logger.debug(
                    "AWS create endpoint config response: %s", create_config_response
                )
            except ClientError as e:
                # create endpoint failed, will remove previously created model
                cleanup_model_error = _cleanup_sagemaker_model(
                    sagemaker_client,
                    deployment_spec.bento_name,
                    deployment_spec.bento_version,
                )
                if cleanup_model_error:
                    cleanup_model_error.error_message = (
                        'Failed to clean up model after unsuccessfully '
                        'create endpoint config: %s',
                        cleanup_model_error.error_message,
                    )
                    return ApplyDeploymentResponse(
                        status=cleanup_model_error, deployment=deployment_pb
                    )

                status = _parse_aws_client_exception_or_raise(e)
                status.error_message = (
                    'Failed to create endpoint config for SageMaker deployment: %s',
                    status.error_message,
                )
                return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

            endpoint_name = generate_aws_compatible_string(
                deployment_pb.namespace + '-' + deployment_spec.bento_name
            )
            try:
                if prev_deployment:
                    logger.debug("Updating sagemaker endpoint %s", endpoint_name)
                    update_endpoint_response = sagemaker_client.update_endpoint(
                        EndpointName=endpoint_name,
                        EndpointConfigName=endpoint_config_name,
                    )
                    logger.debug(
                        "AWS update endpoint response: %s", update_endpoint_response
                    )
                else:
                    logger.debug("Creating sagemaker endpoint %s", endpoint_name)
                    create_endpoint_response = sagemaker_client.create_endpoint(
                        EndpointName=endpoint_name,
                        EndpointConfigName=endpoint_config_name,
                    )
                    logger.debug(
                        "AWS create endpoint response: %s", create_endpoint_response
                    )
            except ClientError as e:
                # create/update endpoint failed, will remove previously created config
                # and then remove the model
                cleanup_endpoint_config_error = _cleanup_sagemaker_endpoint_config(
                    client=sagemaker_client,
                    name=deployment_spec.bento_name,
                    version=deployment_spec.bento_version,
                )
                if cleanup_endpoint_config_error:
                    cleanup_endpoint_config_error.error_message = (
                        'Failed to clean up endpoint config after unsuccessfully '
                        'apply SageMaker deployment: %s',
                        cleanup_endpoint_config_error.error_message,
                    )
                    return ApplyDeploymentResponse(
                        status=cleanup_endpoint_config_error, deployment=deployment_pb
                    )

                cleanup_model_error = _cleanup_sagemaker_model(
                    client=sagemaker_client,
                    name=deployment_spec.bento_name,
                    version=deployment_spec.bento_version,
                )
                if cleanup_model_error:
                    cleanup_model_error.error_message = (
                        'Failed to clean up model after unsuccessfully '
                        'apply SageMaker deployment: %s',
                        cleanup_model_error.error_message,
                    )
                    return ApplyDeploymentResponse(
                        status=cleanup_model_error, deployment=deployment_pb
                    )

                status = _parse_aws_client_exception_or_raise(e)
                status.error_message = (
                    'Failed to apply SageMaker deployment: %s',
                    status.error_message,
                )
                return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

            res_deployment_pb = Deployment(state=DeploymentState())
            res_deployment_pb.CopyFrom(deployment_pb)

            return ApplyDeploymentResponse(
                status=Status.OK(), deployment=res_deployment_pb
            )
        except BentoMLException as error:
            return ApplyDeploymentResponse(status=exception_to_return_status(error))
Example #22
0
    def _add(self, deployment_pb, bento_pb, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._add(deployment_pb, bento_pb, local_path)

        deployment_spec = deployment_pb.spec
        lambda_deployment_config = deployment_spec.aws_lambda_operator_config
        bento_service_metadata = bento_pb.bento.bento_service_metadata
        lambda_s3_bucket = generate_aws_compatible_string(
            'btml-{namespace}-{name}-{random_string}'.format(
                namespace=deployment_pb.namespace,
                name=deployment_pb.name,
                random_string=uuid.uuid4().hex[:6].lower(),
            ))

        try:
            py_major, py_minor, _ = bento_service_metadata.env.python_version.split(
                '.')
            if py_major != '3':
                raise BentoMLException(
                    'Python 2 is not supported for Lambda Deployment')
            python_runtime = 'python{}.{}'.format(py_major, py_minor)

            artifact_types = [
                item.artifact_type for item in bento_service_metadata.artifacts
            ]
            if any(i in ['TensorflowSavedModelArtifact', 'KerasModelArtifact']
                   for i in artifact_types) and (py_major, py_minor) != ('3',
                                                                         '6'):
                raise BentoMLException(
                    'For Tensorflow and Keras model, only python3.6 is '
                    'supported for AWS Lambda deployment')

            api_names = ([lambda_deployment_config.api_name]
                         if lambda_deployment_config.api_name else
                         [api.name for api in bento_service_metadata.apis])

            raise_if_api_names_not_found_in_bento_service_metadata(
                bento_service_metadata, api_names)

            create_s3_bucket_if_not_exists(lambda_s3_bucket,
                                           lambda_deployment_config.region)
            deployment_path_prefix = os.path.join(deployment_pb.namespace,
                                                  deployment_pb.name)
            with TempDirectory() as lambda_project_dir:
                logger.debug(
                    'Generating cloudformation template.yaml for lambda project at %s',
                    lambda_project_dir,
                )
                template_file_path = _create_aws_lambda_cloudformation_template_file(
                    project_dir=lambda_project_dir,
                    namespace=deployment_pb.namespace,
                    deployment_name=deployment_pb.name,
                    deployment_path_prefix=deployment_path_prefix,
                    api_names=api_names,
                    bento_service_name=deployment_spec.bento_name,
                    s3_bucket_name=lambda_s3_bucket,
                    py_runtime=python_runtime,
                    memory_size=lambda_deployment_config.memory_size,
                    timeout=lambda_deployment_config.timeout,
                )
                logger.debug('Validating generated template.yaml')
                validate_lambda_template(
                    template_file_path,
                    lambda_deployment_config.region,
                    lambda_project_dir,
                )
                logger.debug(
                    'Initializing lambda project in directory: %s ...',
                    lambda_project_dir,
                )
                init_sam_project(
                    lambda_project_dir,
                    bento_path,
                    deployment_pb.name,
                    deployment_spec.bento_name,
                    api_names,
                    aws_region=lambda_deployment_config.region,
                )
                for api_name in api_names:
                    build_directory = os.path.join(lambda_project_dir,
                                                   '.aws-sam', 'build',
                                                   api_name)
                    logger.debug(
                        'Checking is function "%s" bundle under lambda size '
                        'limit',
                        api_name,
                    )
                    # Since we only use s3 get object in lambda function, and
                    # lambda function pack their own boto3/botocore modules,
                    # we will just delete those modules from function bundle
                    # directory
                    delete_list = ['boto3', 'botocore']
                    for name in delete_list:
                        logger.debug('Remove module "%s" from build directory',
                                     name)
                        shutil.rmtree(os.path.join(build_directory, name))
                    total_build_dir_size = total_file_or_directory_size(
                        build_directory)
                    if total_build_dir_size > LAMBDA_FUNCTION_MAX_LIMIT:
                        raise BentoMLException(
                            'Build function size is over 700MB, max size '
                            'capable for AWS Lambda function')
                    if total_build_dir_size >= LAMBDA_FUNCTION_LIMIT:
                        logger.debug(
                            'Function %s is over lambda size limit, attempting '
                            'reduce it',
                            api_name,
                        )
                        reduce_bundle_size_and_upload_extra_resources_to_s3(
                            build_directory=build_directory,
                            region=lambda_deployment_config.region,
                            s3_bucket=lambda_s3_bucket,
                            deployment_prefix=deployment_path_prefix,
                            function_name=api_name,
                            lambda_project_dir=lambda_project_dir,
                        )
                    else:
                        logger.debug(
                            'Function bundle is within Lambda limit, removing '
                            'download_extra_resources.py file from function bundle'
                        )
                        os.remove(
                            os.path.join(build_directory,
                                         'download_extra_resources.py'))
                logger.info('Packaging AWS Lambda project at %s ...',
                            lambda_project_dir)
                lambda_package(
                    lambda_project_dir,
                    lambda_deployment_config.region,
                    lambda_s3_bucket,
                    deployment_path_prefix,
                )
                logger.info('Deploying lambda project')
                stack_name = generate_aws_compatible_string(
                    deployment_pb.namespace + '-' + deployment_pb.name)
                lambda_deploy(
                    lambda_project_dir,
                    lambda_deployment_config.region,
                    stack_name=stack_name,
                )

            deployment_pb.state.state = DeploymentState.PENDING
            return ApplyDeploymentResponse(status=Status.OK(),
                                           deployment=deployment_pb)
        except BentoMLException as error:
            if lambda_s3_bucket and lambda_deployment_config:
                _cleanup_s3_bucket_if_exist(lambda_s3_bucket,
                                            lambda_deployment_config.region)
            raise error
Example #23
0
    def apply(self, deployment_pb, yatai_service, prev_deployment=None):
        try:
            ensure_docker_available_or_raise()
            deployment_spec = deployment_pb.spec
            aws_config = deployment_spec.aws_lambda_operator_config

            bento_pb = yatai_service.GetBento(
                GetBentoRequest(
                    bento_name=deployment_spec.bento_name,
                    bento_version=deployment_spec.bento_version,
                )
            )
            if bento_pb.bento.uri.type != BentoUri.LOCAL:
                raise BentoMLException(
                    'BentoML currently only support local repository'
                )
            else:
                bento_path = bento_pb.bento.uri.uri
            bento_service_metadata = bento_pb.bento.bento_service_metadata

            template = 'aws-python3'
            if version.parse(bento_service_metadata.env.python_version) < version.parse(
                '3.0.0'
            ):
                template = 'aws-python'

            api_names = (
                [aws_config.api_name]
                if aws_config.api_name
                else [api.name for api in bento_service_metadata.apis]
            )
            ensure_deploy_api_name_exists_in_bento(
                [api.name for api in bento_service_metadata.apis], api_names
            )

            with TempDirectory() as serverless_project_dir:
                init_serverless_project_dir(
                    serverless_project_dir,
                    bento_path,
                    deployment_pb.name,
                    deployment_spec.bento_name,
                    template,
                )
                generate_aws_lambda_handler_py(
                    deployment_spec.bento_name, api_names, serverless_project_dir
                )
                generate_aws_lambda_serverless_config(
                    bento_service_metadata.env.python_version,
                    deployment_pb.name,
                    api_names,
                    serverless_project_dir,
                    aws_config.region,
                    # BentoML deployment namespace is mapping to serverless `stage`
                    # concept
                    stage=deployment_pb.namespace,
                )
                logger.info(
                    'Installing additional packages: serverless-python-requirements'
                )
                install_serverless_plugin(
                    "serverless-python-requirements", serverless_project_dir
                )
                logger.info('Deploying to AWS Lambda')
                call_serverless_command(["deploy"], serverless_project_dir)

            res_deployment_pb = Deployment(state=DeploymentState())
            res_deployment_pb.CopyFrom(deployment_pb)
            state = self.describe(res_deployment_pb, yatai_service).state
            res_deployment_pb.state.CopyFrom(state)
            return ApplyDeploymentResponse(
                status=Status.OK(), deployment=res_deployment_pb
            )
        except BentoMLException as error:
            return ApplyDeploymentResponse(status=exception_to_return_status(error))
Example #24
0
    def _update(self, deployment_pb, current_deployment, bento_pb, bento_path):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._update(deployment_pb, current_deployment,
                                    bento_pb, local_path)
        updated_deployment_spec = deployment_pb.spec
        updated_sagemaker_config = updated_deployment_spec.sagemaker_operator_config
        sagemaker_client = boto3.client('sagemaker',
                                        updated_sagemaker_config.region)

        try:
            raise_if_api_names_not_found_in_bento_service_metadata(
                bento_pb.bento.bento_service_metadata,
                [updated_sagemaker_config.api_name],
            )
            describe_latest_deployment_state = self.describe(deployment_pb)
            current_deployment_spec = current_deployment.spec
            current_sagemaker_config = current_deployment_spec.sagemaker_operator_config
            latest_deployment_state = json.loads(
                describe_latest_deployment_state.state.info_json)

            current_ecr_image_tag = latest_deployment_state[
                'ProductionVariants'][0]['DeployedImages'][0]['SpecifiedImage']
            if (updated_deployment_spec.bento_name !=
                    current_deployment_spec.bento_name
                    or updated_deployment_spec.bento_version !=
                    current_deployment_spec.bento_version):
                logger.debug(
                    'BentoService tag is different from current deployment, '
                    'creating new docker image and push to ECR')
                with TempDirectory() as temp_dir:
                    sagemaker_project_dir = os.path.join(
                        temp_dir, updated_deployment_spec.bento_name)
                    _init_sagemaker_project(
                        sagemaker_project_dir,
                        bento_path,
                        bento_pb.bento.bento_service_metadata.env.
                        docker_base_image,
                    )
                    ecr_image_path = create_and_push_docker_image_to_ecr(
                        updated_sagemaker_config.region,
                        updated_deployment_spec.bento_name,
                        updated_deployment_spec.bento_version,
                        sagemaker_project_dir,
                    )
            else:
                logger.debug('Using existing ECR image for Sagemaker model')
                ecr_image_path = current_ecr_image_tag

            (
                updated_sagemaker_model_name,
                updated_sagemaker_endpoint_config_name,
                sagemaker_endpoint_name,
            ) = _get_sagemaker_resource_names(deployment_pb)
            (
                current_sagemaker_model_name,
                current_sagemaker_endpoint_config_name,
                _,
            ) = _get_sagemaker_resource_names(current_deployment)

            if (updated_sagemaker_config.api_name !=
                    current_sagemaker_config.api_name
                    or updated_sagemaker_config.
                    num_of_gunicorn_workers_per_instance !=
                    current_sagemaker_config.
                    num_of_gunicorn_workers_per_instance
                    or ecr_image_path != current_ecr_image_tag):
                logger.debug(
                    'Sagemaker model requires update. Delete current sagemaker model %s'
                    'and creating new model %s',
                    current_sagemaker_model_name,
                    updated_sagemaker_model_name,
                )
                _delete_sagemaker_model_if_exist(sagemaker_client,
                                                 current_sagemaker_model_name)
                _create_sagemaker_model(
                    sagemaker_client,
                    updated_sagemaker_model_name,
                    ecr_image_path,
                    updated_sagemaker_config,
                )
            # When bento service tag is not changed, we need to delete the current
            # endpoint configuration in order to create new one to avoid name collation
            if (current_sagemaker_endpoint_config_name ==
                    updated_sagemaker_endpoint_config_name):
                logger.debug(
                    'Current sagemaker config name %s is same as updated one, '
                    'delete it before create new endpoint config',
                    current_sagemaker_endpoint_config_name,
                )
                _delete_sagemaker_endpoint_config_if_exist(
                    sagemaker_client, current_sagemaker_endpoint_config_name)
            logger.debug(
                'Create new endpoint configuration %s',
                updated_sagemaker_endpoint_config_name,
            )
            _create_sagemaker_endpoint_config(
                sagemaker_client,
                updated_sagemaker_model_name,
                updated_sagemaker_endpoint_config_name,
                updated_sagemaker_config,
            )
            logger.debug(
                'Updating endpoint to new endpoint configuration %s',
                updated_sagemaker_endpoint_config_name,
            )
            _update_sagemaker_endpoint(
                sagemaker_client,
                sagemaker_endpoint_name,
                updated_sagemaker_endpoint_config_name,
            )
            logger.debug(
                'Delete old sagemaker endpoint config %s',
                current_sagemaker_endpoint_config_name,
            )
            _delete_sagemaker_endpoint_config_if_exist(
                sagemaker_client, current_sagemaker_endpoint_config_name)
        except AWSServiceError as e:
            delete_sagemaker_deployment_resources_if_exist(deployment_pb)
            raise e

        return ApplyDeploymentResponse(status=Status.OK(),
                                       deployment=deployment_pb)
Example #25
0
    def ApplyDeployment(self, request, context=None):
        try:
            # apply default namespace if not set
            request.deployment.namespace = (
                request.deployment.namespace or self.default_namespace
            )

            validation_errors = validate_deployment_pb_schema(request.deployment)
            if validation_errors:
                raise InvalidArgument(
                    'Failed to validate deployment. {errors}'.format(
                        errors=validation_errors
                    )
                )

            previous_deployment = self.deployment_store.get(
                request.deployment.name, request.deployment.namespace
            )
            if not previous_deployment:
                request.deployment.created_at.GetCurrentTime()
            request.deployment.last_updated_at.GetCurrentTime()

            self.deployment_store.insert_or_update(request.deployment)
            # find deployment operator based on deployment spec
            operator = get_deployment_operator(self, request.deployment)

            # deploying to target platform
            if previous_deployment:
                response = operator.update(request.deployment, previous_deployment)
            else:
                response = operator.add(request.deployment)

            if response.status.status_code == status_pb2.Status.OK:
                # update deployment state
                if response and response.deployment:
                    self.deployment_store.insert_or_update(response.deployment)
                else:
                    raise BentoMLException(
                        "DeploymentOperator Internal Error: failed to add or update "
                        "deployment metadata to database"
                    )
                logger.info(
                    "ApplyDeployment (%s, namespace %s) succeeded",
                    request.deployment.name,
                    request.deployment.namespace,
                )
            else:
                if not previous_deployment:
                    # When failed to create the deployment, delete it from active
                    # deployments records
                    self.deployment_store.delete(
                        request.deployment.name, request.deployment.namespace
                    )
                logger.debug(
                    "ApplyDeployment (%s, namespace %s) failed: %s",
                    request.deployment.name,
                    request.deployment.namespace,
                    response.status.error_message,
                )

            return response

        except BentoMLException as e:
            logger.error("RPC ERROR ApplyDeployment: %s", e)
            return ApplyDeploymentResponse(status=e.status_proto)
Example #26
0
    def _apply(
        self, deployment_pb, bento_pb, yatai_service, bento_path, prev_deployment=None
    ):
        if loader._is_remote_path(bento_path):
            with loader._resolve_remote_bundle_path(bento_path) as local_path:
                return self._apply(
                    deployment_pb, bento_pb, yatai_service, local_path, prev_deployment
                )

        deployment_spec = deployment_pb.spec
        sagemaker_config = deployment_spec.sagemaker_operator_config

        ensure_deploy_api_name_exists_in_bento(
            [api.name for api in bento_pb.bento.bento_service_metadata.apis],
            [sagemaker_config.api_name],
        )

        sagemaker_client = boto3.client('sagemaker', sagemaker_config.region)

        with TempDirectory() as temp_dir:
            sagemaker_project_dir = os.path.join(temp_dir, deployment_spec.bento_name)
            init_sagemaker_project(sagemaker_project_dir, bento_path)
            ecr_image_path = create_push_docker_image_to_ecr(
                sagemaker_config.region,
                deployment_spec.bento_name,
                deployment_spec.bento_version,
                sagemaker_project_dir,
            )

        try:
            model_name = _create_sagemaker_model(
                sagemaker_client,
                deployment_spec.bento_name,
                deployment_spec.bento_version,
                ecr_image_path,
                sagemaker_config.api_name,
            )
        except ClientError as e:
            status = _parse_aws_client_exception(e)
            status.error_message = (
                'Failed to create model for SageMaker'
                ' Deployment: {}'.format(status.error_message)
            )
            return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

        try:
            endpoint_config_name = _create_sagemaker_endpoint_config(
                sagemaker_client,
                model_name,
                deployment_spec.bento_name,
                deployment_spec.bento_version,
                sagemaker_config,
            )
        except ClientError as e:
            # create endpoint failed, will remove previously created model
            cleanup_model_error = _cleanup_sagemaker_model(
                sagemaker_client,
                deployment_spec.bento_name,
                deployment_spec.bento_version,
            )
            if cleanup_model_error:
                cleanup_model_error.error_message = (
                    'Failed to clean up model after unsuccessfully '
                    'create endpoint config: %s',
                    cleanup_model_error.error_message,
                )
                return ApplyDeploymentResponse(
                    status=cleanup_model_error, deployment=deployment_pb
                )

            status = _parse_aws_client_exception(e)
            status.error_message = (
                'Failed to create endpoint config for SageMaker deployment: %s',
                status.error_message,
            )
            return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

        endpoint_name = generate_aws_compatible_string(
            deployment_pb.namespace + '-' + deployment_spec.bento_name
        )
        try:
            if prev_deployment:
                logger.debug("Updating sagemaker endpoint %s", endpoint_name)
                update_endpoint_response = sagemaker_client.update_endpoint(
                    EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
                )
                logger.debug(
                    "AWS update endpoint response: %s", update_endpoint_response
                )
            else:
                logger.debug("Creating sagemaker endpoint %s", endpoint_name)
                create_endpoint_response = sagemaker_client.create_endpoint(
                    EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
                )
                logger.debug(
                    "AWS create endpoint response: %s", create_endpoint_response
                )
        except ClientError as e:
            # create/update endpoint failed, will remove previously created config
            # and then remove the model
            cleanup_endpoint_config_error = _cleanup_sagemaker_endpoint_config(
                client=sagemaker_client,
                name=deployment_spec.bento_name,
                version=deployment_spec.bento_version,
            )
            if cleanup_endpoint_config_error:
                cleanup_endpoint_config_error.error_message = (
                    'Failed to clean up endpoint config after unsuccessfully '
                    'apply SageMaker deployment: %s',
                    cleanup_endpoint_config_error.error_message,
                )
                return ApplyDeploymentResponse(
                    status=cleanup_endpoint_config_error, deployment=deployment_pb
                )

            cleanup_model_error = _cleanup_sagemaker_model(
                client=sagemaker_client,
                name=deployment_spec.bento_name,
                version=deployment_spec.bento_version,
            )
            if cleanup_model_error:
                cleanup_model_error.error_message = (
                    'Failed to clean up model after unsuccessfully apply '
                    'SageMaker deployment: {}'.format(cleanup_model_error.error_message)
                )
                return ApplyDeploymentResponse(
                    status=cleanup_model_error, deployment=deployment_pb
                )

            status = _parse_aws_client_exception(e)
            status.error_message = 'Failed to apply SageMaker ' 'deployment: {}'.format(
                status.error_message
            )
            return ApplyDeploymentResponse(status=status, deployment=deployment_pb)

        res_deployment_pb = Deployment(state=DeploymentState())
        res_deployment_pb.CopyFrom(deployment_pb)

        return ApplyDeploymentResponse(status=Status.OK(), deployment=res_deployment_pb)
Example #27
0
def create_deployment(
    deployment_name,
    namespace,
    bento_name,
    bento_version,
    platform,
    operator_spec,
    labels=None,
    annotations=None,
    yatai_service=None,
):
    if yatai_service is None:
        from bentoml.yatai import get_yatai_service

        yatai_service = get_yatai_service()

    try:
        # Make sure there is no active deployment with the same deployment name
        get_deployment_pb = yatai_service.GetDeployment(
            GetDeploymentRequest(deployment_name=deployment_name, namespace=namespace)
        )
        if get_deployment_pb.status.status_code == status_pb2.Status.OK:
            raise BentoMLDeploymentException(
                'Deployment "{name}" already existed, use Update or Apply for updating'
                'existing deployment, or create the deployment with a different name or'
                'under a different deployment namespace'.format(name=deployment_name)
            )
        if get_deployment_pb.status.status_code != status_pb2.Status.NOT_FOUND:
            raise BentoMLDeploymentException(
                'Failed accesing YataiService deployment store. {error_code}:'
                '{error_message}'.format(
                    error_code=Status.Name(get_deployment_pb.status.status_code),
                    error_message=get_deployment_pb.status.error_message,
                )
            )

        deployment_dict = {
            "name": deployment_name,
            "namespace": namespace or config().get('deployment', 'default_namespace'),
            "labels": labels,
            "annotations": annotations,
            "spec": {
                "bento_name": bento_name,
                "bento_version": bento_version,
                "operator": platform,
            },
        }

        operator = platform.replace('-', '_').upper()
        try:
            operator_value = DeploymentSpec.DeploymentOperator.Value(operator)
        except ValueError:
            return ApplyDeploymentResponse(
                status=Status.INVALID_ARGUMENT('Invalid platform "{}"'.format(platform))
            )
        if operator_value == DeploymentSpec.AWS_SAGEMAKER:
            deployment_dict['spec']['sagemaker_operator_config'] = {
                'region': operator_spec.get('region')
                or config().get('aws', 'default_region'),
                'instance_count': operator_spec.get('instance_count')
                or config().getint('sagemaker', 'default_instance_count'),
                'instance_type': operator_spec.get('instance_type')
                or config().get('sagemaker', 'default_instance_type'),
                'api_name': operator_spec.get('api_name', ''),
            }
        elif operator_value == DeploymentSpec.AWS_LAMBDA:
            deployment_dict['spec']['aws_lambda_operator_config'] = {
                'region': operator_spec.get('region')
                or config().get('aws', 'default_region')
            }
            if operator_spec.get('api_name'):
                deployment_dict['spec']['aws_lambda_operator_config'][
                    'api_name'
                ] = operator_spec['api_name']
        elif operator_value == DeploymentSpec.GCP_FCUNTION:
            deployment_dict['spec']['gcp_function_operatorConfig'] = {
                'region': operator_spec.get('region')
                or config().get('google-cloud', 'default_region')
            }
            if operator_spec.get('api_name'):
                deployment_dict['spec']['gcp_function_operator_config'][
                    'api_name'
                ] = operator_spec['api_name']
        elif operator_value == DeploymentSpec.KUBERNETES:
            deployment_dict['spec']['kubernetes_operator_config'] = {
                'kube_namespace': operator_spec.get('kube_namespace', ''),
                'replicas': operator_spec.get('replicas', 0),
                'service_name': operator_spec.get('service_name', ''),
                'service_type': operator_spec.get('service_type', ''),
            }
        else:
            raise BentoMLDeploymentException(
                'Platform "{}" is not supported in the current version of '
                'BentoML'.format(platform)
            )

        return apply_deployment(deployment_dict, yatai_service)
    except BentoMLException as error:
        return ApplyDeploymentResponse(status=Status.INTERNAL(str(error)))
Example #28
0
    def apply(self, deployment_pb):
        # deploy code.....

        deployment = self.get(deployment_pb).deployment
        return ApplyDeploymentResponse(status=Status.OK, deployment=deployment)