From 573f9dc119bd751369045907c31f90fce861e6ee Mon Sep 17 00:00:00 2001 From: Santhosha Rajashekar Date: Sun, 9 Jun 2024 22:55:15 +0200 Subject: [PATCH] simulates changes (#9) updated the workflows and dependencies --- .github/workflows/deploy-model-manual.yml | 37 +++++++ .github/workflows/deploy-webapp.yml | 40 ++++++++ .github/workflows/train-model.yml | 5 +- requirements.txt | 7 +- src/deploy-endpoint.yml | 7 +- src/infra/main.bicep | 76 +++++++++++++++ src/job-prod.yml | 2 +- src/myapp/myapp/__init__.py | 0 src/myapp/myapp/app.py | 114 ++++++++++++++++++++++ src/myapp/myapp/requirements.txt | 2 + src/myapp/myapp/templates/index.html | 30 ++++++ src/myapp/myapp/templates/result.html | 10 ++ src/myapp/myapp/web.config | 14 +++ 13 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/deploy-model-manual.yml create mode 100644 .github/workflows/deploy-webapp.yml create mode 100644 src/infra/main.bicep create mode 100644 src/myapp/myapp/__init__.py create mode 100644 src/myapp/myapp/app.py create mode 100644 src/myapp/myapp/requirements.txt create mode 100644 src/myapp/myapp/templates/index.html create mode 100644 src/myapp/myapp/templates/result.html create mode 100644 src/myapp/myapp/web.config diff --git a/.github/workflows/deploy-model-manual.yml b/.github/workflows/deploy-model-manual.yml new file mode 100644 index 0000000..02c24cb --- /dev/null +++ b/.github/workflows/deploy-model-manual.yml @@ -0,0 +1,37 @@ +name: Deploy Model Manually #intrdouced for quick testing + +on: + pull_request_target: + types: [labeled] + +env: + workspace-name: aml-ws-mlops + resource-group: rg-mlops + job-name: amusing_jackal_9w9m3ls2b2 + model-name: azureml_amusing_jackal_9w9m3ls2b2_output_mlflow_log_model_583397521 + +permissions: + id-token: write + contents: read + +jobs: + + deploy-model: + name: Deploy Model + if: github.event.label.name == 'deploy-model' + runs-on: ubuntu-latest + environment: + name: prod + steps: + - name: Check out repo + uses: actions/checkout@main + - name: Install az ml extension + run: az extension add -n ml -y + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Trigger Azure ML job + run: az ml online-deployment create -f src/deploy-endpoint.yml --all-traffic --workspace-name ${{ env.workspace-name }} --resource-group ${{ env.resource-group }} \ No newline at end of file diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml new file mode 100644 index 0000000..d8f6899 --- /dev/null +++ b/.github/workflows/deploy-webapp.yml @@ -0,0 +1,40 @@ +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +env: + AZURE_WEBAPP_NAME: mlops-skaroti-appservice # set this to your web application's name + AZURE_WEBAPP_PACKAGE_PATH: 'src/web/myapp' # set this to the path to your web app project, defaults to the repository root + +jobs: + build: + environment: + name: 'prod' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install dependencies + run: | + pip install -r src/web/myapp/requirements.txt + - name: Deploy App + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: logout + run: | + az logout \ No newline at end of file diff --git a/.github/workflows/train-model.yml b/.github/workflows/train-model.yml index 03215d8..0777c22 100644 --- a/.github/workflows/train-model.yml +++ b/.github/workflows/train-model.yml @@ -1,8 +1,9 @@ name: Train Model on: - pull_request_target: - types: [labeled] + workflow_dispatch: + branches: + - main env: workspace-name: aml-ws-mlops diff --git a/requirements.txt b/requirements.txt index f67fe31..5c94e2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -pytest==7.4.0 +pytest==7.3.1 mlflow==2.6.0 -pandas==2.0.3 +pandas==2.0.1 sklearn==0.0 -scikit-learn==1.3.0 \ No newline at end of file +scikit-learn==1.3.0 +azureml-inference-server-http==1.2.2 \ No newline at end of file diff --git a/src/deploy-endpoint.yml b/src/deploy-endpoint.yml index b4aad16..ac3af49 100644 --- a/src/deploy-endpoint.yml +++ b/src/deploy-endpoint.yml @@ -1,6 +1,7 @@ $schema: https://azuremlschemas.azureedge.net/latest/managedOnlineDeployment.schema.json -name: mlflow-deployment +name: mlops-mlflow-predication-v3 endpoint_name: mlops-mlflow-endpoint-v1 -model: azureml:azureml_amusing_jackal_9w9m3ls2b2_output_mlflow_log_model_583397521:1 +model: azureml:azureml_polite_apple_qkbzxkd1kg_output_mlflow_log_model_299177315:1 instance_type: Standard_F4s_v2 -instance_count: 1 \ No newline at end of file +instance_count: 1 +environment: azureml:AzureML-sklearn-0.24-ubuntu18.04-py37-cpu@latest \ No newline at end of file diff --git a/src/infra/main.bicep b/src/infra/main.bicep new file mode 100644 index 0000000..97ec135 --- /dev/null +++ b/src/infra/main.bicep @@ -0,0 +1,76 @@ +param location string = resourceGroup().location +param keyVaultName string = 'amlwsmlokeyvault7251ed5b' + +@description('Reference an existing Key Vault') +resource kv 'Microsoft.KeyVault/vaults@2023-02-01' existing = { + name: keyVaultName +} + +@description('Create an App Service Plan with a Basic SKU') +resource exampleAppServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { + name: 'mlflow-ASP' + location: location + kind: 'Linux' + properties: { + reserved: true + } + sku: { + tier: 'Basic' + name: 'B1' + } +} + +@description('Create the App Service with a system-assigned identity') +resource exampleAppService 'Microsoft.Web/sites@2022-09-01' = { + name: 'mlops-skaroti-appservice' // must be globally unique + location: location + kind: 'linux' + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: exampleAppServicePlan.id + reserved: true + siteConfig: { + linuxFxVersion: 'PYTHON|3.11' + } + } +} + +@description('Configure the App Service with the Key Vault secrets') +resource exampleAppServiceConfig 'Microsoft.Web/sites/config@2022-09-01' = { + name: 'web' + parent: exampleAppService + properties: { + appSettings: [ + { + name: 'API_KEY' + value: '@Microsoft.KeyVault(SecretUri=https://${kv.name}.vault.azure.net/secrets/apiKey/)' + } + { + name: 'AZURE_ML_ENDPOINT_URL' + value: '@Microsoft.KeyVault(SecretUri=https://${kv.name}.vault.azure.net/secrets/endpointUrl/)' + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: '1' + } + ] + } +} + +@description('This is the built-in Key Vault Secrets User role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles') +resource keyVaultSecretsUser 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { + name: '4633458b-17de-408a-b874-0445c86b69e6' +} + +@description('Assign the Key Vault Secrets User role to the App Service') +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(kv.id, exampleAppService.id) + properties: { + principalId: exampleAppService.identity.principalId + roleDefinitionId: keyVaultSecretsUser.id + principalType: 'ServicePrincipal' + } + scope: kv +} diff --git a/src/job-prod.yml b/src/job-prod.yml index 51ebc73..d6896a3 100644 --- a/src/job-prod.yml +++ b/src/job-prod.yml @@ -10,6 +10,6 @@ inputs: path: azureml:diabetes-prod-folder:1 reg_rate: 0.01 environment: azureml:AzureML-sklearn-0.24-ubuntu18.04-py37-cpu@latest -compute: azureml:aml-ws-compute-ds11-v2 +compute: azureml:aml-ws-compute-f4s-v2 experiment_name: diabetes-classification-production description: Train a classification model to predict diabetes \ No newline at end of file diff --git a/src/myapp/myapp/__init__.py b/src/myapp/myapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/myapp/myapp/app.py b/src/myapp/myapp/app.py new file mode 100644 index 0000000..105e8e4 --- /dev/null +++ b/src/myapp/myapp/app.py @@ -0,0 +1,114 @@ +from flask import Flask, request, render_template +import json +import os +import ssl +import urllib.request + +app = Flask(__name__) + + +def allowSelfSignedHttps(allowed): + # bypass the server certificate verification on client side + if ( + allowed + and not os.environ.get("PYTHONHTTPSVERIFY", "") + and getattr(ssl, "_create_unverified_context", None) + ): + ssl._create_default_https_context = ssl._create_unverified_context + + +allowSelfSignedHttps(True) +# this line is needed if you use self-signed certificate +# in your scoring service. + + +@app.route("/", methods=["GET", "POST"]) +def index(): + if request.method == "POST": + # Get user input + patient_id = request.form["PatientID"] + pregnancies = request.form["Pregnancies"] + plasma_glucose = request.form["PlasmaGlucose"] + diastolic_blood_pressure = request.form["DiastolicBloodPressure"] + triceps_thickness = request.form["TricepsThickness"] + serum_insulin = request.form["SerumInsulin"] + bmi = request.form["BMI"] + diabetes_pedigree = request.form["DiabetesPedigree"] + age = request.form["Age"] + + # Format input data + input_data = { + "input_data": { + "columns": [ + "PatientID", + "Pregnancies", + "PlasmaGlucose", + "DiastolicBloodPressure", + "TricepsThickness", + "SerumInsulin", + "BMI", + "DiabetesPedigree", + "Age", + ], + "index": [0], + "data": [ + [ + patient_id, + pregnancies, + plasma_glucose, + diastolic_blood_pressure, + triceps_thickness, + serum_insulin, + bmi, + diabetes_pedigree, + age, + ] + ], + } + } + + body = str.encode(json.dumps(input_data)) + # Get the AZURE_ML_ENDPOINT_URL environment variable passed from App Service Application settings + url = os.environ["AZURE_ML_ENDPOINT_URL"] + # Get the API_KEY environment variable passed from App Service Application settings + api_key = os.environ["API_KEY"] + if not api_key: + raise Exception("A key should be provided to invoke the endpoint") + + # The azureml-model-deployment header will force the request + # to go to a specific deployment. + # Remove this header to have the request observe the endpoint + # traffic rules + headers = { + "Content-Type": "application/json", + "Authorization": ("Bearer " + api_key), + "azureml-model-deployment": "mlflow-deployment", + } + + req = urllib.request.Request(url, body, headers) + + try: + response = urllib.request.urlopen(req) + + result = json.loads(response.read()) + + # Display result + if result[0] == 1: + return render_template("result.html", result="The patient is diabetic.") + else: + return render_template( + "result.html", result="The patient is not diabetic." + ) + except urllib.error.HTTPError as error: + print("The request failed with status code: " + str(error.code)) + + # Print the headers - they include the requert ID and the + # timestamp, which are useful for debugging the failure + print(error.info()) + print(error.read().decode("utf8", "ignore")) + else: + return render_template("index.html") + + +if __name__ == "__main__": + app.run() \ No newline at end of file diff --git a/src/myapp/myapp/requirements.txt b/src/myapp/myapp/requirements.txt new file mode 100644 index 0000000..556e0d3 --- /dev/null +++ b/src/myapp/myapp/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.2.5 +requests==2.31.0 \ No newline at end of file diff --git a/src/myapp/myapp/templates/index.html b/src/myapp/myapp/templates/index.html new file mode 100644 index 0000000..6b58d91 --- /dev/null +++ b/src/myapp/myapp/templates/index.html @@ -0,0 +1,30 @@ + + + + Diabetes Prediction + + +

Diabetes Prediction

+
+
+

+
+

+
+

+
+

+
+

+
+

+
+

+
+

+
+

+ +
+ + \ No newline at end of file diff --git a/src/myapp/myapp/templates/result.html b/src/myapp/myapp/templates/result.html new file mode 100644 index 0000000..6974ce2 --- /dev/null +++ b/src/myapp/myapp/templates/result.html @@ -0,0 +1,10 @@ + + + + Diabetes Prediction Result + + +

Diabetes Prediction Result

+

{{ result }}

+ + \ No newline at end of file diff --git a/src/myapp/myapp/web.config b/src/myapp/myapp/web.config new file mode 100644 index 0000000..2c77c02 --- /dev/null +++ b/src/myapp/myapp/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file