Skip to content
This repository was archived by the owner on Jan 4, 2024. It is now read-only.

Commit be1eae4

Browse files
authored
Merge pull request #5 from Merlion-Crew/ml_model_uc81
UC-81 Build Jenkins pipeline to deploy ML model directly to Azure App Service (without docker image)
2 parents 38168bd + 4aa8e1c commit be1eae4

9 files changed

Lines changed: 229 additions & 63 deletions
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
pipeline {
2+
agent { label 'master' }
3+
environment {
4+
ML_IMAGE_FOLDER = 'imagefiles'
5+
IMAGE_NAME = 'mlmodelimage'
6+
MODEL_NAME = "${MODEL_NAME}"
7+
SCORE_SCRIPT = 'scoring/score.py'
8+
RESOURCE_GROUP = "${RESOURCE_GROUP}"
9+
WORKSPACE_NAME = "${WORKSPACE_NAME}"
10+
ML_CONTAINER_REGISTRY = "${ML_CONTAINER_REGISTRY}"
11+
}
12+
stages {
13+
stage('initialize') {
14+
steps {
15+
echo 'Remove the previous one!'
16+
}
17+
post {
18+
always {
19+
deleteDir() /* clean up our workspace */
20+
}
21+
}
22+
}
23+
stage ('Download build artifacts (get model name)') {
24+
environment {
25+
BUILD_ARTIFACT_FOLDER = "download"
26+
}
27+
steps {
28+
script {
29+
copyArtifacts(projectName: "${env.build_job_name}", selector: buildParameter("ml_model_selector"), target: "${env.BUILD_ARTIFACT_FOLDER}");
30+
def FILES_LIST = sh (script: "ls '${env.BUILD_ARTIFACT_FOLDER}'", returnStdout: true).trim()
31+
//DEBUG
32+
echo "FILES_LIST : ${FILES_LIST}"
33+
MODEL_NAME = sh (script: "cat ${env.BUILD_ARTIFACT_FOLDER}/model_name.txt",
34+
returnStdout: true).trim()
35+
sh "echo ${MODEL_NAME}"
36+
}
37+
}
38+
}
39+
stage('generate_dockerfile') {
40+
steps {
41+
echo "Hello docker image build ${env.BUILD_ID}"
42+
checkout scm
43+
//checkout([$class: 'GitSCM', branches: [[name: '*/ml_model_uc81']],
44+
// userRemoteConfigs: [[url: 'https://github.com/Merlion-Crew/MLOpsPython.git/']]])
45+
46+
sh '''
47+
conda env create --file ./diabetes_regression/ci_dependencies.yml --force
48+
'''
49+
50+
withCredentials([azureServicePrincipal("${AZURE_SP}")]) {
51+
sh '''#!/bin/bash -ex
52+
az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET -t $AZURE_TENANT_ID
53+
az account set -s $AZURE_SUBSCRIPTION_ID
54+
export SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID
55+
source /home/azureuser/anaconda3/bin/activate mlopspython_ci
56+
python3 -m ml_service.util.create_scoring_image
57+
'''
58+
}
59+
}
60+
}
61+
stage('build_and_push') {
62+
steps {
63+
echo "Build docker images"
64+
65+
sh '''#!/bin/bash -ex
66+
az acr login --name $ML_CONTAINER_REGISTRY
67+
docker build -t $NEXUS_DOCKER_REGISTRY_URL/$IMAGE_NAME:$BUILD_ID ./diabetes_regression/scoring/$ML_IMAGE_FOLDER/
68+
'''
69+
70+
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'nexus-docker-repo',
71+
usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) {
72+
73+
sh '''#!/bin/bash -ex
74+
docker login -u $USERNAME --password $PASSWORD https://$NEXUS_DOCKER_REGISTRY_URL
75+
docker push $NEXUS_DOCKER_REGISTRY_URL/$IMAGE_NAME:$BUILD_ID
76+
'''
77+
}
78+
}
79+
}
80+
stage('deploy') {
81+
steps {
82+
echo "Deploy to Azure App Service"
83+
84+
withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'nexus-docker-repo',
85+
usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD']]) {
86+
87+
sh '''#!/bin/bash -ex
88+
az webapp config container set --name $AZURE_APPSERVICE_NAME --resource-group $APPSERVICE_RESOURCE_GROUP --docker-custom-image-name $NEXUS_DOCKER_REGISTRY_URL/$IMAGE_NAME:$BUILD_ID --docker-registry-server-url https://$NEXUS_DOCKER_REGISTRY_URL --docker-registry-server-user $USERNAME --docker-registry-server-password $PASSWORD
89+
az webapp restart --name $AZURE_APPSERVICE_NAME --resource-group $APPSERVICE_RESOURCE_GROUP
90+
'''
91+
}
92+
}
93+
}
94+
}
95+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
pipeline {
2+
agent { label 'master' }
3+
environment {
4+
ML_IMAGE_FOLDER = 'imagefiles'
5+
IMAGE_NAME = 'mlmodelimage'
6+
MODEL_NAME = "${MODEL_NAME}"
7+
SCORE_SCRIPT = 'scoring/score.py'
8+
RESOURCE_GROUP = "${RESOURCE_GROUP}"
9+
WORKSPACE_NAME = "${WORKSPACE_NAME}"
10+
PACKAGE_FOLDER = './download'
11+
}
12+
stages {
13+
stage('initialize') {
14+
steps {
15+
echo 'Remove the previous one!'
16+
}
17+
post {
18+
always {
19+
deleteDir() /* clean up our workspace */
20+
}
21+
}
22+
}
23+
stage('download_model') {
24+
steps {
25+
echo "Downloading..."
26+
checkout scm
27+
28+
withCredentials([azureServicePrincipal("${AZURE_SP}")]) {
29+
sh '''#!/bin/bash -ex
30+
az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET -t $AZURE_TENANT_ID
31+
az account set -s $AZURE_SUBSCRIPTION_ID
32+
MODEL_ID=$(az ml model list --workspace-name $WORKSPACE_NAME --model-name $MODEL_NAME --resource-group $RESOURCE_GROUP --latest --query [0].id)
33+
az ml model download --resource-group $RESOURCE_GROUP --workspace-name $WORKSPACE_NAME --model-id $MODEL_ID --target-dir $PACKAGE_FOLDER
34+
'''
35+
}
36+
}
37+
}
38+
stage('build_zip_package') {
39+
steps {
40+
echo "Packaging..."
41+
42+
sh '''#!/bin/bash -ex
43+
cp ./ml_service/util/scoring/* $PACKAGE_FOLDER/
44+
cp ./diabetes_regression/$SCORE_SCRIPT $PACKAGE_FOLDER/
45+
cd $PACKAGE_FOLDER/
46+
zip -r deployment-${BUILD_NUMBER}.zip .
47+
'''
48+
}
49+
}
50+
stage('prod') {
51+
steps {
52+
echo 'Deploying!'
53+
sh '''
54+
cd $PACKAGE_FOLDER/
55+
curl -X POST -u '\$'$AZURE_APPSERVICE_NAME:$AZURE_APPSERVICE_DEPLOYMENT_PASSWORD --data-binary @"deployment-${BUILD_NUMBER}.zip" https://$AZURE_APPSERVICE_NAME.scm.azurewebsites.net/api/zipdeploy
56+
'''
57+
}
58+
}
59+
}
60+
}

diabetes_regression/ci_dependencies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ dependencies:
1717

1818
- pip:
1919
# dependencies with versions aligned with conda_dependencies.yml.
20-
- azureml-sdk==1.13.*
20+
- azureml-sdk
2121

2222
# Additional pip dependencies for the CI environment.
2323
- pytest==5.4.*

diabetes_regression/conda_dependencies.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ dependencies:
2323

2424
- pip:
2525
# Base AzureML SDK
26-
- azureml-sdk==1.13.*
26+
- azureml-sdk
2727

2828
# Minimum required for the scoring environment. Must match AzureML SDK version.
2929
# https://docs.microsoft.com/en-us/azure/machine-learning/concept-environments
30-
- azureml-defaults==1.13.*
30+
- azureml-defaults
3131

3232
# Training deps
3333
- scikit-learn

ml_service/pipelines/build-model-container-image.Jenkinsfile

Lines changed: 0 additions & 59 deletions
This file was deleted.

ml_service/util/create_scoring_image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
args = parser.parse_args()
2626

27-
model = Model(ws, name=e.model_name, version=e.model_version)
27+
model = Model(ws, name=e.model_name)
2828
sources_dir = e.sources_directory_train
2929
if (sources_dir is None):
3030
sources_dir = 'diabetes_regression'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from flask import Flask, request, make_response, jsonify
2+
import main
3+
4+
app = Flask(__name__)
5+
main.init()
6+
7+
@app.route("/")
8+
def hello():
9+
return "Hello World!"
10+
11+
@app.route('/score', methods=['POST'])
12+
def score_realtime():
13+
14+
print(request.get_json()['data'])
15+
16+
#require request data in json format
17+
if 'Content-Type' not in request.headers or request.headers['Content-Type'] != 'application/json':
18+
return make_response(
19+
'Expects Content-Type to be application/json!',
20+
415
21+
)
22+
23+
response = main.run(request.get_json()['data'], request.headers)
24+
headers = {"Content-Type": "application/json"}
25+
return make_response(
26+
jsonify(response),
27+
200,
28+
{"Content-Type": "application/json"}
29+
)

ml_service/util/scoring/main.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import inspect
3+
import importlib.util as imp
4+
import logging
5+
import sys
6+
7+
8+
9+
script_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'score.py')
10+
driver_module_spec = imp.spec_from_file_location('service_driver', script_location)
11+
driver_module = imp.module_from_spec(driver_module_spec)
12+
driver_module_spec.loader.exec_module(driver_module)
13+
14+
def run(http_body, request_headers):
15+
global run_supports_request_headers
16+
17+
arguments = {run_input_parameter_name: http_body}
18+
if run_supports_request_headers:
19+
arguments["request_headers"] = request_headers
20+
21+
return_obj = driver_module.run(**arguments)
22+
23+
return return_obj
24+
25+
26+
def init():
27+
global run_input_parameter_name
28+
global run_supports_request_headers
29+
30+
run_args = inspect.signature(driver_module.run).parameters.keys()
31+
run_args_list = list(run_args)
32+
run_input_parameter_name = run_args_list[0] if run_args_list[0] != "request_headers" else run_args_list[1]
33+
run_supports_request_headers = "request_headers" in run_args_list
34+
35+
driver_module.init()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
numpy==1.18.*
2+
joblib==0.16.0
3+
azureml-sdk==1.14.*
4+
azureml-defaults==1.14.*
5+
inference-schema[numpy-support]
6+
sklearn

0 commit comments

Comments
 (0)