This tutorial shows how to implement a Service in the Swiss AI Center project step by step. It will guide you through the process of creating a Service with or without a model.
Info
Note that a Service can be implemented in any programming language as long as it follows the specifications of the Swiss AI Center project. This tutorial is using Python 3.10.
For the Service to work we will need to install numpy and opencv-python in addition to the dependencies of the template. So edit the requirements.txt file and add the following lines:
This will install the default Service dependencies and the ones we just added. The freeze file will be used to ensure all the developers have the same dependencies.
# Image RotateThis service rotates an image by 90, 180 or 270 degrees clockwise.
_Check the [related documentation](https://swiss-ai-center.github.io/swiss-ai-center/reference/image-rotate) for more information._
[project]# TODO: 1. CHANGE THE NAME OF THE PROJECT (1)!name="image-rotate"[tool.pytest.ini_options]pythonpath=[".","src"]addopts="--cov-config=.coveragerc --cov-report xml --cov-report term-missing --cov=./src"
Change the name of the project to image-rotate.
1.4.3 Update the service kubernetes configuration¶
In the kubernetes folder, you will find the configuration files for the Service.
Rename all the files by replacing sample-service with image-rotate.
apiVersion:v1kind:ConfigMapmetadata:# TODO: 1. CHANGE THE NAME OF THE CONFIG MAP (1)!name:image-rotate-configlabels:# TODO: 2. CHANGE THE APP LABEL (2)!app:image-rotatedata:ENVIRONMENT:developmentLOG_LEVEL:debugENGINE_URLS:'["http://core-engine-service:8080"]'# TODO: 3. CHANGE THE SERVICE URL (3)!# (the port must be the same as in the sample-service.service.yml and unused by other services)SERVICE_URL:http://image-rotate-service:8001
Change the name of the config map to image-rotate-config
Change the app label to image-rotate
Change the service url to http://image-rotate-service:8001. The port must be the same as in the image-rotate.service.yaml and unused by other services. (this is for local development only)
Open the image-rotate.ingress.yaml file and update sample-service with image-rotate.
apiVersion:networking.k8s.io/v1kind:Ingressmetadata:# TODO: 1. CHANGE THE NAME OF THE INGRESS (1)!name:image-rotate-ingressannotations:nginx.ingress.kubernetes.io/proxy-body-size:"16m"nginx.org/client-max-body-size:"16m"spec:rules:# TODO: 2. CHANGE THE HOST (2)!-host:image-rotate-swiss-ai-center.kube.isc.heia-fr.chhttp:paths:-path:/pathType:Prefixbackend:service:# TODO: 3. CHANGE THE NAME OF THE SERVICE (3)!name:image-rotate-serviceport:number:80tls:-hosts:# TODO: 4. CHANGE THE HOST (4)!-image-rotate-swiss-ai-center.kube.isc.heia-fr.ch
Change the name of the ingress to image-rotate-ingress
Change the host to image-rotate-swiss-ai-center.kube.isc.heia-fr.ch
Change the name of the service to image-rotate-service
Change the host to image-rotate-swiss-ai-center.kube.isc.heia-fr.ch
Note
The host can be changed to your own domain name if the Service is deployed on another Kubernetes cluster.
Open the image-rotate.service.yaml file and update sample-service with image-rotate.
apiVersion:v1kind:Servicemetadata:# TODO: 1. CHANGE THE NAME OF THE SERVICE (1)!name:image-rotate-servicespec:type:LoadBalancerports:-name:http# TODO: 2. CHANGE THE PORT (must be the same as in the sample-service.config-map.yml) (2)!port:8001targetPort:80protocol:TCPselector:# TODO: 3. CHANGE THE APP LABEL (3)!app:image-rotate
Change the name of the service to image-rotate-service
Change the port to 8001. The port must be the same as in the image-rotate.config-map.yaml and unused by other services. (this is for local development only)
Change the app label to image-rotate
Open the image-rotate.stateful.yaml file and update sample-service with image-rotate.
apiVersion:apps/v1kind:StatefulSetmetadata:# This name uniquely identifies the stateful set# TODO: 1. CHANGE THE NAME OF THE STATEFUL SET (1)!name:sample-service-statefullabels:# TODO: 2. CHANGE THE APP LABEL (2)!app:sample-servicespec:# TODO: 3. CHANGE THE NAME OF THE SERVICE (3)!serviceName:sample-servicereplicas:1selector:matchLabels:# TODO: 4. CHANGE THE APP LABEL (4)!app:sample-servicetemplate:metadata:labels:# TODO: 5. CHANGE THE APP LABEL (5)!app:sample-servicespec:containers:# TODO: 6. CHANGE THE NAME OF THE CONTAINER (6)!-name:sample-service# TODO: 7. CHANGE THE IMAGE NAME (7)!image:ghcr.io/swiss-ai-center/sample-service:latest# If you build the image locally, change the next line to `imagePullPolicy: Never` - there is no need to pull the imageimagePullPolicy:Alwaysports:-name:httpcontainerPort:80env:-name:MAX_TASKSvalue:"50"-name:ENGINE_ANNOUNCE_RETRIESvalue:"5"-name:ENGINE_ANNOUNCE_RETRY_DELAYvalue:"3"envFrom:-configMapRef:# TODO: 8. CHANGE THE NAME OF THE CONFIG MAP (8)!name:sample-service-config
Change the name of the stateful set to image-rotate-stateful
Change the app label to image-rotate
Change the name of the service to image-rotate
Change the app label to image-rotate
Change the app label to image-rotate
Change the name of the container to image-rotate
Change the image name to ghcr.io/swiss-ai-center/image-rotate-service:latest
Change the name of the config map to image-rotate-config
TODOs
When you are done, you need to remove all the TODOs from the files.
First open the .env file and update the SERVICE_URL variable to http://localhost:8001. The port must be the same as in the image-rotate.config-map.yaml file.
# Log levelLOG_LEVEL=debug
# EnvironmentENVIRONMENT=development
# The engine URLsENGINE_URLS=["http://localhost:8080"]# The Service URL# TODO: 1. REPLACE THE PORT WITH THE SAME AS IN THE CONFIG-MAP FILE (1)!SERVICE_URL="http://localhost:8001"# The maximum of tasks the service can processMAX_TASKS=50# The number of times the service tries to announce itself to the enginesENGINE_ANNOUNCE_RETRIES=5# The number of seconds between each retryENGINE_ANNOUNCE_RETRY_DELAY=3
Replace the port with the same as in the image-rotate.config-map.yaml file.
All the code of the Service is in the main.py file. The Service is a simple image rotation service that rotates the image by 90, 180, 270 degrees clockwise depending on the value of the rotation parameter.
Open the main.py with your favorite editor and follow the instructions below.
importasyncioimporttimefromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewarefromfastapi.responsesimportRedirectResponsefromcommon_code.configimportget_settingsfrompydanticimportFieldfromcommon_code.http_clientimportHttpClientfromcommon_code.logger.loggerimportget_loggerfromcommon_code.service.controllerimportrouterasservice_routerfromcommon_code.service.serviceimportServiceServicefromcommon_code.storage.serviceimportStorageServicefromcommon_code.tasks.controllerimportrouterastasks_routerfromcommon_code.tasks.serviceimportTasksServicefromcommon_code.service.modelsimportService,FieldDescriptionfromcommon_code.service.enumsimportServiceStatusfromcommon_code.common.enumsimportFieldDescriptionType# Imports required by the service's model# TODO: 1. ADD REQUIRED IMPORTS (ALSO IN THE REQUIREMENTS.TXT) (1)!importcv2importnumpyasnpfromcommon_code.tasks.serviceimportget_extensionsettings=get_settings()classMyService(Service):# TODO: 2. CHANGE THIS DESCRIPTION (2)!""" Image rotate model """# Any additional fields must be excluded for Pydantic to workmodel:object=Field(exclude=True)def__init__(self):super().__init__(# TODO: 3. CHANGE THE SERVICE NAME AND SLUG (3)!name="Image Rotate",slug="image-rotate",url=settings.service_url,summary=api_summary,description=api_description,status=ServiceStatus.AVAILABLE,# TODO: 4. CHANGE THE INPUT AND OUTPUT FIELDS, THE TAGS AND THE HAS_AI VARIABLE (4)!data_in_fields=[FieldDescription(name="image",type=[FieldDescriptionType.IMAGE_PNG,FieldDescriptionType.IMAGE_JPEG]),FieldDescription(name="rotation",type=[FieldDescriptionType.TEXT_PLAIN]),],data_out_fields=[FieldDescription(name="result",type=[FieldDescriptionType.IMAGE_PNG,FieldDescriptionType.IMAGE_JPEG]),],tags=[ExecutionUnitTag(name=ExecutionUnitTagName.IMAGE_PROCESSING,acronym=ExecutionUnitTagAcronym.IMAGE_PROCESSING),],has_ai=False)# TODO: 5. CHANGE THE PROCESS METHOD (CORE OF THE SERVICE) (5)!defprocess(self,data):# NOTE that the data is a dictionary with the keys being the field names set in the data_in_fieldsraw=data["image"].datainput_type=data["image"].typerotation=data["rotation"].dataiflen(rotation)==0:rotation=90else:rotation=int(rotation)# Decode the imageimg=cv2.imdecode(np.frombuffer(raw,np.uint8),1)# Rotate the imageforiinrange(int(rotation/90)):img=cv2.rotate(img,cv2.ROTATE_90_CLOCKWISE)# Encode the image with the same format as the inputguessed_extension=get_extension(input_type)is_success,out_buff=cv2.imencode(guessed_extension,img)# NOTE that the result must be a dictionary with the keys being the field names set in the data_out_fieldsreturn{"result":TaskData(data=out_buff.tobytes(),type=input_type,)}# TODO: 6. CHANGE THE API DESCRIPTION AND SUMMARY (6)!api_description="""Rotate an image by 90 degrees clockwise depending on the value of the `rotation` parameter. (90, 180, 270)"""api_summary="""Rotate an image by 90 degrees clockwise."""# Define the FastAPI application with information# TODO: 7. CHANGE THE API TITLE, VERSION, CONTACT AND LICENSE (7)!app=FastAPI(title="Image Rotate API.",description=api_description,version="1.0.0",contact={"name":"Swiss AI Center","url":"https://swiss-ai-center.ch/","email":"ia.recherche@hes-so.ch",},swagger_ui_parameters={"tagsSorter":"alpha","operationsSorter":"method",},license_info={"name":"GNU Affero General Public License v3.0 (GNU AGPLv3)","url":"https://choosealicense.com/licenses/agpl-3.0/",},)...
Import the OpenCV library and the get_extension function from the tasks service. This function is used to guess the extension of the image based on the input type.
Change the description of the service.
Change the name and the slug of the service. This is used to identify the service in the Core Engine.
Change the input/output fields of the service. The name of the field is the key of the dictionary that will be used in the process function. The type of the field is the type of the data that will be sent to the service. They are defined in the FieldDescriptionType enum. The tags are used to identify the service in the Core Engine. The has_ai variable is used to identify if the service is an AI service.
Change the process function. This is the core of the service. The data is a dictionary with the keys being the field names set in the data_in_fields. The result must be a dictionary with the keys being the field names set in the data_out_fields.
Change the API description and summary.
Change the API title, version, contact and license.
# Base imageFROMpython:3.10# Install all required packages to run the model# TODO: 1. Add any additional packages required to run your model (1)!RUNaptupdate&&aptinstall--yesffmpeglibsm6libxext6
...
Add ffmpeg, libsm6 and libxext6 to the list of packages to install.
First, if you don't have the file already, download the sample-service-without-model.yml file from the GitHub repository and rename it to image-rotate.yml in the .github/workflows folder.
Open it with your IDE and modify the sample-service texts with image-rotate
# Documentation: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses# TODO: 1. CHANGE THE NAME AND THE RUN NAME (1)!name:image-rotate_workflowrun-name:image-rotate workflow# Allow one concurrent deploymentconcurrency:# TODO: 2. CHANGE THE GROUP NAME (2)!group:"image-rotate"cancel-in-progress:trueon:push:paths:-.github/actions/build-and-push-docker-image-to-github/action.yml-.github/actions/execute-command-on-kubernetes-cluster/action.yml-.github/actions/test-python-app/action.yml# TODO: 3. CHANGE THE WORKFLOW NAME (3)!-.github/workflows/image-rotate.yml# TODO: 4. CHANGE THE PATH TO THE PYTHON APP (4)!-services/image-rotate/**/*# Allows you to run this workflow manually from the Actions tabworkflow_dispatch:jobs:run-workflow:runs-on:ubuntu-lateststeps:-name:Clone repositoryuses:actions/checkout@v3-name:Lint Python appuses:./.github/actions/lint-python-appwith:# TODO: 5. CHANGE THE PATH TO THE PYTHON APP (5)!python-app-path:./services/image-rotate-name:Test Python appuses:./.github/actions/test-python-appwith:# TODO: 6. CHANGE THE PATH TO THE PYTHON APP (6)!python-app-path:./services/image-rotate-name:Build and push Docker image to GitHubid:build-and-push-docker-image-to-github# Only run on mainif:github.ref == 'refs/heads/main'uses:./.github/actions/build-and-push-docker-image-to-githubwith:docker-registry-username:${{ github.actor }}docker-registry-password:${{ secrets.GITHUB_TOKEN }}# TODO: 7. CHANGE THE DOCKER IMAGE NAME (7)!docker-image-name:swiss-ai-center/image-rotate# TODO: 8. CHANGE THE PATH TO THE DOCKER IMAGE CONTEXT (8)!docker-image-context:./services/image-rotate-name:Prepare configuration files with secrets from GitHub Secrets# Only run on mainif:github.ref == 'refs/heads/main'shell:bash# TODO: 9. CHANGE THE PATH TO THE KUBERNETES CONFIGURATION FILES (9)!working-directory:services/image-rotate/kubernetesenv:ENVIRONMENT:productionLOG_LEVEL:infoENGINE_URLS:"'[\"https://core-engine-swiss-ai-center.kube.isc.heia-fr.ch\"]'"# TODO: 10. CHANGE THE URL OF THE SAMPLE SERVICE (10)!SERVICE_URL:https://image-rotate-swiss-ai-center.kube.isc.heia-fr.ch# TODO: 11. CHANGE THE NAME OF THE CONFIGURATION FILES (11)!run:|# Set image-rotate versiondocker_image_tags=(${{ steps.build-and-push-docker-image-to-github.outputs.docker-image-tags }})docker_image_sha_tag="${docker_image_tags[1]}"yq ".spec.template.spec.containers[0].image = \"$docker_image_sha_tag\"" image-rotate.stateful.yml > new-image-rotate.stateful.yml && mv new-image-rotate.stateful.yml image-rotate.stateful.yml# Set image-rotate configuration (ConfigMap)yq '.data = (.data | to_entries | map({"key": .key, "value": "${" + .key + "}"}) | from_entries)' image-rotate.config-map.yml | envsubst > new-image-rotate.config-map.yml && mv new-image-rotate.config-map.yml image-rotate.config-map.yml# Set image-rotate configuration (Ingress)yq ".spec.rules[0].host = \"${SERVICE_URL#*://}\"" image-rotate.ingress.yml > image-rotate.ingress.ymlyq ".spec.tls[0].hosts[0] = \"${SERVICE_URL#*://}\"" image-rotate.ingress.yml > image-rotate.ingress.yml# TODO: 12. CHANGE THE NAME OF THE ACTION (12)!-name:Deploy image-rotate on the Kubernetes cluster# Only run on mainif:github.ref == 'refs/heads/main'uses:./.github/actions/execute-command-on-kubernetes-clusterwith:kube-config:${{ secrets.KUBE_CONFIG }}kube-namespace:swiss-ai-center-prod# TODO: 13. CHANGE THE KUBERNETES CONTEXT (13)!kubectl-context:./services/image-rotate/kubernetes# TODO: 14. CHANGE THE PATH TO THE KUBERNETES CONFIGURATION FILES (14)!kubectl-args:|apply \-f image-rotate.config-map.yml \-f image-rotate.stateful.yml \-f image-rotate.service.yml \-f image-rotate.ingress.yml
Change the name and the run name of the workflow.
Change the group name.
Change the workflow name.
Change the path to the Python app.
Change the path to the Python app.
Change the path to the Python app.
Change the Docker image name.
Change the path to the Docker image context.
Change the path to the Kubernetes configuration files.
Change the URL of the sample service.
Change the name of the configuration files.
Change the name of the action.
Change the Kubernetes context.
Change the path to the Kubernetes configuration files.
Note
The host can be changed to your own domain name if the Service is deployed on another Kubernetes cluster.
INFO:Willwatchforchangesinthesedirectories:['/Users/andrea/Git/iCoSys/swiss-ai-center/services/image-rotate/src']INFO:Uvicornrunningonhttp://localhost:9393(PressCTRL+Ctoquit)INFO:Startedreloaderprocess[22602]usingStatReload
INFO:Startedserverprocess[22604]INFO:Waitingforapplicationstartup.
INFO:[2023-03-0111:14:17,950][common_code.service.service]:Startedtasksservice
DEBUG:[2023-03-0111:14:17,950][common_code.service.service]:Announcingservice:{'name':'Image Rotate','slug':'image-rotate','url':'http://localhost:9393','summary':'\nRotate an image by 90 degrees clockwise.\n','description':'\nRotate an image by 90 degrees clockwise depending on the value of the `rotation` parameter. (90, 180, 270)\n','status':'available','data_in_fields':[{'name':'image','type':['image/png','image/jpeg']},{'name':'rotation','type':['text/plain']}],'data_out_fields':[{'name':'result','type':['image/png','image/jpeg']}]}INFO:[2023-03-0111:14:17,953]Applicationstartupcomplete.
INFO:[2023-03-0111:14:18,005]127.0.0.1:54863-"GET /status HTTP/1.1"200OK
INFO:[2023-03-0111:14:18,023][common_code.service.service]:Successfullyannouncedtotheengine
Now, you can test the Service by sending a request to the Core Engine. To do so, open your browser and navigate to the following URL: http://localhost:8080/. You should see the following page:
Now you can test the Service by uploading an image and selecting the rotation. Create a file called rotation.txt and add the following content:
Now, you can unfold the /image-rotate endpoint and click on the Try it out button. Now upload the image and the rotation file and click on the Execute button. The response body should be something similar to the following:
{"created_at":"2023-03-01T10:59:41","updated_at":null,"data_in":["a38ef233-ac01-431d-adc8-cb6269cdeb71.png","a45f42d8-8750-4063-92f0-dd961558c489.txt"],"data_out":null,"status":"pending","service_id":"bcc6970e-8655-4173-a543-9da1cf2d0477","pipeline_id":null,"id":"20422a05-1f14-41b3-bee0-2c365451ce95","service":{"created_at":"2023-03-01T10:41:07","updated_at":"2023-03-01T10:59:33","description":"\nRotate an image by 90 degrees clockwise depending on the value of the `rotation` parameter. (90, 180, 270)\n","status":"available","data_in_fields":[{"name":"image","type":["image/png","image/jpeg"]},{"name":"rotation","type":["text/plain"]}],"data_out_fields":[{"name":"result","type":["image/png","image/jpeg"]}],"id":"bcc6970e-8655-4173-a543-9da1cf2d0477","name":"Image Rotate","slug":"image-rotate","url":"http://localhost:9393","summary":"\nRotate an image by 90 degrees clockwise.\n"},"pipeline":null}
Now, copy the id of the task and unfold the GET /tasks/{task_id} endpoint under the Tasks name.
Click on Try it out and paste the id in the task_id field.
Click on Execute.
In the body response, find the data_out field and copy the id of the image (e.g. a38ef233-ac01-431d-adc8-cb6269cdeb71.png).
Now, unfold the GET /storage/{key} endpoint under the Storage name.
Click on Try it out and paste the id of the image in the key field.
Click on Execute.
Click on the Download file button and save the image in your computer.
The image should be rotated by 90 degrees.
Congratulations!
You have successfully created a Service and tested it locally. Now, you can push the Service to GitHub and deploy it on the Core Engine using the workflow created in the previous section.
This will install the default Service dependencies and the ones we just added. The freeze file will be used to ensure all the developers have the same dependencies.
# Anomaly detectionThis service detects anomalies in a time series.
_Check the [related documentation](https://swiss-ai-center.github.io/swiss-ai-center/reference/ae-ano-detection) for more information._
[project]# TODO: 1. CHANGE THE NAME OF THE PROJECT (1)!name="ano-detection"[tool.pytest.ini_options]pythonpath=[".","src"]addopts="--cov-config=.coveragerc --cov-report xml --cov-report term-missing --cov=./src"
Change the name of the project to ano-detection.
2.4.3 Update the service kubernetes configuration¶
In the kubernetes folder, you will find the configuration files for the Service.
Rename all the files by replacing sample-service with ano-detection.
apiVersion:v1kind:ConfigMapmetadata:# TODO: 1. CHANGE THE NAME OF THE CONFIG MAP (1)!name:ano-detection-configlabels:# TODO: 2. CHANGE THE APP LABEL (2)!app:ano-detectiondata:ENVIRONMENT:developmentLOG_LEVEL:debugENGINE_URLS:'["http://core-engine-service:8080"]'# TODO: 3. CHANGE THE SERVICE URL (3)!# (the port must be the same as in the sample-service.service.yml and unused by other services)SERVICE_URL:http://ano-detection-service:8001
Change the name of the config map to ano-detection-config
Change the app label to ano-detection
Change the service url to http://ano-detection-service:8001. The port must be the same as in the ano-detection.service.yaml and unused by other services. (this is for local development only)
Open the ano-detection.ingress.yaml file and update sample-service with ano-detection.
apiVersion:networking.k8s.io/v1kind:Ingressmetadata:# TODO: 1. CHANGE THE NAME OF THE INGRESS (1)!name:ano-detection-ingressannotations:nginx.ingress.kubernetes.io/proxy-body-size:"16m"nginx.org/client-max-body-size:"16m"spec:rules:# TODO: 2. CHANGE THE HOST (2)!-host:ano-detection-swiss-ai-center.kube.isc.heia-fr.chhttp:paths:-path:/pathType:Prefixbackend:service:# TODO: 3. CHANGE THE NAME OF THE SERVICE (3)!name:ano-detection-serviceport:number:80tls:-hosts:# TODO: 4. CHANGE THE HOST (4)!-ano-detection-swiss-ai-center.kube.isc.heia-fr.ch
Change the name of the ingress to ano-detection-ingress
Change the host to ano-detection-swiss-ai-center.kube.isc.heia-fr.ch
Change the name of the service to ano-detection-service
Change the host to ano-detection-swiss-ai-center.kube.isc.heia-fr.ch
Note
The host can be changed to your own domain name if the Service is deployed on another Kubernetes cluster.
Open the ano-detection.service.yaml file and update sample-service with ano-detection.
apiVersion:v1kind:Servicemetadata:# TODO: 1. CHANGE THE NAME OF THE SERVICE (1)!name:ano-detection-servicespec:type:LoadBalancerports:-name:http# TODO: 2. CHANGE THE PORT (must be the same as in the sample-service.config-map.yml) (2)!port:8001targetPort:80protocol:TCPselector:# TODO: 3. CHANGE THE APP LABEL (3)!app:ano-detection
Change the name of the service to ano-detection-service
Change the port to 8001. The port must be the same as in the ano-detection.config-map.yaml and unused by other services. (this is for local development only)
Change the app label to ano-detection
Open the ano-detection.stateful.yaml file and update sample-service with ano-detection.
apiVersion:apps/v1kind:StatefulSetmetadata:# This name uniquely identifies the stateful set# TODO: 1. CHANGE THE NAME OF THE STATEFUL SET (1)!name:sample-service-statefullabels:# TODO: 2. CHANGE THE APP LABEL (2)!app:sample-servicespec:# TODO: 3. CHANGE THE NAME OF THE SERVICE (3)!serviceName:sample-servicereplicas:1selector:matchLabels:# TODO: 4. CHANGE THE APP LABEL (4)!app:sample-servicetemplate:metadata:labels:# TODO: 5. CHANGE THE APP LABEL (5)!app:sample-servicespec:containers:# TODO: 6. CHANGE THE NAME OF THE CONTAINER (6)!-name:sample-service# TODO: 7. CHANGE THE IMAGE NAME (7)!image:ghcr.io/swiss-ai-center/sample-service:latest# If you build the image locally, change the next line to `imagePullPolicy: Never` - there is no need to pull the imageimagePullPolicy:Alwaysports:-name:httpcontainerPort:80env:-name:MAX_TASKSvalue:"50"-name:ENGINE_ANNOUNCE_RETRIESvalue:"5"-name:ENGINE_ANNOUNCE_RETRY_DELAYvalue:"3"envFrom:-configMapRef:# TODO: 8. CHANGE THE NAME OF THE CONFIG MAP (8)!name:sample-service-config
Change the name of the stateful set to ano-detection-stateful
Change the app label to ano-detection
Change the name of the service to ano-detection
Change the app label to ano-detection
Change the app label to ano-detection
Change the name of the container to ano-detection
Change the image name to ghcr.io/swiss-ai-center/ano-detection-service:latest
Change the name of the config map to ano-detection-config
TODOs
When you are done, you need to remove all the TODOs from the files.
First open the .env file and update the SERVICE_URL variable to http://localhost:8001. The port must be the same as in the ano-detection.config-map.yaml file.
# Log levelLOG_LEVEL=debug
# EnvironmentENVIRONMENT=development
# The engines URLENGINE_URLS=["http://localhost:8080"]# The Service URL# TODO: 1. REPLACE THE PORT WITH THE SAME AS IN THE CONFIG-MAP FILE (1)!SERVICE_URL="http://localhost:8001"# The maximum of tasks the service can processMAX_TASKS=50# The number of times the service tries to announce itself to the enginesENGINE_ANNOUNCE_RETRIES=5# The number of seconds between each retryENGINE_ANNOUNCE_RETRY_DELAY=3
Replace the port with the same as in the ano-detection.config-map.yaml file.
All the code of the Service is in the main.py file.
Open the main.py with your favorite editor and follow the instructions below.
importasyncioimporttimefromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddlewarefromfastapi.responsesimportRedirectResponsefromcommon_code.configimportget_settingsfrompydanticimportFieldfromcommon_code.http_clientimportHttpClientfromcommon_code.logger.loggerimportget_loggerfromcommon_code.service.controllerimportrouterasservice_routerfromcommon_code.service.serviceimportServiceServicefromcommon_code.storage.serviceimportStorageServicefromcommon_code.tasks.controllerimportrouterastasks_routerfromcommon_code.tasks.serviceimportTasksServicefromcommon_code.service.modelsimportService,FieldDescriptionfromcommon_code.service.enumsimportServiceStatus,FieldDescriptionType# Imports required by the service's model# TODO: 1. ADD REQUIRED IMPORTS (ALSO IN THE REQUIREMENTS.TXT) (1)!importtensorflowastffrommatplotlibimportpyplotaspltimportnumpyasnpimportpandasaspdimportiosettings=get_settings()classMyService(Service):# TODO: 2. CHANGE THIS DESCRIPTION (2)!""" Anomaly Detection model """# Any additional fields must be excluded for Pydantic to workmodel:object=Field(exclude=True)def__init__(self):super().__init__(# TODO: 3. CHANGE THE SERVICE NAME AND SLUG (3)!name="Anomaly Detection",slug="ano-detection",url=settings.service_url,summary=api_summary,description=api_description,status=ServiceStatus.AVAILABLE,# TODO: 4. CHANGE THE INPUT AND OUTPUT FIELDS, THE TAGS AND THE HAS_AI VARIABLE (4)!data_in_fields=[FieldDescription(name="text",type=[FieldDescriptionType.TEXT_CSV,FieldDescriptionType.TEXT_PLAIN]),],data_out_fields=[FieldDescription(name="result",type=[FieldDescriptionType.IMAGE_PNG]),],tags=[ExecutionUnitTag(name=ExecutionUnitTagName.ANOMALY_DETECTION,acronym=ExecutionUnitTagAcronym.ANOMALY_DETECTION),ExecutionUnitTag(name=ExecutionUnitTagName.TIME_SERIES,acronym=ExecutionUnitTagAcronym.TIME_SERIES),],has_ai=True,)self.model=tf.keras.models.load_model("../model/ae_model.h5")# TODO: 5. CHANGE THE PROCESS METHOD (CORE OF THE SERVICE) (5)!asyncdefprocess(self,data):# NOTE that the data is a dictionary with the keys being the field names set in the data_in_fieldsraw=str(data["text"].data)[2:-1]raw=raw.replace('\\t',',').replace('\\n','\n').replace('\\r','\n')X_test=pd.read_csv(io.StringIO(raw),dtype={"value":np.float64})# Use the model to reconstruct the original time series datareconstructed_X=self.model.predict(X_test)# Calculate the reconstruction error for each point in the time seriesreconstruction_error=np.square(X_test-reconstructed_X).mean(axis=1)err=X_testfig,ax=plt.subplots(figsize=(20,6))a=err.loc[reconstruction_error>=np.max(reconstruction_error)]# anomalyax.plot(err,color='blue',label='Normal')ax.scatter(a.index,a,color='red',label='Anomaly')plt.legend()buf=io.BytesIO()plt.savefig(buf,format='png')buf.seek(0)# NOTE that the result must be a dictionary with the keys being the field names set in the data_out_fieldsreturn{"result":TaskData(data=buf.read(),type=FieldDescriptionType.IMAGE_PNG)}# TODO: 6. CHANGE THE API DESCRIPTION AND SUMMARY (6)!api_description="""Anomaly detection of a time series with an autoencoder"""api_summary="""Anomaly detection of a time series with an autoencoder"""# Define the FastAPI application with information# TODO: 7. CHANGE THE API TITLE, VERSION, CONTACT AND LICENSE (7)!app=FastAPI(title="Anomaly Detection API.",description=api_description,version="1.0.0",contact={"name":"Swiss AI Center","url":"https://swiss-ai-center.ch/","email":"ia.recherche@hes-so.ch",},swagger_ui_parameters={"tagsSorter":"alpha","operationsSorter":"method",},license_info={"name":"GNU Affero General Public License v3.0 (GNU AGPLv3)","url":"https://choosealicense.com/licenses/agpl-3.0/",},)...
Import the library.
Change the description of the service.
Change the name and the slug of the service. This is used to identify the service in the Core Engine.
Change the input/output fields of the service. The name of the field is the key of the dictionary that will be used in the process function. The type of the field is the type of the data that will be sent to the service. They are defined in the FieldDescriptionType enum. The tags are used to identify the service in the Core Engine. The has_ai variable is used to identify if the service is an AI service.
Change the process function. This is the core of the service. The data is a dictionary with the keys being the field names set in the data_in_fields. The result must be a dictionary with the keys being the field names set in the data_out_fields.
Change the API description and summary.
Change the API title, version, contact and license.
First, if you don't have the file already, download the sample-service-without-model.yml file from the GitHub repository and rename it to ano-detection.yml in the .github/workflows folder.
Open it with your IDE and modify the sample-service texts with ano-detection
# Documentation: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses# TODO: 1. CHANGE THE NAME AND THE RUN NAME (1)!name:ano-detection_workflowrun-name:ano-detection workflow# Allow one concurrent deploymentconcurrency:# TODO: 2. CHANGE THE GROUP NAME (2)!group:"ano-detection"cancel-in-progress:trueon:push:paths:-.github/actions/build-and-push-docker-image-to-github/action.yml-.github/actions/execute-command-on-kubernetes-cluster/action.yml-.github/actions/test-python-app/action.yml# TODO: 3. CHANGE THE WORKFLOW NAME (3)!-.github/workflows/ano-detection.yml# TODO: 4. CHANGE THE PATH TO THE PYTHON APP (4)!-services/ano-detection/**/*# Allows you to run this workflow manually from the Actions tabworkflow_dispatch:jobs:run-workflow:runs-on:ubuntu-lateststeps:-name:Clone repositoryuses:actions/checkout@v3-name:Lint Python appuses:./.github/actions/lint-python-appwith:# TODO: 5. CHANGE THE PATH TO THE PYTHON APP (5)!python-app-path:./services/ano-detection-name:Test Python appuses:./.github/actions/test-python-appwith:# TODO: 6. CHANGE THE PATH TO THE PYTHON APP (6)!python-app-path:./services/ano-detection-name:Build and push Docker image to GitHubid:build-and-push-docker-image-to-github# Only run on mainif:github.ref == 'refs/heads/main'uses:./.github/actions/build-and-push-docker-image-to-githubwith:docker-registry-username:${{ github.actor }}docker-registry-password:${{ secrets.GITHUB_TOKEN }}# TODO: 7. CHANGE THE DOCKER IMAGE NAME (7)!docker-image-name:swiss-ai-center/ano-detection# TODO: 8. CHANGE THE PATH TO THE DOCKER IMAGE CONTEXT (8)!docker-image-context:./services/ano-detection-name:Prepare configuration files with secrets from GitHub Secrets# Only run on mainif:github.ref == 'refs/heads/main'shell:bash# TODO: 9. CHANGE THE PATH TO THE KUBERNETES CONFIGURATION FILES (9)!working-directory:services/ano-detection/kubernetesenv:ENVIRONMENT:productionLOG_LEVEL:infoENGINE_URLS:"'[\"https://core-engine-swiss-ai-center.kube.isc.heia-fr.ch\"]'"# TODO: 10. CHANGE THE URL OF THE SAMPLE SERVICE (10)!SERVICE_URL:https://ano-detection-swiss-ai-center.kube.isc.heia-fr.ch# TODO: 11. CHANGE THE NAME OF THE CONFIGURATION FILES (11)!run:|# Set ano-detection versiondocker_image_tags=(${{ steps.build-and-push-docker-image-to-github.outputs.docker-image-tags }})docker_image_sha_tag="${docker_image_tags[1]}"yq ".spec.template.spec.containers[0].image = \"$docker_image_sha_tag\"" ano-detection.stateful.yml > new-ano-detection.stateful.yml && mv new-ano-detection.stateful.yml ano-detection.stateful.yml# Set ano-detection configurationyq '.data = (.data | to_entries | map({"key": .key, "value": "${" + .key + "}"}) | from_entries)' ano-detection.config-map.yml | envsubst > new-ano-detection.config-map.yml && mv new-ano-detection.config-map.yml ano-detection.config-map.yml# TODO: 12. CHANGE THE NAME OF THE ACTION (12)!-name:Deploy ano-detection on the Kubernetes cluster# Only run on mainif:github.ref == 'refs/heads/main'uses:./.github/actions/execute-command-on-kubernetes-clusterwith:kube-config:${{ secrets.KUBE_CONFIG }}kube-namespace:swiss-ai-center-prod# TODO: 13. CHANGE THE KUBERNETES CONTEXT (13)!kubectl-context:./services/ano-detection/kubernetes# TODO: 14. CHANGE THE PATH TO THE KUBERNETES CONFIGURATION FILES (14)!kubectl-args:|apply \-f ano-detection.config-map.yml \-f ano-detection.stateful.yml \-f ano-detection.service.yml \-f ano-detection.ingress.yml
Change the name and the run name of the workflow.
Change the group name.
Change the workflow name.
Change the path to the Python app.
Change the path to the Python app.
Change the path to the Python app.
Change the Docker image name.
Change the path to the Docker image context.
Change the path to the Kubernetes configuration files.
Change the URL of the sample service.
Change the name of the configuration files.
Change the name of the action.
Change the Kubernetes context.
Change the path to the Kubernetes configuration files.
Note
The host can be changed to your own domain name if the Service is deployed on another Kubernetes cluster.
Congratulations!
You have successfully created a Service locally. Now, you can push the Service to GitHub and deploy it on the Core Engine using the workflow created in the previous section.