From 675eb22f8a9cf1330c178b231a003654af2c9b8f Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Fri, 8 Nov 2019 18:44:06 -0800 Subject: [PATCH 01/59] build image pipeline --- .pipelines/azdo-ci-image.yml | 37 ++++ .pipelines/azdo-release-abtest-pipeline.yml | 209 ++++++++++++++++++++ docs/canary_ab_deployment.md | 75 +++++++ ml_service/util/create_scoring_image.py | 11 +- vss-extension.json | 38 ++++ 5 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 .pipelines/azdo-ci-image.yml create mode 100644 .pipelines/azdo-release-abtest-pipeline.yml create mode 100644 docs/canary_ab_deployment.md create mode 100644 vss-extension.json diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml new file mode 100644 index 00000000..51da32fb --- /dev/null +++ b/.pipelines/azdo-ci-image.yml @@ -0,0 +1,37 @@ +pr: none +trigger: + branches: + include: + - master + paths: + include: + - ml_service/util/create_scoring_image.py + - ml_service/util/Dockerfile + - code/scoring/ + exclude: + - code/scoring/deployment_config_aci.yml + - code/scoring/deployment_config_aks.yml + +pool: + vmImage: 'ubuntu-latest' + +container: mcr.microsoft.com/mlops/python:latest + +variables: +- group: devopsforai-aml-vg +- name: 'SCORE_SCRIPT' + value: 'score.py' + +name: $(Date:yyyyMMdd)$(Rev:r) +steps: + +- bash: | + # Invoke the Python building and publishing a training pipeline with Python on ML Compute + python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py + failOnStderr: 'false' + env: + SP_APP_SECRET: '$(SP_APP_SECRET)' + MODEL_VERSION: 1 + displayName: 'Create Scoring Image' + enabled: 'true' + diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml new file mode 100644 index 00000000..60e21b90 --- /dev/null +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -0,0 +1,209 @@ +variables: +- group: 'devopsforai-aml-vg' +- name: 'helmVersion' + value: 'v3.0.0-rc.3' +- name: 'imgTag' + value: 1 +- name: 'imgName' + value: amlabtest74c95585.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) +- name: 'kubernetesServiceConnection' + value: 'abtesting-cluster' +- name: 'blueReleaseName' + value: 'model-blue' +- name: 'greenReleaseName' + value: 'model-green' +- name: 'kubernetesNamespace' + value: 'abtesting' + +trigger: +- master + +stages: +- stage: 'Blue_Staging' + jobs: + - job: "Deploy_to_Staging" + timeoutInMinutes: 0 + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm && helm version + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + releaseName: $(blueReleaseName) + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' + install: true + arguments: --namespace $(kubernetesNamespace) + - publish: $(System.DefaultWorkingDirectory)/charts + artifact: allcharts + +- stage: 'Blue_50' + jobs: + - job: 'Blue_Rollout_50' + displayName: 50 50 rollout to blue environment + timeoutInMinutes: 0 + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + releaseName: 'abtest-istio' + overrideValues: 'weight.blue=50,weight.green=50' + install: true + arguments: --namespace $(kubernetesNamespace) + +- stage: 'Blue_100' + jobs: + - deployment: 'blue_Rollout_100' + timeoutInMinutes: 0 + environment: abtestenv + strategy: + runOnce: + deploy: + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' + releaseName: 'abtest-istio' + overrideValues: 'weight.blue=100,weight.green=0' + arguments: --namespace $(kubernetesNamespace) + +- stage: 'Rollback' + dependsOn: 'Blue_100' + condition: failed() + jobs: + - deployment: 'Roll_Back' + displayName: 'Roll Back after failure' + environment: abtestenv + strategy: + runOnce: + deploy: + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' + releaseName: 'abtest-istio' + overrideValues: 'weight.blue=0,weight.green=100' + arguments: --namespace $(kubernetesNamespace) + +- stage: 'Set_Production_Tag' + dependsOn: 'Blue_100' + condition: succeeded() + jobs: + - job: 'green_blue_tagging' + timeoutInMinutes: 0 + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + releaseName: $(greenReleaseName) + overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' + arguments: --namespace $(kubernetesNamespace) + +- stage: 'Green_100' + jobs: + - job: 'Prod_Rollout_100' + timeoutInMinutes: 0 + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: upgrade + chartType: FilePath + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + releaseName: 'abtest-istio' + overrideValues: 'weight.blue=0,weight.green=100' + arguments: --namespace $(kubernetesNamespace) + +- stage: 'Disable_blue' + condition: always() + jobs: + - job: 'blue_disable' + timeoutInMinutes: 0 + steps: + - task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - task: HelmDeploy@0 + displayName: 'helm uninstall blue' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(kubernetesServiceConnection) + command: delete + arguments: $(blueReleaseName) --namespace $(kubernetesNamespace) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md new file mode 100644 index 00000000..93e4bcbc --- /dev/null +++ b/docs/canary_ab_deployment.md @@ -0,0 +1,75 @@ +Model deployment to a Kubernetes cluster with Canary and A/B testing deployemnt strategies. + +If you target deployment environment is a K8s cluster and you want to implement Canary and/or A/B testing deployemnt [link to adf practice] strategies you can follow this sample guidance: + +The steps below + +1. Install Istio on a K8s cluster. +This guidance uses Istio service mesh implememtation to control traffic routing between model versions. The instruction on installing Istio is available [here](https://docs.microsoft.com/en-us/azure/aks/servicemesh-istio-install?pivots=client-operating-system-linux). + +Having the Istio installed, figure out the Istio gateway endpoint: + +GATEWAY_IP=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + + +2. Configure a pipeline to build a Scoring Image +Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) pipeline to build a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: + +{Picture} + + +3. Configure a Relese Pipeline. +Use [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) to configure a multistage release pipeline: + +{Picture} + +Make sure that the release pipeline is configured to be triggered once the scoring image build is completed: + +{Picture} + +Approvals!!!!! + +4. Build a new Scoring Image. + +Change the code in the [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) and merge it to the master branch. +It will trigger the entire the building pipeline and the release pipeline after that. + +The release pipeline deploys a new scoring image with the following stages implementing Canary deployment strategy: + +| Stage | Green Weight| Blue Weight| Description| +| ------------------- |-------------|------------|------------- +| Blue_0 |100 |0 |New image (blue) is deployed. +| | | |But all traffic (100%) is still routed to the old (green) image. +| ------------------- |-------------|------------|------------- +| Blue_50 |50 |50 |Traffic is split between old (green) and new (blue) images 50/50. +| ------------------- |-------------|------------|------------- +| Blue_100 |0 |100 |All traffic (100%) is routed to the blue image. +| ------------------- |-------------|------------|------------- +| Blue_Green |0 |100 |Old green image is removed. The new blue image is copied as green. +| | | |Blue and Green images are equal. +| | | |All traffic (100%) is routed to the blue image. +| ------------------- |-------------|------------|------------- +| Green_100 |100 |0 |All traffic (100%) is routed to the green image. +| | | |The blue image is removed + +At ecah stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with Postman or with 'curl': + +curl $GATEWAY_IP/score + +You can also emulate a simple load test on the gateway with the load_test.sh: + +./charts/load_test.sh $GATEWAY_IP 10 + +The command above sends 10 requests to the gateway. So, if the pipeline has completted stage Blue_50, the result will look like this: +. +.. +. +. +. + +Despite what blue/green weights are configured now on the cluster you can perform A/B testing and send requests directly to either blue or green images: + +./charts/load_test.sh $GATEWAY_IP 10 blue + +./charts/load_test.sh $GATEWAY_IP 10 green + diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 08ae49b5..63fcdec6 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -10,13 +10,14 @@ TENANT_ID = os.environ.get('TENANT_ID') APP_ID = os.environ.get('SP_APP_ID') APP_SECRET = os.environ.get('SP_APP_SECRET') -WORKSPACE_NAME = os.environ.get("BASE_NAME")+"-AML-WS" +WORKSPACE_NAME = "aml-abtest" SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("BASE_NAME")+"-AML-RG" +RESOURCE_GROUP = os.environ.get("BASE_NAME") MODEL_NAME = os.environ.get('MODEL_NAME') MODEL_VERSION = os.environ.get('MODEL_VERSION') IMAGE_NAME = os.environ.get('IMAGE_NAME') - +SCORE_SCRIPT = os.environ.get('SCORE_SCRIPT') +BUILD_NUMBER = os.environ.get('BUILD_BUILDNUMBER') SP_AUTH = ServicePrincipalAuthentication( tenant_id=TENANT_ID, @@ -35,7 +36,7 @@ os.chdir("./code/scoring") image_config = ContainerImage.image_configuration( - execution_script="score.py", + execution_script=SCORE_SCRIPT, runtime="python", conda_file="conda_dependencies.yml", description="Image with ridge regression model", @@ -43,7 +44,7 @@ ) image = Image.create( - name=IMAGE_NAME, models=[model], image_config=image_config, workspace=ws + name=IMAGE_NAME + "-" + BUILD_NUMBER, models=[model], image_config=image_config, workspace=ws ) image.wait_for_creation(show_output=True) diff --git a/vss-extension.json b/vss-extension.json new file mode 100644 index 00000000..2d54a5cc --- /dev/null +++ b/vss-extension.json @@ -0,0 +1,38 @@ +{ + "manifestVersion": 1, + "id": "SecurityPipelineDecorator", + "name": "Automatic Credential Scanning", + "version": "0.0.0.9", + "publisher": "CSE-DevOps-Team6", + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "description": "Organizational Pipeline Decorator to enforce credential scanning on all pipelines.", + "categories": [ + "Azure Pipelines" + ], + "icons": { + "default": "images/extension-icon.png" + }, + "contributions": [ + { + "id": "Credential-Scanning-Task", + "type": "ms.azure-pipelines.pipeline-decorator", + "targets": [ + "ms.azure-pipelines-agent-job.pre-job-tasks" + ], + "properties": { + "template": "securitydecorator/sec-decorator-filter.yml" + } + } + ], + "files": [ + { + "path": "securitydecorator/sec-decorator-filter.yml", + "addressable": true, + "contentType": "text/plain" + } + ] +} From 5172ab9fe3e3ba44aca7b20e7dbb0c89b1510398 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Fri, 8 Nov 2019 18:53:44 -0800 Subject: [PATCH 02/59] workspace name --- ml_service/util/create_scoring_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 63fcdec6..27176fef 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -10,7 +10,7 @@ TENANT_ID = os.environ.get('TENANT_ID') APP_ID = os.environ.get('SP_APP_ID') APP_SECRET = os.environ.get('SP_APP_SECRET') -WORKSPACE_NAME = "aml-abtest" +WORKSPACE_NAME = os.environ.get('AML_WORKSPACE_NAME') SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') RESOURCE_GROUP = os.environ.get("BASE_NAME") MODEL_NAME = os.environ.get('MODEL_NAME') From 115aade24c6778c675f38d0565a244b54e8eef18 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Fri, 8 Nov 2019 18:58:41 -0800 Subject: [PATCH 03/59] resource group name --- ml_service/util/create_scoring_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 27176fef..100deb23 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -12,7 +12,7 @@ APP_SECRET = os.environ.get('SP_APP_SECRET') WORKSPACE_NAME = os.environ.get('AML_WORKSPACE_NAME') SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("BASE_NAME") +RESOURCE_GROUP = os.environ.get("RESOURCE_GROUP_NAME") MODEL_NAME = os.environ.get('MODEL_NAME') MODEL_VERSION = os.environ.get('MODEL_VERSION') IMAGE_NAME = os.environ.get('IMAGE_NAME') From 206794892f38ad0f3f276fab667eda33b88a6de8 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 15:40:15 -0800 Subject: [PATCH 04/59] canary deployment --- code/scoring/scoreA.py | 2 ++ code/scoring/scoreB.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 code/scoring/scoreA.py create mode 100644 code/scoring/scoreB.py diff --git a/code/scoring/scoreA.py b/code/scoring/scoreA.py new file mode 100644 index 00000000..3c600b30 --- /dev/null +++ b/code/scoring/scoreA.py @@ -0,0 +1,2 @@ +def run(raw_data): + return "New Model A" \ No newline at end of file diff --git a/code/scoring/scoreB.py b/code/scoring/scoreB.py new file mode 100644 index 00000000..439b3c5a --- /dev/null +++ b/code/scoring/scoreB.py @@ -0,0 +1,2 @@ +def run(raw_data): + return "New Model B" \ No newline at end of file From d07f786c001aa921379d02462f87a1f74db99955 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 15:41:47 -0800 Subject: [PATCH 05/59] canary deployment --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index 51da32fb..95d020a0 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'score.py' + value: 'scoreA.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 92648cd69f15a6a31c79d1f6532463cb4090a557 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 17:32:31 -0800 Subject: [PATCH 06/59] canary deployment --- charts/abtest-istio/Chart.yaml | 5 ++ .../abtest-istio/templates/istio-canary.yaml | 60 +++++++++++++++++++ charts/abtest-istio/values.yaml | 15 +++++ charts/abtest-model/Chart.yaml | 5 ++ charts/abtest-model/templates/deployment.yaml | 37 ++++++++++++ charts/abtest-model/templates/service.yaml | 13 ++++ charts/abtest-model/values.yaml | 18 ++++++ docs/canary_ab_deployment.md | 16 ++--- 8 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 charts/abtest-istio/Chart.yaml create mode 100644 charts/abtest-istio/templates/istio-canary.yaml create mode 100644 charts/abtest-istio/values.yaml create mode 100644 charts/abtest-model/Chart.yaml create mode 100644 charts/abtest-model/templates/deployment.yaml create mode 100644 charts/abtest-model/templates/service.yaml create mode 100644 charts/abtest-model/values.yaml diff --git a/charts/abtest-istio/Chart.yaml b/charts/abtest-istio/Chart.yaml new file mode 100644 index 00000000..bfcf8584 --- /dev/null +++ b/charts/abtest-istio/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: abtest-istio +version: 0.1.0 diff --git a/charts/abtest-istio/templates/istio-canary.yaml b/charts/abtest-istio/templates/istio-canary.yaml new file mode 100644 index 00000000..a030fd0d --- /dev/null +++ b/charts/abtest-istio/templates/istio-canary.yaml @@ -0,0 +1,60 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: Gateway +metadata: + name: mlmodel-gateway + namespace: abtesting +spec: + selector: + istio: ingressgateway + servers: + - port: + number: {{ .Values.ingress.port }} + name: http + protocol: HTTP + hosts: + - "*" +--- +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: mlmodel-virtualservice + namespace: abtesting +spec: + gateways: + - mlmodel-gateway + hosts: + - '*' + http: + - match: + - uri: + prefix: /score + headers: + x-api-version: + exact: 'blue' + route: + - destination: + host: {{ .Values.svc.name }}-blue.abtesting.svc.cluster.local + port: + number: {{ .Values.svc.port }} + - match: + - uri: + prefix: /score + headers: + x-api-version: + exact: 'green' + route: + - destination: + host: {{ .Values.svc.name }}-green.abtesting.svc.cluster.local + port: + number: {{ .Values.svc.port }} + - route: + - destination: + host: {{ .Values.svc.name }}-green.abtesting.svc.cluster.local + port: + number: {{ .Values.svc.port }} + weight: {{ .Values.weight.green }} + - destination: + host: {{ .Values.svc.name }}-blue.abtesting.svc.cluster.local + port: + number: {{ .Values.svc.port }} + weight: {{ .Values.weight.blue }} \ No newline at end of file diff --git a/charts/abtest-istio/values.yaml b/charts/abtest-istio/values.yaml new file mode 100644 index 00000000..014845bc --- /dev/null +++ b/charts/abtest-istio/values.yaml @@ -0,0 +1,15 @@ +ingress: + port: 80 + +svc: + port: 5001 + name: model-svc + + +weight: + green: 50 + blue: 50 + +uri: + prefix: /score + diff --git a/charts/abtest-model/Chart.yaml b/charts/abtest-model/Chart.yaml new file mode 100644 index 00000000..eeaa24bf --- /dev/null +++ b/charts/abtest-model/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: abtest-model +version: 0.1.0 diff --git a/charts/abtest-model/templates/deployment.yaml b/charts/abtest-model/templates/deployment.yaml new file mode 100644 index 00000000..2860c534 --- /dev/null +++ b/charts/abtest-model/templates/deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.deployment.name }} + namespace: {{ .Values.namespace }} + labels: + app: {{ .Values.appname }} + model_version: {{ .Values.deployment.bluegreen }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ .Values.appname }} + model_version: {{ .Values.deployment.bluegreen }} + template: + metadata: + labels: + app: {{ .Values.appname }} + model_version: {{ .Values.deployment.bluegreen }} + spec: + containers: + - name: {{ .Values.deployment.container.name }} + image: "{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}" + imagePullPolicy: Always + ports: + - name: http + containerPort: 5001 + - name: probe + containerPort: 8086 + env: + - name: APPLICATIONINSIGHTS_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: app-insight-secret + key: InstrumentationKey + imagePullSecrets: + - name: aks-secret diff --git a/charts/abtest-model/templates/service.yaml b/charts/abtest-model/templates/service.yaml new file mode 100644 index 00000000..a4a6ed8b --- /dev/null +++ b/charts/abtest-model/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Values.svc.name }}-{{ .Values.deployment.bluegreen }}" + namespace: {{ .Values.namespace }} +spec: + selector: + app: {{ .Values.appname }} + model_version: {{ .Values.deployment.bluegreen }} + ports: + - port: {{ .Values.svc.port }} + targetPort: {{ .Values.deployment.container.port }} + \ No newline at end of file diff --git a/charts/abtest-model/values.yaml b/charts/abtest-model/values.yaml new file mode 100644 index 00000000..3a824439 --- /dev/null +++ b/charts/abtest-model/values.yaml @@ -0,0 +1,18 @@ +namespace: abtesting +appname: model + +deployment: + name: model-green + bluegreen: green + container: + name: model + port: 5001 + image: + name: amlabtest74c95585.azurecr.io/abtestimg + tag: 2 + +svc: + name: model-svc + port: 5001 + +initialDeployment: false \ No newline at end of file diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 93e4bcbc..2cdec921 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -1,8 +1,8 @@ Model deployment to a Kubernetes cluster with Canary and A/B testing deployemnt strategies. -If you target deployment environment is a K8s cluster and you want to implement Canary and/or A/B testing deployemnt [link to adf practice] strategies you can follow this sample guidance: +If you target deployment environment is a K8s cluster and you want to implement [Canary and/or A/B testing deployemnt strategies](http://adfpractice-fedor.blogspot.com/2019/04/deployment-strategies-with-kubernetes.html) you can follow this sample guidance: -The steps below +**Note:** It is assumed that tou have an AKS instance and configured ***kubectl*** to communicate with the cluster. 1. Install Istio on a K8s cluster. This guidance uses Istio service mesh implememtation to control traffic routing between model versions. The instruction on installing Istio is available [here](https://docs.microsoft.com/en-us/azure/aks/servicemesh-istio-install?pivots=client-operating-system-linux). @@ -13,11 +13,9 @@ GATEWAY_IP=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{ 2. Configure a pipeline to build a Scoring Image -Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) pipeline to build a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: - +Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to create a pipeline building a scoring image. {Picture} - 3. Configure a Relese Pipeline. Use [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) to configure a multistage release pipeline: @@ -29,10 +27,14 @@ Make sure that the release pipeline is configured to be triggered once the scori Approvals!!!!! +Manually run a pipeline builing scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: + +The release pipeline will be triggered automatically and it will deploy the scroring imnage to the Kubernetes cluster. + 4. Build a new Scoring Image. -Change the code in the [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) and merge it to the master branch. -It will trigger the entire the building pipeline and the release pipeline after that. +Change the code in the [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) and merge it to the master branch. +It will trigger the building pipeline and the release pipeline after that. The release pipeline deploys a new scoring image with the following stages implementing Canary deployment strategy: From d586f05eb635799663a23eb1ae0981ccb5b2f230 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 18:04:21 -0800 Subject: [PATCH 07/59] Canary deployment --- .pipelines/azdo-release-abtest-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 60e21b90..60b5807a 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -5,7 +5,7 @@ variables: - name: 'imgTag' value: 1 - name: 'imgName' - value: amlabtest74c95585.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) + value: myuniquemlamlcr.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) - name: 'kubernetesServiceConnection' value: 'abtesting-cluster' - name: 'blueReleaseName' From f094763516e2dfdd39efdfd6be1a5c9ea37210a9 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 18:28:00 -0800 Subject: [PATCH 08/59] Canary deployment --- code/scoring/conda_dependencies.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/code/scoring/conda_dependencies.yml b/code/scoring/conda_dependencies.yml index f13c3c3d..ad11a936 100644 --- a/code/scoring/conda_dependencies.yml +++ b/code/scoring/conda_dependencies.yml @@ -33,22 +33,21 @@ dependencies: # Currently Azure ML Workbench only supports 3.5.2 and later. -- python=3.6.2 +- python=3.7.5 # Required by azureml-defaults, installed separately through Conda to # get a prebuilt version and not require build tools for the install. -- psutil=5.3 +- psutil=5.6 #latest - pip: # Required packages for AzureML execution, history, and data preparation. - - azureml-sdk[notebooks] # add the version to lock it ==0.1.74 - - scipy==1.0.0 - - scikit-learn==0.21.3 - - pandas==0.23.1 - - numpy==1.14.5 - - joblib==0.13.2 - - gunicorn==19.9.0 - - flask==1.1.1 - - azure-ml-api-sdk - + - azureml-sdk==1.0.72 #1.0.72 + - scipy==1.3.1 #1.3.1 + - scikit-learn==0.21.3 #latest + - pandas==0.25.3 #0.25.3 + - numpy==1.17.3 #1.17.3 + - joblib==0.14.0 #0.14.0 + - gunicorn==19.9.0 #latest + - flask==1.1.1 #latest + From a49a67bf70dca4fd2905506502f4a2c551672391 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 18:51:26 -0800 Subject: [PATCH 09/59] Canary deployment --- charts/abtest-model/templates/deployment.yaml | 6 ------ code/scoring/deployment_config_aks.yml | 2 +- code/scoring/scoreA.py | 14 +++++++++++++- code/scoring/scoreB.py | 14 +++++++++++++- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/charts/abtest-model/templates/deployment.yaml b/charts/abtest-model/templates/deployment.yaml index 2860c534..6b64e8b2 100644 --- a/charts/abtest-model/templates/deployment.yaml +++ b/charts/abtest-model/templates/deployment.yaml @@ -27,11 +27,5 @@ spec: containerPort: 5001 - name: probe containerPort: 8086 - env: - - name: APPLICATIONINSIGHTS_CONNECTION_STRING - valueFrom: - secretKeyRef: - name: app-insight-secret - key: InstrumentationKey imagePullSecrets: - name: aks-secret diff --git a/code/scoring/deployment_config_aks.yml b/code/scoring/deployment_config_aks.yml index 1299dc9d..6aeaf89e 100644 --- a/code/scoring/deployment_config_aks.yml +++ b/code/scoring/deployment_config_aks.yml @@ -9,7 +9,7 @@ authEnabled: True containerResourceRequirements: cpu: 1 memoryInGB: 4 -appInsightsEnabled: True +appInsightsEnabled: false scoringTimeoutMs: 5000 maxConcurrentRequestsPerContainer: 2 maxQueueWaitMs: 5000 diff --git a/code/scoring/scoreA.py b/code/scoring/scoreA.py index 3c600b30..7f2ba061 100644 --- a/code/scoring/scoreA.py +++ b/code/scoring/scoreA.py @@ -1,2 +1,14 @@ +import json +import numpy +from azureml.core.model import Model +import joblib + + +def init(): + global model + + + def run(raw_data): - return "New Model A" \ No newline at end of file + return "New Model A" + diff --git a/code/scoring/scoreB.py b/code/scoring/scoreB.py index 439b3c5a..a0225204 100644 --- a/code/scoring/scoreB.py +++ b/code/scoring/scoreB.py @@ -1,2 +1,14 @@ +import json +import numpy +from azureml.core.model import Model +import joblib + + +def init(): + global model + + + def run(raw_data): - return "New Model B" \ No newline at end of file + return "New Model B" + From a5365338ee8d99993cdc5e4d7cd3d936205b8432 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Thu, 14 Nov 2019 19:06:14 -0800 Subject: [PATCH 10/59] Canary deployment --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index 95d020a0..b7b4fb25 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreA.py' + value: 'scoreB.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From f7ec2e05abfeb9a5e957573a3e0c7c05ed94df34 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 14:52:08 -0800 Subject: [PATCH 11/59] workspace name --- ml_service/util/create_scoring_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 100deb23..c20b4411 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -10,9 +10,9 @@ TENANT_ID = os.environ.get('TENANT_ID') APP_ID = os.environ.get('SP_APP_ID') APP_SECRET = os.environ.get('SP_APP_SECRET') -WORKSPACE_NAME = os.environ.get('AML_WORKSPACE_NAME') +WORKSPACE_NAME = os.environ.get('WORKSPACE_NAME') SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("RESOURCE_GROUP_NAME") +RESOURCE_GROUP = os.environ.get("RESOURCE_GROUP") MODEL_NAME = os.environ.get('MODEL_NAME') MODEL_VERSION = os.environ.get('MODEL_VERSION') IMAGE_NAME = os.environ.get('IMAGE_NAME') From 487a44dde87bcfc3fe15689871461cb847aca6a9 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 15:28:20 -0800 Subject: [PATCH 12/59] imgname --- .pipelines/azdo-release-abtest-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 60b5807a..b06557f2 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -5,7 +5,7 @@ variables: - name: 'imgTag' value: 1 - name: 'imgName' - value: myuniquemlamlcr.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) + value: mlopspyciamlcr.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) - name: 'kubernetesServiceConnection' value: 'abtesting-cluster' - name: 'blueReleaseName' From e89d851d297455562e36393e6d8223ac6fb5f447 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 15:43:09 -0800 Subject: [PATCH 13/59] start --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index b7b4fb25..95d020a0 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreB.py' + value: 'scoreA.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 3c10b738e8ce6f2bcf1432189b2135ae63a8de3d Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 16:15:04 -0800 Subject: [PATCH 14/59] Score B --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index 95d020a0..b7b4fb25 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreA.py' + value: 'scoreB.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 99d7f26d8a851b5af4690f7cb2cd0f6f462f6537 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 16:34:22 -0800 Subject: [PATCH 15/59] ScoreB --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index b7b4fb25..95d020a0 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreB.py' + value: 'scoreA.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From a4b59e805287f99e282fcd1a63133f691609acc9 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 16:46:49 -0800 Subject: [PATCH 16/59] load test script --- charts/load_test.sh | 8 ++++++++ docs/canary_ab_deployment.md | 13 ++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) create mode 100755 charts/load_test.sh diff --git a/charts/load_test.sh b/charts/load_test.sh new file mode 100755 index 00000000..25a06452 --- /dev/null +++ b/charts/load_test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +for ((i=1;i<=$1;i++)) +do + curl --header "x-api-version: $3" $2 + echo + sleep .2 +done \ No newline at end of file diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 2cdec921..af335909 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -25,11 +25,12 @@ Make sure that the release pipeline is configured to be triggered once the scori {Picture} -Approvals!!!!! +Check variables (aka image_name, model_name,)!!!!! +Kubernetes Service Connection Manually run a pipeline builing scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: -The release pipeline will be triggered automatically and it will deploy the scroring imnage to the Kubernetes cluster. +The release pipeline will be triggered automatically and it will deploy the scroring image to the Kubernetes cluster. 4. Build a new Scoring Image. @@ -60,7 +61,9 @@ curl $GATEWAY_IP/score You can also emulate a simple load test on the gateway with the load_test.sh: -./charts/load_test.sh $GATEWAY_IP 10 +./charts/load_test.sh 10 + +Add Kiali!!! The command above sends 10 requests to the gateway. So, if the pipeline has completted stage Blue_50, the result will look like this: . @@ -71,7 +74,7 @@ The command above sends 10 requests to the gateway. So, if the pipeline has comp Despite what blue/green weights are configured now on the cluster you can perform A/B testing and send requests directly to either blue or green images: -./charts/load_test.sh $GATEWAY_IP 10 blue +./charts/load_test.sh 10 $GATEWAY_IP/score blue -./charts/load_test.sh $GATEWAY_IP 10 green +./charts/load_test.sh 10 $GATEWAY_IP/score green From 0763c03bb52df98aa762a8a7ce332593e21f5b35 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 17:22:36 -0800 Subject: [PATCH 17/59] yml refactor --- .pipelines/azdo-helm-install.yml | 9 + .pipelines/azdo-helm-upgrade.yml | 19 ++ .pipelines/azdo-release-abtest-pipeline.yml | 271 +++++++++++--------- 3 files changed, 178 insertions(+), 121 deletions(-) create mode 100644 .pipelines/azdo-helm-install.yml create mode 100644 .pipelines/azdo-helm-upgrade.yml diff --git a/.pipelines/azdo-helm-install.yml b/.pipelines/azdo-helm-install.yml new file mode 100644 index 00000000..21b3c0fe --- /dev/null +++ b/.pipelines/azdo-helm-install.yml @@ -0,0 +1,9 @@ +steps: +- task: Bash@3 + displayName: 'Install Helm $(helmVersion)' + inputs: + targetType: inline + script: wget -q $helmDownloadURL -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + env: + HELM_VERSION: $(helmVersion) + FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz diff --git a/.pipelines/azdo-helm-upgrade.yml b/.pipelines/azdo-helm-upgrade.yml new file mode 100644 index 00000000..d5488a50 --- /dev/null +++ b/.pipelines/azdo-helm-upgrade.yml @@ -0,0 +1,19 @@ +parameters: + chartPath: '' + releaseName: '' + overrideValues: '' + +steps: +- template: azdo-heln-install.yml +- task: HelmDeploy@0 + displayName: 'helm upgrade' + inputs: + connectionType: 'Kubernetes Service Connection' + kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + command: upgrade + chartType: FilePath + chartPath: ${{ parameters.chartPath }} + releaseName: ${{ parameters.releaseName }} + overrideValues: ${{ parameters.overrideValues }} + install: true + arguments: --namespace $(K8S_AB_NAMESPACE) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index b06557f2..b5eda4e7 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -2,18 +2,17 @@ variables: - group: 'devopsforai-aml-vg' - name: 'helmVersion' value: 'v3.0.0-rc.3' +- name: 'helmDownloadURL' + value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' - name: 'imgTag' value: 1 - name: 'imgName' - value: mlopspyciamlcr.azurecr.io/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) -- name: 'kubernetesServiceConnection' - value: 'abtesting-cluster' + value: $(IMAGE_REPO_NAME)/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) - name: 'blueReleaseName' value: 'model-blue' - name: 'greenReleaseName' value: 'model-green' -- name: 'kubernetesNamespace' - value: 'abtesting' + trigger: - master @@ -24,26 +23,33 @@ stages: - job: "Deploy_to_Staging" timeoutInMinutes: 0 steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm && helm version - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath + - template: azdo-helm-upgrade.yml + paramters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - releaseName: $(blueReleaseName) + releaseName: $(blueReleaseName) overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - install: true - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm && helm version + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + # releaseName: $(blueReleaseName) + # overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' + # install: true + # arguments: --namespace $(K8S_AB_NAMESPACE) + - publish: $(System.DefaultWorkingDirectory)/charts artifact: allcharts @@ -53,26 +59,32 @@ stages: displayName: 50 50 rollout to blue environment timeoutInMinutes: 0 steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath + - template: azdo-helm-upgrade.yml + paramters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' - releaseName: 'abtest-istio' + releaseName: 'abtest-istio' overrideValues: 'weight.blue=50,weight.green=50' - install: true - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + # releaseName: 'abtest-istio' + # overrideValues: 'weight.blue=50,weight.green=50' + # install: true + # arguments: --namespace $(K8S_AB_NAMESPACE) - stage: 'Blue_100' jobs: @@ -83,25 +95,31 @@ stages: runOnce: deploy: steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath - chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' - releaseName: 'abtest-istio' + - template: azdo-helm-upgrade.yml + paramters: + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + releaseName: 'abtest-istio' overrideValues: 'weight.blue=100,weight.green=0' - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' + # releaseName: 'abtest-istio' + # overrideValues: 'weight.blue=100,weight.green=0' + # arguments: --namespace $(K8S_AB_NAMESPACE) - stage: 'Rollback' dependsOn: 'Blue_100' @@ -114,25 +132,31 @@ stages: runOnce: deploy: steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath - chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' - releaseName: 'abtest-istio' + - template: azdo-helm-upgrade.yml + paramters: + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' + # releaseName: 'abtest-istio' + # overrideValues: 'weight.blue=0,weight.green=100' + # arguments: --namespace $(K8S_AB_NAMESPACE) - stage: 'Set_Production_Tag' dependsOn: 'Blue_100' @@ -141,50 +165,62 @@ stages: - job: 'green_blue_tagging' timeoutInMinutes: 0 steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath + - template: azdo-helm-upgrade.yml + paramters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - releaseName: $(greenReleaseName) + releaseName: $(greenReleaseName) overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + # releaseName: $(greenReleaseName) + # overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' + # arguments: --namespace $(K8S_AB_NAMESPACE) - stage: 'Green_100' jobs: - job: 'Prod_Rollout_100' timeoutInMinutes: 0 steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - - task: HelmDeploy@0 - displayName: 'helm upgrade' - inputs: - connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) - command: upgrade - chartType: FilePath + - template: azdo-helm-upgrade.yml + paramters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' - releaseName: 'abtest-istio' + releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' - arguments: --namespace $(kubernetesNamespace) + + # - task: Bash@3 + # displayName: 'Install Helm $(helmVersion)' + # inputs: + # targetType: inline + # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + # env: + # HELM_VERSION: $(helmVersion) + # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + # - task: HelmDeploy@0 + # displayName: 'helm upgrade' + # inputs: + # connectionType: 'Kubernetes Service Connection' + # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) + # command: upgrade + # chartType: FilePath + # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + # releaseName: 'abtest-istio' + # overrideValues: 'weight.blue=0,weight.green=100' + # arguments: --namespace $(K8S_AB_NAMESPACE) - stage: 'Disable_blue' condition: always() @@ -192,18 +228,11 @@ stages: - job: 'blue_disable' timeoutInMinutes: 0 steps: - - task: Bash@3 - displayName: 'Install Helm $(helmVersion)' - inputs: - targetType: inline - script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - env: - HELM_VERSION: $(helmVersion) - FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz + - template: azdo-helm-install.yml - task: HelmDeploy@0 displayName: 'helm uninstall blue' inputs: connectionType: 'Kubernetes Service Connection' - kubernetesServiceConnection: $(kubernetesServiceConnection) + kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) command: delete - arguments: $(blueReleaseName) --namespace $(kubernetesNamespace) + arguments: $(blueReleaseName) --namespace $(K8S_AB_NAMESPACE) From 7d9431550ed2b4232d26f88a931cd16052aa3b8c Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 17:34:38 -0800 Subject: [PATCH 18/59] typo --- .pipelines/azdo-release-abtest-pipeline.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index b5eda4e7..65ef59f8 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -24,7 +24,7 @@ stages: timeoutInMinutes: 0 steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' releaseName: $(blueReleaseName) overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' @@ -60,7 +60,7 @@ stages: timeoutInMinutes: 0 steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=50,weight.green=50' @@ -96,7 +96,7 @@ stages: deploy: steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=100,weight.green=0' @@ -133,7 +133,7 @@ stages: deploy: steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' @@ -166,7 +166,7 @@ stages: timeoutInMinutes: 0 steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' releaseName: $(greenReleaseName) overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' @@ -197,7 +197,7 @@ stages: timeoutInMinutes: 0 steps: - template: azdo-helm-upgrade.yml - paramters: + parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' From cdcdd87e3a1610a098ca74c1297327eebd968025 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 17:35:23 -0800 Subject: [PATCH 19/59] typo --- .pipelines/azdo-helm-upgrade.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-helm-upgrade.yml b/.pipelines/azdo-helm-upgrade.yml index d5488a50..949ddde7 100644 --- a/.pipelines/azdo-helm-upgrade.yml +++ b/.pipelines/azdo-helm-upgrade.yml @@ -4,7 +4,7 @@ parameters: overrideValues: '' steps: -- template: azdo-heln-install.yml +- template: azdo-helm-install.yml - task: HelmDeploy@0 displayName: 'helm upgrade' inputs: From d2d183adf395d47b11e43ed28c7160112ec67a43 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 17:36:22 -0800 Subject: [PATCH 20/59] ScoreB --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index 95d020a0..b7b4fb25 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreA.py' + value: 'scoreB.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 4768ed0e50d4951b561bd1eb7d445edab95c43ee Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 17:46:24 -0800 Subject: [PATCH 21/59] typo --- .pipelines/azdo-helm-install.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-helm-install.yml b/.pipelines/azdo-helm-install.yml index 21b3c0fe..fc5eac6f 100644 --- a/.pipelines/azdo-helm-install.yml +++ b/.pipelines/azdo-helm-install.yml @@ -3,7 +3,7 @@ steps: displayName: 'Install Helm $(helmVersion)' inputs: targetType: inline - script: wget -q $helmDownloadURL -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm + script: wget -q $(helmDownloadURL) -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm env: HELM_VERSION: $(helmVersion) FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz From 8981039efe665c71d7edecfd9635b1564cbeb6e8 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Mon, 9 Dec 2019 18:05:57 -0800 Subject: [PATCH 22/59] wrong path --- .pipelines/azdo-release-abtest-pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 65ef59f8..6026bb42 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -97,7 +97,7 @@ stages: steps: - template: azdo-helm-upgrade.yml parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=100,weight.green=0' @@ -134,7 +134,7 @@ stages: steps: - template: azdo-helm-upgrade.yml parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' From fcd67c25615ad729f2220c786aef22b5c736f8bf Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 09:41:20 -0800 Subject: [PATCH 23/59] scoreA --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index b7b4fb25..95d020a0 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreB.py' + value: 'scoreA.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 8eacbc3056d3994589d47bc2517f25b58d13d020 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 11:38:01 -0800 Subject: [PATCH 24/59] docs update --- .pipelines/azdo-ci-image.yml | 1 - .pipelines/azdo-release-abtest-pipeline.yml | 122 ------------ docs/canary_ab_deployment.md | 201 +++++++++++++++----- docs/images/canary-deployment-trigger.png | Bin 0 -> 61799 bytes docs/images/scoring_image.png | Bin 0 -> 91162 bytes 5 files changed, 149 insertions(+), 175 deletions(-) create mode 100644 docs/images/canary-deployment-trigger.png create mode 100644 docs/images/scoring_image.png diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index 95d020a0..d7824fa2 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -26,7 +26,6 @@ name: $(Date:yyyyMMdd)$(Rev:r) steps: - bash: | - # Invoke the Python building and publishing a training pipeline with Python on ML Compute python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py failOnStderr: 'false' env: diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 6026bb42..34ea49d7 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -29,27 +29,6 @@ stages: releaseName: $(blueReleaseName) overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm && helm version - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - # releaseName: $(blueReleaseName) - # overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - # install: true - # arguments: --namespace $(K8S_AB_NAMESPACE) - - publish: $(System.DefaultWorkingDirectory)/charts artifact: allcharts @@ -65,27 +44,6 @@ stages: releaseName: 'abtest-istio' overrideValues: 'weight.blue=50,weight.green=50' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' - # releaseName: 'abtest-istio' - # overrideValues: 'weight.blue=50,weight.green=50' - # install: true - # arguments: --namespace $(K8S_AB_NAMESPACE) - - stage: 'Blue_100' jobs: - deployment: 'blue_Rollout_100' @@ -101,26 +59,6 @@ stages: releaseName: 'abtest-istio' overrideValues: 'weight.blue=100,weight.green=0' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' - # releaseName: 'abtest-istio' - # overrideValues: 'weight.blue=100,weight.green=0' - # arguments: --namespace $(K8S_AB_NAMESPACE) - - stage: 'Rollback' dependsOn: 'Blue_100' condition: failed() @@ -138,26 +76,6 @@ stages: releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(Pipeline.Workspace)/allcharts/abtest-istio' - # releaseName: 'abtest-istio' - # overrideValues: 'weight.blue=0,weight.green=100' - # arguments: --namespace $(K8S_AB_NAMESPACE) - - stage: 'Set_Production_Tag' dependsOn: 'Blue_100' condition: succeeded() @@ -171,26 +89,6 @@ stages: releaseName: $(greenReleaseName) overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - # releaseName: $(greenReleaseName) - # overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' - # arguments: --namespace $(K8S_AB_NAMESPACE) - - stage: 'Green_100' jobs: - job: 'Prod_Rollout_100' @@ -202,26 +100,6 @@ stages: releaseName: 'abtest-istio' overrideValues: 'weight.blue=0,weight.green=100' - # - task: Bash@3 - # displayName: 'Install Helm $(helmVersion)' - # inputs: - # targetType: inline - # script: wget -q https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz -O /tmp/$FILENAME && tar -zxvf /tmp/$FILENAME -C /tmp && sudo mv /tmp/linux-amd64/helm /usr/local/bin/helm - # env: - # HELM_VERSION: $(helmVersion) - # FILENAME: helm-$(helmVersion)-linux-amd64.tar.gz - # - task: HelmDeploy@0 - # displayName: 'helm upgrade' - # inputs: - # connectionType: 'Kubernetes Service Connection' - # kubernetesServiceConnection: $(K8S_AB_SERVICE_CONNECTION) - # command: upgrade - # chartType: FilePath - # chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-istio' - # releaseName: 'abtest-istio' - # overrideValues: 'weight.blue=0,weight.green=100' - # arguments: --namespace $(K8S_AB_NAMESPACE) - - stage: 'Disable_blue' condition: always() jobs: diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index af335909..6c60b028 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -1,80 +1,177 @@ -Model deployment to a Kubernetes cluster with Canary and A/B testing deployemnt strategies. +## Model deployment to a Kubernetes cluster with Canary and A/B testing deployemnt strategies. -If you target deployment environment is a K8s cluster and you want to implement [Canary and/or A/B testing deployemnt strategies](http://adfpractice-fedor.blogspot.com/2019/04/deployment-strategies-with-kubernetes.html) you can follow this sample guidance: +If your target deployment environment is a K8s cluster and you want to implement [Canary and/or A/B testing deployemnt strategies](http://adfpractice-fedor.blogspot.com/2019/04/deployment-strategies-with-kubernetes.html) you can follow this sample guidance. -**Note:** It is assumed that tou have an AKS instance and configured ***kubectl*** to communicate with the cluster. +**Note:** It is assumed that you have an AKS instance and configured ***kubectl*** to communicate with the cluster. -1. Install Istio on a K8s cluster. -This guidance uses Istio service mesh implememtation to control traffic routing between model versions. The instruction on installing Istio is available [here](https://docs.microsoft.com/en-us/azure/aks/servicemesh-istio-install?pivots=client-operating-system-linux). +#### 1. Install Istio on a K8s cluster. -Having the Istio installed, figure out the Istio gateway endpoint: +This guidance uses [Istio](https://istio.io) service mesh implememtation to control traffic routing between model versions. The instruction on installing Istio is available [here](https://docs.microsoft.com/en-us/azure/aks/servicemesh-istio-install?pivots=client-operating-system-linux). +Having the Istio installed, figure out the Istio gateway endpoint on your K8s cluster: + +```bash GATEWAY_IP=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` +#### 2. Set up variables + +There are some extra variables that you need to setup in ***devopsforai-aml-vg*** variable group: + +| Variable Name | Suggested Value | +| --------------------------- | -----------------------------------------------------| +| K8S_AB_SERVICE_CONNECTION | AzDo service connection to a K8s cluster | +| K8S_AB_NAMESPACE | Namespace in a K8s cluster to deploy the model | +| IMAGE_REPO_NAME | Image reposiory name (e.g. mlopspyciamlcr.azurecr.io)| +| IMAGE_NAME | Scoring image name (e.g. myscoring) | +| MODEL_NAME | Name of the registered in AML model to be deployed | +| MODEL_VERSION | Version of the registered in AML model to be deployed| -2. Configure a pipeline to build a Scoring Image -Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to create a pipeline building a scoring image. -{Picture} -3. Configure a Relese Pipeline. -Use [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) to configure a multistage release pipeline: +#### 3. Configure a pipeline to build a Scoring Image -{Picture} +Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to create a pipeline building a scoring image. +```yaml +pr: none +trigger: + branches: + include: + - master + paths: + include: + - ml_service/util/create_scoring_image.py + - ml_service/util/Dockerfile + - code/scoring/ + exclude: + - code/scoring/deployment_config_aci.yml + - code/scoring/deployment_config_aks.yml + +pool: + vmImage: 'ubuntu-latest' + +container: mcr.microsoft.com/mlops/python:latest + +variables: +- group: devopsforai-aml-vg +- name: 'SCORE_SCRIPT' + value: 'scoreA.py' + +name: $(Date:yyyyMMdd)$(Rev:r) +steps: + +- bash: | + python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py + failOnStderr: 'false' + env: + SP_APP_SECRET: '$(SP_APP_SECRET)' + MODEL_VERSION: 1 + displayName: 'Create Scoring Image' + enabled: 'true' +``` + +#### 4. Configure a deployment pipeline. + +Use [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) to configure a multistage deployment pipeline: + +```yaml +variables: +- group: 'devopsforai-aml-vg' +- name: 'helmVersion' + value: 'v3.0.0-rc.3' +- name: 'helmDownloadURL' + value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' +- name: 'imgTag' + value: 1 +- name: 'imgName' + value: $(IMAGE_REPO_NAME)/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) +- name: 'blueReleaseName' + value: 'model-blue' +- name: 'greenReleaseName' + value: 'model-green' + + +trigger: +- master + +stages: +- stage: 'Blue_Staging' + jobs: + - job: "Deploy_to_Staging" + timeoutInMinutes: 0 + steps: + - template: azdo-helm-upgrade.yml + parameters: + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + releaseName: $(blueReleaseName) + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' + + - publish: $(System.DefaultWorkingDirectory)/charts + artifact: allcharts +... +``` Make sure that the release pipeline is configured to be triggered once the scoring image build is completed: -{Picture} +![canary deployment trigger](./images/canary-deployment-trigger.png) -Check variables (aka image_name, model_name,)!!!!! -Kubernetes Service Connection -Manually run a pipeline builing scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: +Manually run a pipeline building a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: -The release pipeline will be triggered automatically and it will deploy the scroring image to the Kubernetes cluster. +![scoring image](./images/scoring_image.png) -4. Build a new Scoring Image. +The release pipeline will be triggered automatically and it will deploy the scroring image to the Kubernetes cluster. -Change the code in the [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) and merge it to the master branch. -It will trigger the building pipeline and the release pipeline after that. +```bash +kubectl get deployments --namespace abtesting +NAME READY UP-TO-DATE AVAILABLE AGE +model-green 1/1 1 1 19h +``` -The release pipeline deploys a new scoring image with the following stages implementing Canary deployment strategy: - -| Stage | Green Weight| Blue Weight| Description| -| ------------------- |-------------|------------|------------- -| Blue_0 |100 |0 |New image (blue) is deployed. -| | | |But all traffic (100%) is still routed to the old (green) image. -| ------------------- |-------------|------------|------------- -| Blue_50 |50 |50 |Traffic is split between old (green) and new (blue) images 50/50. -| ------------------- |-------------|------------|------------- -| Blue_100 |0 |100 |All traffic (100%) is routed to the blue image. -| ------------------- |-------------|------------|------------- -| Blue_Green |0 |100 |Old green image is removed. The new blue image is copied as green. -| | | |Blue and Green images are equal. -| | | |All traffic (100%) is routed to the blue image. -| ------------------- |-------------|------------|------------- -| Green_100 |100 |0 |All traffic (100%) is routed to the green image. -| | | |The blue image is removed - -At ecah stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with Postman or with 'curl': +#### 5. Build a new Scoring Image. -curl $GATEWAY_IP/score +Change value of the ***SCORE_SCRIPT*** variable in the [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to point to ***scoreA.py*** and merge it to the master branch. +It will trigger the building pipeline and the release pipeline after that. -You can also emulate a simple load test on the gateway with the load_test.sh: +The release pipeline deploys a new scoring image with the following stages implementing ***Canary*** deployment strategy: -./charts/load_test.sh 10 +| Stage | Green Weight| Blue Weight| Description | +| ------------------- |-------------|------------|-----------------------------------------------------------------| +| Blue_0 |100 |0 |New image (blue) is deployed.
But all traffic (100%) is still routed to the old (green) image.| +| Blue_50 |50 |50 |Traffic is split between old (green) and new (blue) images 50/50.| +| Blue_100 |0 |100 |All traffic (100%) is routed to the blue image.| +| Blue_Green |0 |100 |Old green image is removed. The new blue image is copied as green.
Blue and Green images are equal.
All traffic (100%) is routed to the blue image.| +| Green_100 |100 |0 |All traffic (100%) is routed to the green image.
The blue image is removed -Add Kiali!!! +At ecah stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with ***Postman*** or with ***curl***: -The command above sends 10 requests to the gateway. So, if the pipeline has completted stage Blue_50, the result will look like this: -. -.. -. -. -. +```bash +curl $GATEWAY_IP/score +``` -Despite what blue/green weights are configured now on the cluster you can perform A/B testing and send requests directly to either blue or green images: +You can also emulate a simple load test on the gateway with the ***load_test.sh***: +```bash +./charts/load_test.sh 10 +``` + +The command above sends 10 requests to the gateway. So if the pipeline has completted stage Blue_50, the result will look like this: + +```bash +"New Model A" +"New Model A" +"New Model A" +"New Model B" +"New Model A" +"New Model B" +"New Model B" +"New Model A" +"New Model A" +"New Model A" +``` + +Despite what blue/green weights are configured now on the cluster, you can perform ***A/B testing*** and send requests directly to either blue or green images: + +```bash ./charts/load_test.sh 10 $GATEWAY_IP/score blue - ./charts/load_test.sh 10 $GATEWAY_IP/score green - +``` diff --git a/docs/images/canary-deployment-trigger.png b/docs/images/canary-deployment-trigger.png new file mode 100644 index 0000000000000000000000000000000000000000..697c5d1d77fefd8d725ef06dcc274df6c339e9b0 GIT binary patch literal 61799 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 literal 0 HcmV?d00001 diff --git a/docs/images/scoring_image.png b/docs/images/scoring_image.png new file mode 100644 index 0000000000000000000000000000000000000000..ecb1c24546e381bff3d6ece9ba160edd29fd0489 GIT binary patch literal 91162 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

IR=Sa2e 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 Date: Tue, 10 Dec 2019 11:42:45 -0800 Subject: [PATCH 25/59] merge with master --- code/scoring/conda_dependencies.yml | 19 ------------------- ml_service/util/create_scoring_image.py | 25 ------------------------- 2 files changed, 44 deletions(-) diff --git a/code/scoring/conda_dependencies.yml b/code/scoring/conda_dependencies.yml index 5fbce951..41a05694 100644 --- a/code/scoring/conda_dependencies.yml +++ b/code/scoring/conda_dependencies.yml @@ -18,31 +18,13 @@ name: project_environment dependencies: # The python interpreter version. # Currently Azure ML Workbench only supports 3.5.2 and later. -<<<<<<< HEAD - -======= ->>>>>>> master - python=3.7.5 # Required by azureml-defaults, installed separately through Conda to # get a prebuilt version and not require build tools for the install. -<<<<<<< HEAD - -======= ->>>>>>> master - psutil=5.6 #latest - pip: # Required packages for AzureML execution, history, and data preparation. -<<<<<<< HEAD - - azureml-sdk==1.0.72 #1.0.72 - - scipy==1.3.1 #1.3.1 - - scikit-learn==0.21.3 #latest - - pandas==0.25.3 #0.25.3 - - numpy==1.17.3 #1.17.3 - - joblib==0.14.0 #0.14.0 - - gunicorn==19.9.0 #latest - - flask==1.1.1 #latest -======= - azureml-model-management-sdk==1.0.1b6.post1 - azureml-sdk==1.0.74 - scipy==1.3.1 @@ -52,5 +34,4 @@ dependencies: - joblib==0.14.0 - gunicorn==19.9.0 - flask==1.1.1 ->>>>>>> master diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index e1d65223..23dd6ff9 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -7,28 +7,7 @@ sys.path.append(os.path.abspath("./ml_service/util")) # NOQA: E402 from env_variables import Env -<<<<<<< HEAD -load_dotenv() - -TENANT_ID = os.environ.get('TENANT_ID') -APP_ID = os.environ.get('SP_APP_ID') -APP_SECRET = os.environ.get('SP_APP_SECRET') -WORKSPACE_NAME = os.environ.get('WORKSPACE_NAME') -SUBSCRIPTION_ID = os.environ.get('SUBSCRIPTION_ID') -RESOURCE_GROUP = os.environ.get("RESOURCE_GROUP") -MODEL_NAME = os.environ.get('MODEL_NAME') -MODEL_VERSION = os.environ.get('MODEL_VERSION') -IMAGE_NAME = os.environ.get('IMAGE_NAME') -SCORE_SCRIPT = os.environ.get('SCORE_SCRIPT') -BUILD_NUMBER = os.environ.get('BUILD_BUILDNUMBER') - -SP_AUTH = ServicePrincipalAuthentication( - tenant_id=TENANT_ID, - service_principal_id=APP_ID, - service_principal_password=APP_SECRET) -======= e = Env() ->>>>>>> master # Get Azure machine learning workspace ws = Workspace.get( @@ -58,11 +37,7 @@ ) image = Image.create( -<<<<<<< HEAD - name=IMAGE_NAME + "-" + BUILD_NUMBER, models=[model], image_config=image_config, workspace=ws -======= name=e.image_name, models=[model], image_config=image_config, workspace=ws ->>>>>>> master ) os.chdir("../..") From 1d7b1a9039bacaf6dc2ce2476ab013ae42a1ab9e Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 11:43:27 -0800 Subject: [PATCH 26/59] test --- .pipelines/azdo-ci-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index d7824fa2..debedc4d 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -20,7 +20,7 @@ container: mcr.microsoft.com/mlops/python:latest variables: - group: devopsforai-aml-vg - name: 'SCORE_SCRIPT' - value: 'scoreA.py' + value: 'scoreB.py' name: $(Date:yyyyMMdd)$(Rev:r) steps: From 8c94c5d43968ad2c8ff1ef3340814b38af51fe31 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 12:39:07 -0800 Subject: [PATCH 27/59] merging with master --- .pipelines/azdo-ci-image.yml | 15 ++++++++------- ml_service/util/create_scoring_image.py | 2 +- ml_service/util/env_variables.py | 6 ++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index debedc4d..d035464e 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -25,12 +25,13 @@ variables: name: $(Date:yyyyMMdd)$(Rev:r) steps: -- bash: | - python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py - failOnStderr: 'false' - env: - SP_APP_SECRET: '$(SP_APP_SECRET)' - MODEL_VERSION: 1 +- task: AzureCLI@1 + inputs: + azureSubscription: '$(WORKSPACE_SVC_CONNECTION)' + scriptLocation: inlineScript + inlineScript: | + set -e + export SUBSCRIPTION_ID=$(az account show --query id -o tsv) + python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py displayName: 'Create Scoring Image' - enabled: 'true' diff --git a/ml_service/util/create_scoring_image.py b/ml_service/util/create_scoring_image.py index 23dd6ff9..edc57bf1 100644 --- a/ml_service/util/create_scoring_image.py +++ b/ml_service/util/create_scoring_image.py @@ -29,7 +29,7 @@ os.chdir("./code/scoring") image_config = ContainerImage.image_configuration( - execution_script=SCORE_SCRIPT, + execution_script=e.score_script, runtime="python", conda_file="conda_dependencies.yml", description="Image with ridge regression model", diff --git a/ml_service/util/env_variables.py b/ml_service/util/env_variables.py index 5ed59866..de0a2211 100644 --- a/ml_service/util/env_variables.py +++ b/ml_service/util/env_variables.py @@ -38,6 +38,8 @@ def __init__(self): self._image_name = os.environ.get('IMAGE_NAME') self._model_path = os.environ.get('MODEL_PATH') self._db_cluster_id = os.environ.get("DB_CLUSTER_ID") + self._score_script = os.environ.get("SCORE_SCRIPT") + self._build_number = os.environ.get("BUILD_BUILDNUMBER") @property def workspace_name(self): @@ -130,3 +132,7 @@ def image_name(self): @property def model_path(self): return self._model_path + + @property + def score_script(self): + return self._score_script From 370f3eb6330b70024bb929e7000b7348d440a5e5 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:22:16 -0800 Subject: [PATCH 28/59] multistage --- .pipelines/azdo-release-abtest-pipeline.yml | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 34ea49d7..ad565d3f 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -12,12 +12,39 @@ variables: value: 'model-blue' - name: 'greenReleaseName' value: 'model-green' +- name: 'SCORE_SCRIPT' + value: 'scoreB.py' trigger: - master stages: +- stage: 'Building' + jobs: + - job: "Build_Scoring_image" + timeoutInMinutes: 0 + steps: + - task: AzureCLI@1 + inputs: + azureSubscription: '$(WORKSPACE_SVC_CONNECTION)' + scriptLocation: inlineScript + inlineScript: | + set -e + export SUBSCRIPTION_ID=$(az account show --query id -o tsv) + python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt + # Output image location to Azure DevOps job + IMAGE_LOCATION="$(cat image_location.txt)" + echo "##vso[task.setvariable variable=IMAGE_LOCATION]$IMAGE_LOCATION" + displayName: 'Create Scoring Image' + - script: | + echo $IMAGE_LOCATION + displayName: 'Test Variable' + + + # - publish: $(System.DefaultWorkingDirectory)/charts + # artifact: allcharts + - stage: 'Blue_Staging' jobs: - job: "Deploy_to_Staging" From fc76269f04b3d2062381ae79b56befc93afeb710 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:23:36 -0800 Subject: [PATCH 29/59] tab --- .pipelines/azdo-release-abtest-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index ad565d3f..dd5ce77a 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -38,7 +38,7 @@ stages: echo "##vso[task.setvariable variable=IMAGE_LOCATION]$IMAGE_LOCATION" displayName: 'Create Scoring Image' - script: | - echo $IMAGE_LOCATION + echo $IMAGE_LOCATION displayName: 'Test Variable' From 275d516f27fb0f8a4f18e02e8192358182ff34b0 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:23:56 -0800 Subject: [PATCH 30/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index dd5ce77a..ccd7e65a 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -50,6 +50,9 @@ stages: - job: "Deploy_to_Staging" timeoutInMinutes: 0 steps: + - script: | + echo $IMAGE_LOCATION + displayName: 'Test Variable' - template: azdo-helm-upgrade.yml parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' From bf9de53f5aa1392d7941651b4f51a2a5e37b0f0a Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:26:50 -0800 Subject: [PATCH 31/59] container --- .pipelines/azdo-release-abtest-pipeline.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index ccd7e65a..ed1722a0 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -24,6 +24,9 @@ stages: jobs: - job: "Build_Scoring_image" timeoutInMinutes: 0 + pool: + vmImage: 'ubuntu-latest' + container: mcr.microsoft.com/mlops/python:latest steps: - task: AzureCLI@1 inputs: From 51f36b7ac4bf7c9fadbb8f802595979ca73e62f5 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:38:36 -0800 Subject: [PATCH 32/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index ed1722a0..cbe2df07 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -35,11 +35,12 @@ stages: inlineScript: | set -e export SUBSCRIPTION_ID=$(az account show --query id -o tsv) - python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt - # Output image location to Azure DevOps job - IMAGE_LOCATION="$(cat image_location.txt)" - echo "##vso[task.setvariable variable=IMAGE_LOCATION]$IMAGE_LOCATION" + # python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt + # IMAGE_LOCATION="$(cat image_location.txt)" + IMAGE_LOCATION="test value" + echo "##vso[task.setvariable variable=IMAGE_LOCATION;isOutput=true]$IMAGE_LOCATION" displayName: 'Create Scoring Image' + name: 'buildscoringimage' - script: | echo $IMAGE_LOCATION displayName: 'Test Variable' @@ -51,6 +52,9 @@ stages: - stage: 'Blue_Staging' jobs: - job: "Deploy_to_Staging" + dependsOn: "Build_Scoring_image" + variables: + IMAGE_LOCATION: $[ dependencies.Build_Scoring_image.outputs['buildscoringimage.IMAGE_LOCATION'] ] timeoutInMinutes: 0 steps: - script: | From 06caba0d63e97566f85252b9007bad2fe74bbde6 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 13:53:45 -0800 Subject: [PATCH 33/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index cbe2df07..5eb62455 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -39,6 +39,7 @@ stages: # IMAGE_LOCATION="$(cat image_location.txt)" IMAGE_LOCATION="test value" echo "##vso[task.setvariable variable=IMAGE_LOCATION;isOutput=true]$IMAGE_LOCATION" + echo "mlopspyciamlcr.azurecr.io/scoringimg:2" > image_location.txt displayName: 'Create Scoring Image' name: 'buildscoringimage' - script: | @@ -46,18 +47,16 @@ stages: displayName: 'Test Variable' - # - publish: $(System.DefaultWorkingDirectory)/charts - # artifact: allcharts + - publish: image_location.txt + artifact: image_location - stage: 'Blue_Staging' jobs: - job: "Deploy_to_Staging" - dependsOn: "Build_Scoring_image" - variables: - IMAGE_LOCATION: $[ dependencies.Build_Scoring_image.outputs['buildscoringimage.IMAGE_LOCATION'] ] timeoutInMinutes: 0 steps: - script: | + IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" echo $IMAGE_LOCATION displayName: 'Test Variable' - template: azdo-helm-upgrade.yml From 5c3197242f43f48b1c37da631dc3990650e30ead Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:03:22 -0800 Subject: [PATCH 34/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 5eb62455..d43006fc 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -26,7 +26,7 @@ stages: timeoutInMinutes: 0 pool: vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest + # container: mcr.microsoft.com/mlops/python:latest steps: - task: AzureCLI@1 inputs: @@ -43,7 +43,8 @@ stages: displayName: 'Create Scoring Image' name: 'buildscoringimage' - script: | - echo $IMAGE_LOCATION + cat image_location.txt + ls -ltr displayName: 'Test Variable' From 2d7d74c7444e097e5fb352f29893543c4246249e Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:09:48 -0800 Subject: [PATCH 35/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 34 ++++++++++++--------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index d43006fc..d40f0f65 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -47,27 +47,31 @@ stages: ls -ltr displayName: 'Test Variable' - - publish: image_location.txt artifact: image_location + - publish: $(System.DefaultWorkingDirectory)/charts + artifact: allcharts + + - stage: 'Blue_Staging' jobs: - - job: "Deploy_to_Staging" + - deployment: "Deploy_to_Staging" timeoutInMinutes: 0 - steps: - - script: | - IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" - echo $IMAGE_LOCATION - displayName: 'Test Variable' - - template: azdo-helm-upgrade.yml - parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - releaseName: $(blueReleaseName) - overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - - - publish: $(System.DefaultWorkingDirectory)/charts - artifact: allcharts + environment: abtestenv + strategy: + runOnce: + deploy: + steps: + - script: | + IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" + echo $IMAGE_LOCATION + displayName: 'Test Variable' + - template: azdo-helm-upgrade.yml + parameters: + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + releaseName: $(blueReleaseName) + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - stage: 'Blue_50' jobs: From 4dba3645919207ba4d200627b2fd5432f17a1e12 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:18:04 -0800 Subject: [PATCH 36/59] test --- .pipelines/azdo-helm-install.yml | 6 +++ .pipelines/azdo-release-abtest-pipeline.yml | 38 ++++++++----------- charts/abtest-model/templates/deployment.yaml | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.pipelines/azdo-helm-install.yml b/.pipelines/azdo-helm-install.yml index fc5eac6f..9b29bdda 100644 --- a/.pipelines/azdo-helm-install.yml +++ b/.pipelines/azdo-helm-install.yml @@ -1,3 +1,9 @@ +variables: +- name: 'helmVersion' + value: 'v3.0.0-rc.3' +- name: 'helmDownloadURL' + value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' + steps: - task: Bash@3 displayName: 'Install Helm $(helmVersion)' diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index d40f0f65..d7f856b5 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -1,13 +1,5 @@ variables: - group: 'devopsforai-aml-vg' -- name: 'helmVersion' - value: 'v3.0.0-rc.3' -- name: 'helmDownloadURL' - value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' -- name: 'imgTag' - value: 1 -- name: 'imgName' - value: $(IMAGE_REPO_NAME)/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) - name: 'blueReleaseName' value: 'model-blue' - name: 'greenReleaseName' @@ -35,11 +27,6 @@ stages: inlineScript: | set -e export SUBSCRIPTION_ID=$(az account show --query id -o tsv) - # python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt - # IMAGE_LOCATION="$(cat image_location.txt)" - IMAGE_LOCATION="test value" - echo "##vso[task.setvariable variable=IMAGE_LOCATION;isOutput=true]$IMAGE_LOCATION" - echo "mlopspyciamlcr.azurecr.io/scoringimg:2" > image_location.txt displayName: 'Create Scoring Image' name: 'buildscoringimage' - script: | @@ -66,12 +53,12 @@ stages: - script: | IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" echo $IMAGE_LOCATION - displayName: 'Test Variable' + displayName: 'Get Image Location' - template: azdo-helm-upgrade.yml parameters: chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' releaseName: $(blueReleaseName) - overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$(IMAGE_LOCATION)' - stage: 'Blue_50' jobs: @@ -121,14 +108,21 @@ stages: dependsOn: 'Blue_100' condition: succeeded() jobs: - - job: 'green_blue_tagging' + - deployment: 'green_blue_tagging' timeoutInMinutes: 0 - steps: - - template: azdo-helm-upgrade.yml - parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - releaseName: $(greenReleaseName) - overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.tag=$(imgTag),initialDeployment=true,deployment.image.name=$(imgName)' + environment: abtestenv + strategy: + runOnce: + deploy: + steps: + - script: | + IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" + displayName: 'Get Image Location' + - template: azdo-helm-upgrade.yml + parameters: + chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + releaseName: $(greenReleaseName) + overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$(IMAGE_LOCATION)' - stage: 'Green_100' jobs: diff --git a/charts/abtest-model/templates/deployment.yaml b/charts/abtest-model/templates/deployment.yaml index 6b64e8b2..13572b36 100644 --- a/charts/abtest-model/templates/deployment.yaml +++ b/charts/abtest-model/templates/deployment.yaml @@ -20,7 +20,7 @@ spec: spec: containers: - name: {{ .Values.deployment.container.name }} - image: "{{ .Values.deployment.image.name }}:{{ .Values.deployment.image.tag }}" + image: "{{ .Values.deployment.image.name }}" imagePullPolicy: Always ports: - name: http From 8cb94fde166ff8fb716d925eb9be178dd70cf26e Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:19:57 -0800 Subject: [PATCH 37/59] test --- .pipelines/azdo-helm-install.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pipelines/azdo-helm-install.yml b/.pipelines/azdo-helm-install.yml index 9b29bdda..f0d2e078 100644 --- a/.pipelines/azdo-helm-install.yml +++ b/.pipelines/azdo-helm-install.yml @@ -1,8 +1,8 @@ variables: -- name: 'helmVersion' - value: 'v3.0.0-rc.3' -- name: 'helmDownloadURL' - value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' + - name: 'helmVersion' + value: 'v3.0.0-rc.3' + - name: 'helmDownloadURL' + value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' steps: - task: Bash@3 From c64a140d94f756b400f205914209571f89634944 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:22:58 -0800 Subject: [PATCH 38/59] test --- .pipelines/azdo-helm-install.yml | 6 ------ .pipelines/azdo-release-abtest-pipeline.yml | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.pipelines/azdo-helm-install.yml b/.pipelines/azdo-helm-install.yml index f0d2e078..fc5eac6f 100644 --- a/.pipelines/azdo-helm-install.yml +++ b/.pipelines/azdo-helm-install.yml @@ -1,9 +1,3 @@ -variables: - - name: 'helmVersion' - value: 'v3.0.0-rc.3' - - name: 'helmDownloadURL' - value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' - steps: - task: Bash@3 displayName: 'Install Helm $(helmVersion)' diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index d7f856b5..e06aa8ce 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -1,5 +1,9 @@ variables: - group: 'devopsforai-aml-vg' +- name: 'helmVersion' + value: 'v3.0.0-rc.3' +- name: 'helmDownloadURL' + value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' - name: 'blueReleaseName' value: 'model-blue' - name: 'greenReleaseName' @@ -18,7 +22,7 @@ stages: timeoutInMinutes: 0 pool: vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest + container: mcr.microsoft.com/mlops/python:latest steps: - task: AzureCLI@1 inputs: @@ -27,6 +31,7 @@ stages: inlineScript: | set -e export SUBSCRIPTION_ID=$(az account show --query id -o tsv) + python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt displayName: 'Create Scoring Image' name: 'buildscoringimage' - script: | From a4436ceb8aeb446752680a5996d24b1713b458a0 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:33:11 -0800 Subject: [PATCH 39/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 4 ++-- ml_service/util/env_variables.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index e06aa8ce..d2257ec0 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -61,7 +61,7 @@ stages: displayName: 'Get Image Location' - template: azdo-helm-upgrade.yml parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(blueReleaseName) overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$(IMAGE_LOCATION)' @@ -125,7 +125,7 @@ stages: displayName: 'Get Image Location' - template: azdo-helm-upgrade.yml parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' + chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(greenReleaseName) overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$(IMAGE_LOCATION)' diff --git a/ml_service/util/env_variables.py b/ml_service/util/env_variables.py index de0a2211..ed7f25f3 100644 --- a/ml_service/util/env_variables.py +++ b/ml_service/util/env_variables.py @@ -39,7 +39,6 @@ def __init__(self): self._model_path = os.environ.get('MODEL_PATH') self._db_cluster_id = os.environ.get("DB_CLUSTER_ID") self._score_script = os.environ.get("SCORE_SCRIPT") - self._build_number = os.environ.get("BUILD_BUILDNUMBER") @property def workspace_name(self): From 9244c7f8816c60708249222b4a26a8e2671f4321 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:47:06 -0800 Subject: [PATCH 40/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index d2257ec0..3e419146 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -63,7 +63,7 @@ stages: parameters: chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(blueReleaseName) - overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$(IMAGE_LOCATION)' + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$IMAGE_LOCATION' - stage: 'Blue_50' jobs: @@ -127,7 +127,7 @@ stages: parameters: chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(greenReleaseName) - overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$(IMAGE_LOCATION)' + overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$IMAGE_LOCATION' - stage: 'Green_100' jobs: From 8373ebc4e51a7ea895c042b41b9f659c462a77a0 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 14:49:03 -0800 Subject: [PATCH 41/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 3e419146..9a8fb357 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -22,7 +22,7 @@ stages: timeoutInMinutes: 0 pool: vmImage: 'ubuntu-latest' - container: mcr.microsoft.com/mlops/python:latest + # container: mcr.microsoft.com/mlops/python:latest steps: - task: AzureCLI@1 inputs: @@ -31,7 +31,8 @@ stages: inlineScript: | set -e export SUBSCRIPTION_ID=$(az account show --query id -o tsv) - python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt + # python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt + echo "mlopspyciamlcr.azurecr.io/scoringimg:4" > image_location.txt displayName: 'Create Scoring Image' name: 'buildscoringimage' - script: | From ee16e444de439b3f5d0bc962a509ad96b9d24927 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 15:00:29 -0800 Subject: [PATCH 42/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 9a8fb357..4fa4a9ec 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -58,13 +58,13 @@ stages: steps: - script: | IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" - echo $IMAGE_LOCATION + echo "##vso[task.setvariable variable=IMAGE_LOCATION]$IMAGE_LOCATION" displayName: 'Get Image Location' - template: azdo-helm-upgrade.yml parameters: chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(blueReleaseName) - overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$IMAGE_LOCATION' + overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.name=$(IMAGE_LOCATION)' - stage: 'Blue_50' jobs: @@ -123,12 +123,13 @@ stages: steps: - script: | IMAGE_LOCATION="$(cat $(Pipeline.Workspace)/image_location/image_location.txt)" + echo "##vso[task.setvariable variable=IMAGE_LOCATION]$IMAGE_LOCATION" displayName: 'Get Image Location' - template: azdo-helm-upgrade.yml parameters: chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(greenReleaseName) - overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$IMAGE_LOCATION' + overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$(IMAGE_LOCATION)' - stage: 'Green_100' jobs: From 52f907fae9d5467ecf0f19aad6ecaa0f8c07468a Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 15:05:45 -0800 Subject: [PATCH 43/59] test --- .pipelines/azdo-release-abtest-pipeline.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-release-abtest-pipeline.yml index 4fa4a9ec..b5831a37 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-release-abtest-pipeline.yml @@ -9,7 +9,7 @@ variables: - name: 'greenReleaseName' value: 'model-green' - name: 'SCORE_SCRIPT' - value: 'scoreB.py' + value: 'scoreA.py' trigger: @@ -22,7 +22,7 @@ stages: timeoutInMinutes: 0 pool: vmImage: 'ubuntu-latest' - # container: mcr.microsoft.com/mlops/python:latest + container: mcr.microsoft.com/mlops/python:latest steps: - task: AzureCLI@1 inputs: @@ -31,14 +31,9 @@ stages: inlineScript: | set -e export SUBSCRIPTION_ID=$(az account show --query id -o tsv) - # python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt - echo "mlopspyciamlcr.azurecr.io/scoringimg:4" > image_location.txt + python ml_service/util/create_scoring_image.py --output_image_location_file image_location.txt displayName: 'Create Scoring Image' name: 'buildscoringimage' - - script: | - cat image_location.txt - ls -ltr - displayName: 'Test Variable' - publish: image_location.txt artifact: image_location From 4b50d1e91ae7bff46704aa18209fe6c435f681d3 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 15:08:44 -0800 Subject: [PATCH 44/59] cleaning --- .pipelines/azdo-ci-image.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pipelines/azdo-ci-image.yml b/.pipelines/azdo-ci-image.yml index d035464e..b5375ad5 100644 --- a/.pipelines/azdo-ci-image.yml +++ b/.pipelines/azdo-ci-image.yml @@ -22,7 +22,6 @@ variables: - name: 'SCORE_SCRIPT' value: 'scoreB.py' -name: $(Date:yyyyMMdd)$(Rev:r) steps: - task: AzureCLI@1 From 650af1e9b3fbd39327019892aab36e4d337bdc32 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 15:26:33 -0800 Subject: [PATCH 45/59] doc update --- ...-pipeline.yml => azdo-abtest-pipeline.yml} | 15 +++- docs/canary_ab_deployment.md | 85 +++--------------- docs/images/canary-deployment-trigger.png | Bin 61799 -> 0 bytes 3 files changed, 25 insertions(+), 75 deletions(-) rename .pipelines/{azdo-release-abtest-pipeline.yml => azdo-abtest-pipeline.yml} (95%) delete mode 100644 docs/images/canary-deployment-trigger.png diff --git a/.pipelines/azdo-release-abtest-pipeline.yml b/.pipelines/azdo-abtest-pipeline.yml similarity index 95% rename from .pipelines/azdo-release-abtest-pipeline.yml rename to .pipelines/azdo-abtest-pipeline.yml index b5831a37..ca211814 100644 --- a/.pipelines/azdo-release-abtest-pipeline.yml +++ b/.pipelines/azdo-abtest-pipeline.yml @@ -1,3 +1,15 @@ +pr: none +trigger: + branches: + include: + - master + paths: + exclude: + - docs/ + - environment_setup/ + - ml_service/util/create_scoring_image.* + - ml_service/util/smoke_test_scoring_service.py + variables: - group: 'devopsforai-aml-vg' - name: 'helmVersion' @@ -12,9 +24,6 @@ variables: value: 'scoreA.py' -trigger: -- master - stages: - stage: 'Building' jobs: diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 6c60b028..0451f99b 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -23,14 +23,11 @@ There are some extra variables that you need to setup in ***devopsforai-aml-vg** | K8S_AB_SERVICE_CONNECTION | AzDo service connection to a K8s cluster | | K8S_AB_NAMESPACE | Namespace in a K8s cluster to deploy the model | | IMAGE_REPO_NAME | Image reposiory name (e.g. mlopspyciamlcr.azurecr.io)| -| IMAGE_NAME | Scoring image name (e.g. myscoring) | -| MODEL_NAME | Name of the registered in AML model to be deployed | -| MODEL_VERSION | Version of the registered in AML model to be deployed| -#### 3. Configure a pipeline to build a Scoring Image +#### 3. Configure a pipeline to build and deploy a scoring Image -Use [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to create a pipeline building a scoring image. +Use [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to configure a multistage deployment pipeline: ```yaml pr: none @@ -39,87 +36,33 @@ trigger: include: - master paths: - include: - - ml_service/util/create_scoring_image.py - - ml_service/util/Dockerfile - - code/scoring/ exclude: - - code/scoring/deployment_config_aci.yml - - code/scoring/deployment_config_aks.yml - -pool: - vmImage: 'ubuntu-latest' - -container: mcr.microsoft.com/mlops/python:latest + - docs/ + - environment_setup/ + - ml_service/util/create_scoring_image.* + - ml_service/util/smoke_test_scoring_service.py -variables: -- group: devopsforai-aml-vg -- name: 'SCORE_SCRIPT' - value: 'scoreA.py' - -name: $(Date:yyyyMMdd)$(Rev:r) -steps: - -- bash: | - python3 $(Build.SourcesDirectory)/ml_service/util/create_scoring_image.py - failOnStderr: 'false' - env: - SP_APP_SECRET: '$(SP_APP_SECRET)' - MODEL_VERSION: 1 - displayName: 'Create Scoring Image' - enabled: 'true' -``` - -#### 4. Configure a deployment pipeline. - -Use [azdo-release-abtest-pipeline.yml](./.pipelines/azdo-release-abtest-pipeline.yml) to configure a multistage deployment pipeline: - -```yaml variables: - group: 'devopsforai-aml-vg' - name: 'helmVersion' value: 'v3.0.0-rc.3' - name: 'helmDownloadURL' value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' -- name: 'imgTag' - value: 1 -- name: 'imgName' - value: $(IMAGE_REPO_NAME)/$(IMAGE_NAME)-$(Build.TriggeredBy.BuildNumber) - name: 'blueReleaseName' value: 'model-blue' - name: 'greenReleaseName' value: 'model-green' +- name: 'SCORE_SCRIPT' + value: 'scoreA.py' - -trigger: -- master - -stages: -- stage: 'Blue_Staging' - jobs: - - job: "Deploy_to_Staging" - timeoutInMinutes: 0 - steps: - - template: azdo-helm-upgrade.yml - parameters: - chartPath: '$(System.DefaultWorkingDirectory)/charts/abtest-model' - releaseName: $(blueReleaseName) - overrideValues: 'deployment.name=$(blueReleaseName),deployment.bluegreen=blue,deployment.image.tag=$(imgTag),deployment.image.name=$(imgName)' - - - publish: $(System.DefaultWorkingDirectory)/charts - artifact: allcharts ... ``` -Make sure that the release pipeline is configured to be triggered once the scoring image build is completed: - -![canary deployment trigger](./images/canary-deployment-trigger.png) - -Manually run a pipeline building a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: +Manually Run a pipeline building a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: ![scoring image](./images/scoring_image.png) -The release pipeline will be triggered automatically and it will deploy the scroring image to the Kubernetes cluster. +The pipeline will also deploy the scroring image to the Kubernetes cluster. ```bash kubectl get deployments --namespace abtesting @@ -127,12 +70,10 @@ NAME READY UP-TO-DATE AVAILABLE AGE model-green 1/1 1 1 19h ``` -#### 5. Build a new Scoring Image. - -Change value of the ***SCORE_SCRIPT*** variable in the [azdo-ci-image.yml](./.pipelines/azdo-ci-image.yml) to point to ***scoreA.py*** and merge it to the master branch. -It will trigger the building pipeline and the release pipeline after that. +#### 4. Build a new Scoring Image. -The release pipeline deploys a new scoring image with the following stages implementing ***Canary*** deployment strategy: +Change value of the ***SCORE_SCRIPT*** variable in the [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to point to ***scoreA.py*** and merge it to the master branch. +It will automatically trigger the pipeline and it will deploy a new scoring image with the following stages implementing ***Canary*** deployment strategy: | Stage | Green Weight| Blue Weight| Description | | ------------------- |-------------|------------|-----------------------------------------------------------------| diff --git a/docs/images/canary-deployment-trigger.png b/docs/images/canary-deployment-trigger.png deleted file mode 100644 index 697c5d1d77fefd8d725ef06dcc274df6c339e9b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61799 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 From fd42e3510e3be3fb2942bd8ddea5373cf2900235 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 15:30:29 -0800 Subject: [PATCH 46/59] cleaning --- .pipelines/azdo-variables.yml | 4 ++- code/scoring/deployment_config_aks.yml | 2 +- vss-extension.json | 38 -------------------------- 3 files changed, 4 insertions(+), 40 deletions(-) delete mode 100644 vss-extension.json diff --git a/.pipelines/azdo-variables.yml b/.pipelines/azdo-variables.yml index 5d7da750..fcf67c2b 100644 --- a/.pipelines/azdo-variables.yml +++ b/.pipelines/azdo-variables.yml @@ -37,4 +37,6 @@ variables: value: 'mltrained' # Optional. Used by a training pipeline with R on Databricks - name: DB_CLUSTER_ID - value: '' \ No newline at end of file + value: '' +- name: SCORE_SCRIPT + value: score.py \ No newline at end of file diff --git a/code/scoring/deployment_config_aks.yml b/code/scoring/deployment_config_aks.yml index 6aeaf89e..1299dc9d 100644 --- a/code/scoring/deployment_config_aks.yml +++ b/code/scoring/deployment_config_aks.yml @@ -9,7 +9,7 @@ authEnabled: True containerResourceRequirements: cpu: 1 memoryInGB: 4 -appInsightsEnabled: false +appInsightsEnabled: True scoringTimeoutMs: 5000 maxConcurrentRequestsPerContainer: 2 maxQueueWaitMs: 5000 diff --git a/vss-extension.json b/vss-extension.json deleted file mode 100644 index 2d54a5cc..00000000 --- a/vss-extension.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "manifestVersion": 1, - "id": "SecurityPipelineDecorator", - "name": "Automatic Credential Scanning", - "version": "0.0.0.9", - "publisher": "CSE-DevOps-Team6", - "targets": [ - { - "id": "Microsoft.VisualStudio.Services" - } - ], - "description": "Organizational Pipeline Decorator to enforce credential scanning on all pipelines.", - "categories": [ - "Azure Pipelines" - ], - "icons": { - "default": "images/extension-icon.png" - }, - "contributions": [ - { - "id": "Credential-Scanning-Task", - "type": "ms.azure-pipelines.pipeline-decorator", - "targets": [ - "ms.azure-pipelines-agent-job.pre-job-tasks" - ], - "properties": { - "template": "securitydecorator/sec-decorator-filter.yml" - } - } - ], - "files": [ - { - "path": "securitydecorator/sec-decorator-filter.yml", - "addressable": true, - "contentType": "text/plain" - } - ] -} From b9eeb39bf78b3086d5d87ef88dd54715a58a2b50 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 16:11:01 -0800 Subject: [PATCH 47/59] linting --- code/scoring/scoreA.py | 9 --------- code/scoring/scoreB.py | 9 --------- 2 files changed, 18 deletions(-) diff --git a/code/scoring/scoreA.py b/code/scoring/scoreA.py index 7f2ba061..bb757efa 100644 --- a/code/scoring/scoreA.py +++ b/code/scoring/scoreA.py @@ -1,14 +1,5 @@ -import json -import numpy -from azureml.core.model import Model -import joblib - - def init(): global model - - def run(raw_data): return "New Model A" - diff --git a/code/scoring/scoreB.py b/code/scoring/scoreB.py index a0225204..12cbf4a8 100644 --- a/code/scoring/scoreB.py +++ b/code/scoring/scoreB.py @@ -1,14 +1,5 @@ -import json -import numpy -from azureml.core.model import Model -import joblib - - def init(): global model - - def run(raw_data): return "New Model B" - From 04fee4ca8e972e8e5e10460a37d531b26bba0572 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Tue, 10 Dec 2019 16:24:08 -0800 Subject: [PATCH 48/59] linting --- code/scoring/scoreA.py | 1 + code/scoring/scoreB.py | 1 + 2 files changed, 2 insertions(+) diff --git a/code/scoring/scoreA.py b/code/scoring/scoreA.py index bb757efa..ac4a6100 100644 --- a/code/scoring/scoreA.py +++ b/code/scoring/scoreA.py @@ -1,5 +1,6 @@ def init(): global model + def run(raw_data): return "New Model A" diff --git a/code/scoring/scoreB.py b/code/scoring/scoreB.py index 12cbf4a8..c0865269 100644 --- a/code/scoring/scoreB.py +++ b/code/scoring/scoreB.py @@ -1,5 +1,6 @@ def init(): global model + def run(raw_data): return "New Model B" From 0733cacc41a6ac53b5683042171fc391d570f570 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 10:07:49 -0800 Subject: [PATCH 49/59] Link to A/B tutorial --- docs/getting_started.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/getting_started.md b/docs/getting_started.md index b5e92902..b24545f4 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -199,6 +199,8 @@ The final stage is to deploy the model to the production environment running on tutorial, but you can find set up information in the docs [here](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal#create-an-aks-cluster). +**Note:** If your target deployment environment is a K8s cluster and you want to implement Canary and/or A/B testing deployemnt strategies check out this [tutorial](./canary_ab_deployment.md). + In the Variables tab, edit your variable group (`devopsforai-aml-vg`). In the variable group definition, add the following variables: | Variable Name | Suggested Value | From c30e3fcbe32959942d30da29dee52d7aa9787056 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 11 Dec 2019 12:53:46 -0800 Subject: [PATCH 50/59] helm to 3.0.1 --- .pipelines/azdo-abtest-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/azdo-abtest-pipeline.yml b/.pipelines/azdo-abtest-pipeline.yml index ca211814..9178746c 100644 --- a/.pipelines/azdo-abtest-pipeline.yml +++ b/.pipelines/azdo-abtest-pipeline.yml @@ -13,7 +13,7 @@ trigger: variables: - group: 'devopsforai-aml-vg' - name: 'helmVersion' - value: 'v3.0.0-rc.3' + value: 'v3.0.1' - name: 'helmDownloadURL' value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' - name: 'blueReleaseName' From 27229565445b2ddd37763b0a588761dc769da47a Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 11 Dec 2019 13:22:00 -0800 Subject: [PATCH 51/59] Update canary_ab_deployment.md --- docs/canary_ab_deployment.md | 43 ++++++------------------------------ 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 0451f99b..c65be828 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -1,4 +1,4 @@ -## Model deployment to a Kubernetes cluster with Canary and A/B testing deployemnt strategies. +## Model deployment to AKS cluster with Canary deployemnt If your target deployment environment is a K8s cluster and you want to implement [Canary and/or A/B testing deployemnt strategies](http://adfpractice-fedor.blogspot.com/2019/04/deployment-strategies-with-kubernetes.html) you can follow this sample guidance. @@ -16,7 +16,7 @@ GATEWAY_IP=$(kubectl get svc istio-ingressgateway -n istio-system -o jsonpath='{ #### 2. Set up variables -There are some extra variables that you need to setup in ***devopsforai-aml-vg*** variable group: +There are some extra variables that you need to setup in ***devopsforai-aml-vg*** variable group (see [getting started](./getting_started.md)): | Variable Name | Suggested Value | | --------------------------- | -----------------------------------------------------| @@ -27,42 +27,13 @@ There are some extra variables that you need to setup in ***devopsforai-aml-vg** #### 3. Configure a pipeline to build and deploy a scoring Image -Use [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to configure a multistage deployment pipeline: - -```yaml -pr: none -trigger: - branches: - include: - - master - paths: - exclude: - - docs/ - - environment_setup/ - - ml_service/util/create_scoring_image.* - - ml_service/util/smoke_test_scoring_service.py - -variables: -- group: 'devopsforai-aml-vg' -- name: 'helmVersion' - value: 'v3.0.0-rc.3' -- name: 'helmDownloadURL' - value: 'https://get.helm.sh/helm-$HELM_VERSION-linux-amd64.tar.gz' -- name: 'blueReleaseName' - value: 'model-blue' -- name: 'greenReleaseName' - value: 'model-green' -- name: 'SCORE_SCRIPT' - value: 'scoreA.py' - -... -``` +Import and run the [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) multistage deployment pipeline. -Manually Run a pipeline building a scoring image. The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: +The result of the pipeline will be a registered Docker image in the ACR repository attached to the AML Service: ![scoring image](./images/scoring_image.png) -The pipeline will also deploy the scroring image to the Kubernetes cluster. +The pipeline will also deploy the scoring image to the Kubernetes cluster. ```bash kubectl get deployments --namespace abtesting @@ -73,7 +44,7 @@ model-green 1/1 1 1 19h #### 4. Build a new Scoring Image. Change value of the ***SCORE_SCRIPT*** variable in the [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to point to ***scoreA.py*** and merge it to the master branch. -It will automatically trigger the pipeline and it will deploy a new scoring image with the following stages implementing ***Canary*** deployment strategy: +It will automatically trigger the pipeline and deploy a new scoring image with the following stages implementing ***Canary*** deployment strategy: | Stage | Green Weight| Blue Weight| Description | | ------------------- |-------------|------------|-----------------------------------------------------------------| @@ -83,7 +54,7 @@ It will automatically trigger the pipeline and it will deploy a new scoring imag | Blue_Green |0 |100 |Old green image is removed. The new blue image is copied as green.
Blue and Green images are equal.
All traffic (100%) is routed to the blue image.| | Green_100 |100 |0 |All traffic (100%) is routed to the green image.
The blue image is removed -At ecah stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with ***Postman*** or with ***curl***: +At each stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with ***Postman*** or with ***curl***: ```bash curl $GATEWAY_IP/score From 7e8cfb60884acd5d05f46263a161f73e3cc3b359 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 11 Dec 2019 13:29:42 -0800 Subject: [PATCH 52/59] Add build badge --- docs/canary_ab_deployment.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index c65be828..f60628f3 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -1,5 +1,7 @@ ## Model deployment to AKS cluster with Canary deployemnt +[![Build Status](https://aidemos.visualstudio.com/MLOps/_apis/build/status/microsoft.MLOpsPython-Canary?branchName=master)](https://aidemos.visualstudio.com/MLOps/_build/latest?definitionId=133&branchName=master) + If your target deployment environment is a K8s cluster and you want to implement [Canary and/or A/B testing deployemnt strategies](http://adfpractice-fedor.blogspot.com/2019/04/deployment-strategies-with-kubernetes.html) you can follow this sample guidance. **Note:** It is assumed that you have an AKS instance and configured ***kubectl*** to communicate with the cluster. From 8036fbfe262885a46834e891dcbf249e83221e2b Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 15:20:02 -0800 Subject: [PATCH 53/59] Comments --- .pipelines/azdo-abtest-pipeline.yml | 4 ++-- charts/abtest-model/values.yaml | 7 +------ docs/canary_ab_deployment.md | 12 ++++++++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.pipelines/azdo-abtest-pipeline.yml b/.pipelines/azdo-abtest-pipeline.yml index 9178746c..719bd7d2 100644 --- a/.pipelines/azdo-abtest-pipeline.yml +++ b/.pipelines/azdo-abtest-pipeline.yml @@ -21,7 +21,7 @@ variables: - name: 'greenReleaseName' value: 'model-green' - name: 'SCORE_SCRIPT' - value: 'scoreA.py' + value: 'scoreB.py' stages: @@ -133,7 +133,7 @@ stages: parameters: chartPath: '$(Pipeline.Workspace)/allcharts/abtest-model' releaseName: $(greenReleaseName) - overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,initialDeployment=true,deployment.image.name=$(IMAGE_LOCATION)' + overrideValues: 'deployment.name=$(greenReleaseName),deployment.bluegreen=green,deployment.image.name=$(IMAGE_LOCATION)' - stage: 'Green_100' jobs: diff --git a/charts/abtest-model/values.yaml b/charts/abtest-model/values.yaml index 3a824439..c3ab1b60 100644 --- a/charts/abtest-model/values.yaml +++ b/charts/abtest-model/values.yaml @@ -7,12 +7,7 @@ deployment: container: name: model port: 5001 - image: - name: amlabtest74c95585.azurecr.io/abtestimg - tag: 2 svc: name: model-svc - port: 5001 - -initialDeployment: false \ No newline at end of file + port: 5001 \ No newline at end of file diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index f60628f3..5272f4d3 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -46,6 +46,9 @@ model-green 1/1 1 1 19h #### 4. Build a new Scoring Image. Change value of the ***SCORE_SCRIPT*** variable in the [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to point to ***scoreA.py*** and merge it to the master branch. + +**Note:** ***scoreA.py*** and ***scoreB.py*** files used in this tutorial are just mockups returning either "New Model A" or "New Model B" respectively. They are used to demonstrate the concept of testing two scoring images with different models or scoring code. In real life you would implement a scoring file similar to [score.py](./../code/scoring/score.py) (see [getting started](./getting_started.md). + It will automatically trigger the pipeline and deploy a new scoring image with the following stages implementing ***Canary*** deployment strategy: | Stage | Green Weight| Blue Weight| Description | @@ -85,7 +88,16 @@ The command above sends 10 requests to the gateway. So if the pipeline has compl Despite what blue/green weights are configured now on the cluster, you can perform ***A/B testing*** and send requests directly to either blue or green images: +```bash +curl --header "x-api-version: blue" $GATEWAY_IP/score +curl --header "x-api-version: green" $GATEWAY_IP/score +``` + +or with the load_test.sh: + ```bash ./charts/load_test.sh 10 $GATEWAY_IP/score blue ./charts/load_test.sh 10 $GATEWAY_IP/score green ``` + +In this case the Istio Virtual Service analizes the request header and routes the traffic directly to the specified model verison. \ No newline at end of file From 0255c5fcac1ec4f4a67cb78b69bb57a1a2f6419c Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 15:31:27 -0800 Subject: [PATCH 54/59] update --- .pipelines/azdo-abtest-pipeline.yml | 2 +- docs/canary_ab_deployment.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pipelines/azdo-abtest-pipeline.yml b/.pipelines/azdo-abtest-pipeline.yml index 719bd7d2..a74cf330 100644 --- a/.pipelines/azdo-abtest-pipeline.yml +++ b/.pipelines/azdo-abtest-pipeline.yml @@ -21,7 +21,7 @@ variables: - name: 'greenReleaseName' value: 'model-green' - name: 'SCORE_SCRIPT' - value: 'scoreB.py' + value: 'scoreA.py' stages: diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 5272f4d3..54527295 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -68,7 +68,7 @@ curl $GATEWAY_IP/score You can also emulate a simple load test on the gateway with the ***load_test.sh***: ```bash -./charts/load_test.sh 10 +./charts/load_test.sh 10 $GATEWAY_IP/score ``` The command above sends 10 requests to the gateway. So if the pipeline has completted stage Blue_50, the result will look like this: From 42d3bf6663c5d4b21ffc410337451f75c789c841 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 15:49:32 -0800 Subject: [PATCH 55/59] update --- docs/canary_ab_deployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 54527295..695f46d5 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -47,7 +47,7 @@ model-green 1/1 1 1 19h Change value of the ***SCORE_SCRIPT*** variable in the [azdo-abtest-pipeline.yml](./.pipelines/azdo-abtest-pipeline.yml) to point to ***scoreA.py*** and merge it to the master branch. -**Note:** ***scoreA.py*** and ***scoreB.py*** files used in this tutorial are just mockups returning either "New Model A" or "New Model B" respectively. They are used to demonstrate the concept of testing two scoring images with different models or scoring code. In real life you would implement a scoring file similar to [score.py](./../code/scoring/score.py) (see [getting started](./getting_started.md). +**Note:** ***scoreA.py*** and ***scoreB.py*** files used in this tutorial are just mockups returning either "New Model A" or "New Model B" respectively. They are used to demonstrate the concept of testing two scoring images with different models or scoring code. In real life you would implement a scoring file similar to [score.py](./../code/scoring/score.py) (see [getting started](./getting_started.md)). It will automatically trigger the pipeline and deploy a new scoring image with the following stages implementing ***Canary*** deployment strategy: From 76d9a7518a012964f5224cab386a5e8f26c92de7 Mon Sep 17 00:00:00 2001 From: David Tesar Date: Wed, 11 Dec 2019 17:25:46 -0800 Subject: [PATCH 56/59] fix typo --- docs/canary_ab_deployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 695f46d5..f80df017 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -100,4 +100,4 @@ or with the load_test.sh: ./charts/load_test.sh 10 $GATEWAY_IP/score green ``` -In this case the Istio Virtual Service analizes the request header and routes the traffic directly to the specified model verison. \ No newline at end of file +In this case the Istio Virtual Service analyzes the request header and routes the traffic directly to the specified model verison. From b40bfc9591689c7774986db9eec8e50303c92d01 Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 17:34:19 -0800 Subject: [PATCH 57/59] typo --- docs/canary_ab_deployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index f80df017..3fe17c4c 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -100,4 +100,4 @@ or with the load_test.sh: ./charts/load_test.sh 10 $GATEWAY_IP/score green ``` -In this case the Istio Virtual Service analyzes the request header and routes the traffic directly to the specified model verison. +In this case the Istio Virtual Service analyzes the request header and routes the traffic directly to the specified model version. From a20affc9f779fdc81629a46587fe6d064dca9c5e Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 18:02:52 -0800 Subject: [PATCH 58/59] Link to the approvals --- docs/canary_ab_deployment.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index 3fe17c4c..b9394a79 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -59,6 +59,9 @@ It will automatically trigger the pipeline and deploy a new scoring image with t | Blue_Green |0 |100 |Old green image is removed. The new blue image is copied as green.
Blue and Green images are equal.
All traffic (100%) is routed to the blue image.| | Green_100 |100 |0 |All traffic (100%) is routed to the green image.
The blue image is removed + +**Note:** The pipeline performs the rollout without any pausing. You may want to configure [Approvals and Checks](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&tabs=check-pass) for the stages on your environment for better experience of the model testing. The environment ***abtestenv*** will be added automatically to your AzDo project after the first pipoeline run. + At each stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with ***Postman*** or with ***curl***: ```bash From 6273c40f1b85ffa1b3312b9fcacea8c3d699807b Mon Sep 17 00:00:00 2001 From: Eugene Fedorenko Date: Wed, 11 Dec 2019 18:05:28 -0800 Subject: [PATCH 59/59] typo --- docs/canary_ab_deployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/canary_ab_deployment.md b/docs/canary_ab_deployment.md index b9394a79..b114a308 100644 --- a/docs/canary_ab_deployment.md +++ b/docs/canary_ab_deployment.md @@ -60,7 +60,7 @@ It will automatically trigger the pipeline and deploy a new scoring image with t | Green_100 |100 |0 |All traffic (100%) is routed to the green image.
The blue image is removed -**Note:** The pipeline performs the rollout without any pausing. You may want to configure [Approvals and Checks](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&tabs=check-pass) for the stages on your environment for better experience of the model testing. The environment ***abtestenv*** will be added automatically to your AzDo project after the first pipoeline run. +**Note:** The pipeline performs the rollout without any pausing. You may want to configure [Approvals and Checks](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/approvals?view=azure-devops&tabs=check-pass) for the stages on your environment for better experience of the model testing. The environment ***abtestenv*** will be added automatically to your AzDo project after the first pipeline run. At each stage you can verify how the traffic is routed sending requests to $GATEWAY_IP/score with ***Postman*** or with ***curl***: