Tutorial
Deploy a Python-Flask web application by using IBM Cloud Code Engine
Learn how to create extensions for watsonx Assistant and other applications that support the OpenAPI 3.0 specificationTo augment services with AI and applications in general, it can be advantageous to agnostically call web services to achieve your results. In this tutorial, learn how to create and deploy a simple Flask Python web application by using IBM Cloud Code Engine. You see how easily you can deploy an app on IBM Cloud. Using a code engine lets you focus on your code instead of the complexities that are associated with deployment. The app's development effort demonstrates a way to make the code OpenAPI 3.0 specification-compliant. This allows the REST application to be called an extension. Tools such as IBM watsonx Assistant let you customize and enhance native IBM Watson Services to incorporate proprietary algorithms, making the IBM Service unique and sustainable.
For example, imagine that a company has a custom AI model (written in Python) to predict the largest loan that a person would be qualified to obtain. The company wants to inform prospective clients with the information through watsonx Assistant Q&A. Using various factors, the AI produces an amount, and this information is fed back to a person by using the watsonx Assistant, creating a personalized response to the inquiry.
When the model is deployed, it can be used in several situations and in combination with other AI algorithms. Such implementations can be integrated into several IBM Services such as watsonx Assistant and IBM Watson Discovery. Applications also can be used to augment embedded-AI, providing the proper level of processing needed to achieve an objective.
Within the application, fast responses to user queries are one factor to consider. To address speed, three techniques are used.
- Highly available containerized operations
- Asynchronous operations where possible
- Parallel processing where applicable
In addition to speed, complex and customized operations can be implemented immediately.
Learning objectives
Application build
To realize the objectives, the first order of business is to build a simple REST API that is highly available. This is done by using a Python Flask Web API and deployed onto IBM Cloud using Code Engine. I chose this process because of the rich AI and data processing features that are offered by the Python platform and the capabilities of Code Engine to let you focus on code instead of deployment.
Deploy a simple Flask Python web application using Code Engine
Each part of the build process includes methods to test operations before proceeding to the next section. This objective consists of five parts.
- Build a REST API by using Python Flask.
- Build and test a Docker container (Postman is an alternative to Docker).
- Push the container to the IBM Cloud Registry.
- Deploy and manage the application by using Code Engine.
- Be enabled for OpenAPI 3.0.
You can find the code for this process on GitHub.
Special notes
In the GitHub repository, you see three files that begin with "app" and with "Dockerfile." Only app.py and Dockerfile are used at any point in time. The files that contain "-simple" and "-openapi" are placeholders for the code that is required at various points in the tutorial.
app-simple.py and Dockerfile-simple: This is code without the OpenAPI 3.0 specification. You first construct the application without the OpenAPI 3.0 specification to use Code Engine with minimal complexity. This file should be copied into the app.py and Dockerfile, respectively, for the first four steps.
app-openapi.py and Dockerfile-openapi: This file contains the code using the OpenAPI 3.0 specification. The code should be copied into app.py and Dockerfile, respectively, when Step 6 is reached.
Prerequisites
- Deep programming experience to handle setting up local environments
- Access to an integrated development environment (IDE)
- An IBMid
This tutorial uses PyCharm to create the application. However, you can use whatever editor you feel most comfortable with.
Estimated time
It should take you approximately 30 minutes to complete the tutorial.
Steps
Step 1. Build a REST API using Python Flask
- Create a folder for the new application. Call it
byoai. Create a file called
app.py(this is considered a best practice), and enter the following code in the file.Note: The code uses the
@app.routedecorator to specify the available API calls and the code to handle the request. Assume that the site responds to<https://localhost:5000>and subsequently defined site branches.from flask import Flask, request, jsonify # Import necessary packages PORT = 5000 # Specify the port the server will respond to app = Flask(__name__) # Define the app variable languages = ["English", "Spanish", "French", "German", "Italian", "Portuguese", "Swedish"] @app.route("/") # Handle the site home address def home(): return jsonify({"status": "online"}) # Return site status of online @app.route("/languages") # Return list of languages def get_languages(): return jsonify({"languages": languages}) if __name__ == "__main__": # Start the server or listener operation app.run(debug=True, host="0.0.0.0", port=PORT)Create a file called
requirements.txtand paste in the following code. This file is used by the Dockerfile to pull in the Python dependencies.Flask==2.2.3 Flask-JSON==0.3.5Create a file called
runapp.shand paste in the following code. This file is used by the Docker container to start the Flask service.export FLASK_APP=app flask runCreate a file called
Dockerfileand paste in the following code. Note the use ofrequirements.txtwithin the file. Also, note pip3 imports include what is in the requirements.txt file. Note the use of ubi8 from Red Hat to provide container security. See Introducing the Red Hat Universal Base Image for more information.FROM registry.access.redhat.com/ubi8/python-39 # Add application sources with correct permissions for OpenShift USER 0 ADD app-src . RUN chown -R 1001:0 ./ USER 1001 WORKDIR /app COPY ./requirements.txt . RUN pip install -U "pip>=19.3.1" RUN pip3 install -r requirements.txt COPY . . ENV FLASK_APP=app EXPOSE 5000 CMD python app.py runserver 0.0.0.0:5000
Step 2. Perform operation checks
The workspace that is created should look like the list of folders as shown in the following image.

Run the application by opening a terminal instance and running python3 app.py.

The terminal responds with something similar to the following image. Notice that the server is running on
<http://127.0.0.1:5000>. Because the server is running, the prompt is no longer available.
Open a browser and test by using the following two URLs.
<http://localhost:5000><http://localhost:5000/languages>You should see the following output or something similar.
{ "status": "online" }{ "Language_list": [ { "language": "English" }, { "language": "Spanish" }, { "language": "French" }, { "language": "German" }, { "language": "Italian" }, { "language": "Portuguese" }, { "language": "Swedish" } ] }
Step 3. Build and test Docker container
After Docker is installed and other environmental requirements are satisfied, execute the following steps.
Run the following command to build the Docker image.
docker build -t byoai-rest-api .Run the container image using the following command.
docker run -p 5000:5000 -it byoai-rest-apiOpen a browser and test by using the following two URLs. Notice that I mapped the
localhostport of 8080 to the container's port of 5000.<http://localhost:8080><http://localhost:8080/languages>
You should see the following output.
{ "status": "online" }{ "languages": [ "English", "Spanish", "French", "German", "Italian", "Portuguese", "Swedish" ] }
Step 4. Push image to the IBM Container Registry
This process requires that the basic setup of the IBM container registry is already complete.
Log in to the IBM Cloud, and select the Container Registry resource.

Check the Container Registry namespace, and document the following information. The following information is an example of the information needed. Be sure to save the information so that it can be accessed later.
Notes:
- The repository corresponds to one program being deployed and each of its versions.
- Make sure you are in the wanted
Location. In this case, Dallas is the location of interest.
Registry Name: us.icr.io
Namespace Name: djb-ns
Resource Group: david-bacarell-rg
Common Path: us.icr.io/djb-ns/[repository-name] This is the Docker image that is pushed.
Find the local image to be pushed by using the following command on the local machine.
docker images | grep byoai-rest-apiThe output should be similar to the following output.
byoai-rest-api latest b8d9a08180e5 2 days ago 922MBTag the image for the destination repository. Set up environment variables for convenience (information is from Step 3).
export REGISTRY=us.icr.ioexport NAMESPACE=djb-nsNote: The name of the image that is created is the first parameter after the tag subcommand. Notice that the image is being tagged to create a repository with a versioned image.
docker tag byoai-rest-api ${REGISTRY}/${NAMESPACE}/byoai-rest-api:1.0Rerun the following command and observe the tagged image.
docker images | grep byoai-rest-apibyoai-rest-api latest b8d9a08180e5 2 days ago 922MB icr.io/djb-ns/byoai-rest-api 1.0 b8d9a08180e5 2 days ago 922MBLog in to the IBM Cloud CLI and repository.
a. Click the person icon, and then click Log in to CLI and API to get the CLI command to log in with. It looks similar to the following example.
ibmcloud login -a https://cloud.ibm.com -u passcode -p abcdefghij
b. Select the appropriate region from the list.
c. Set the target resource group.
ibmcloud target -g david-bacarella-rgd. Ensure that you are targeting the correct IBM Cloud Container Registry region.
ibmcloud cr region-set us-southe. Log in to the IBM Cloud Container Registry.
ibmcloud cr loginPush the tagged image to the registry that creates the repository. It creates the repository within the namespace that is in the registry.
docker push ${REGISTRY}/${NAMESPACE}/byoai-rest-api:1.0Check the IBM Cloud Registry. The uploaded container should be visible.

Step 5. Deploy and manage the application using Code Engine
In this section, it is assumed that you have access to and are familiar with the basic operations of IBM Cloud.
Open the IBM Cloud Code Engine, and select Overview.

Select Start creating.

Select Application, provide a unique name for the application, and either create a new project or select an existing one. This tutorial uses the preexisting project called code-engine-alpha.

Scroll to the next section as shown in the following image. Ensure that Container image is selected. Specify the image reference, and make sure that you specify the port exposed in the Dockerfile configuration file. The port is essential.
Select Configure image to the right the Image reference field. Recall the information that was collected in Step 4.
- Registry Name: us.icr.io
- Namespace Name: djb-ns
- Resource Group: david-bacarell-rg
Common Path: us.icr.io/djb-ns/[repository-name] This is the Docker image that is pushed.

Be sure to create a new registry access key, if needed. Select the option and follow the prompts. Ensure that the values for Registry server, Namespace, Repository, and Tag are what is expected. Then, click Done.

The application is deployed. Upon completion, test the application. Select Send request, and observe the response. An example of what you should see is shown in the following image. Note that the response is
{"status": "online}just as you saw in Step 1 of the procedure.
Click Open URL.

Add /languages to the end of the URL, and press Enter. Again, you should see something like the following output.
{"languages":["English","French","German","Italian","Portuguese","Swedish"]}
Step 6. Open API 3.0 enablement
Open API 3.0 enablement consists of augmenting the REST service to comply with the Open API 3.0 standard. Open API 3.0 (formerly known as Swagger) is a standard for computer-to-computer documentation of a REST interface. This allows programs to extend their capabilities with little to no code modifications. Watsonx Assistant provides an example of this. You can extend watsonx Assistant by using an Open API 3.0-compliant interface. The extension user needs to reference only one of the API routes directly in the Watson GUI.
To demonstrate, I'll augment the simple REST API developed earlier. You can use several available options to implement the Open API 3.0 specifications. This tutorial shows one way to do it.
Note: You can copy app-openapi.py to app.py and Dockerfile-openapi to Dockerfile, respectively. However, it's a good exercise to manually make these modifications so that you have a good basis for more complex configurations.
Add the required libraries. Each of the libraries serves a specific purpose in the creation of an Open API 3.0 specification.
**from** apispec **import** APISpec **from** apispec.ext.marshmallow **import** MarshmallowPlugin **from** apispec_webframeworks.flask **import** FlaskPlugin **from** flask **import** Flask, jsonify, render_template,send_from_directory **from** marshmallow **import** Schema, fields **from** werkzeug.utils **import** secure_filenameThe original code is provided at https://github.com/ibm-build-lab/Custom-Extensions/blob/main/BYO-AI/app-simple.py.
Change the
appvariable, and add thespecvariable, as shown in the following code.app = Flask(__name__, template_folder=*'./swagger/templates') spec = APISpec( title='flask-api-swagger-doc\', version='1.0.0', openapi_version='3.0.2', plugins=[FlaskPlugin(), MarshmallowPlugin()] )The code should look like the following example.
from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin from flask import Flask, jsonify, render_template, send_from_directory from marshmallow import Schema, fields from werkzeug.utils import secure_filename PORT = 5000 app = Flask(__name__, template_folder='./swagger/templates') spec = APISpec( title='flask-api-swagger-doc', version='1.0.0', openapi_version='3.0.2', plugins=[FlaskPlugin(), MarshmallowPlugin()] )Add routes for Open API access, as shown in the following code.
@app.route('/api/swagger.json') def create_swagger_spec(): return jsonify(spec.to_dict()) @app.route("/docs") @app.route("/docs/<path:path>") def swagger_docs(path=None): if not path or path == 'index.html': return render_template('index.html', base_url='/docs') else: return send_from_directory('./swagger/static', secure_filename(path))Add marshmallow support by adding the following section of code. Marshmallow is a Python package that specifically handles data serialization and deserialization.
# Marshmallow support class LanguageResponseSchema(Schema): language = fields.Str()Add a class definition for the language route response.
class LanguageResponse(Schema): language_list = fields.List(fields.Nested(LanguageResponseSchema))For each route, document the feature, as necessary. For this example, the specification is simple. However, more complex definitions are required for most applications. Leave the / route unaffected by change.
@app.route("/languages") def get_languages(): """Get list of languages --- get: description: Get list of languages responses: 200: description: Return a language list content: application/json: schema: LanguageResponse """ languages = [ {"language": "English"}, {"language": "Spanish"}, {"language": "French"}, {"language": "German"}, {"language": "Italian"}, {"language": "Portuguese"}, {"language": "Swedish"} ] return LanguageResponse().dump({"language_list": languages})Leave the following route that is unaffected by the change. When the code is tested, you see that this function works. However, it is not seen by the OpenAPI tools.
@app.route("/") def home(): return jsonify({ "status": "online" })Add the following code that registers the API. Note that the
viewvariable is given the value of the name of the function that is defined under the language route.# Register API with app.test_request_context(): spec.path(view=language_listing)Add the following code to the bottom of the file.
if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=PORT)Add a swagger subdirectory to your project. This should be created at the level where the app.py file resides. Under the swagger directory, create two directories named static and templates, respectively.
Note: Recall the following line of code.
app = Flask(__name__, template_folder='./swagger/templates')Notice that the directory structure that you just created is specified in the app definition.

Copy the files within the static and templates directories from this GitHub repository.
Modify the
./swagger/static/initializer.jsfile, and set the URL to the URL where the code will run. In this case, the code is configured to run on a local machine.window.onload = function() { //<editor-fold desc="Changeable Configuration Block"> // the following lines will be replaced by docker/configurator, when it runs in a docker-container window.ui = SwaggerUIBundle({ url: "http://localhost:5000/api/swagger.json", dom_id: '#swagger-ui', deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }); //</editor-fold> };Modify the
./swagger/templates/index.htmlfile as shown in the following code.<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Swagger UI</title> <link rel="stylesheet" type="text/css" href="{{base_url}}/swagger-ui.css" /> <link rel="stylesheet" type="text/css" href="{{base_url}}/index.css" /> <link rel="icon" type="image/png" href="{{base_url}}/favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="{{base_url}}/favicon-16x16.png" sizes="16x16" /> </head> <body> <div id="swagger-ui"></div> <script src="{{base_url}}/swagger-ui-bundle.js" charset="UTF-8"> </script> <script src="{{base_url}}/swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script src="{{base_url}}/swagger-initializer.js" charset="UTF-8"> </script> </body> </html>Test it by running the application and then accessing the following links by using a browser. I have provided sample screenshots.
a.
http://localhost:5000/This route is unknown to the OpenAPI tooling. Note that it is still responsive.

b.
<http://localhost:5000/languages>This link provides the results for the main route of the application.

c.
http://localhost:5000/docsEnter the world of swagger.

d. Click Get, then click Try it out. Afterward, click Execute.

Deploy the application using Steps 1 through 5 to see it working on IBM Cloud. You can change the tag to 2.0 to retain both versions of the container.
Summary
Now that you have successfully created an OpenAPI 3.x-compliant RESTful web application, you have the tools necessary for more complex applications. If you modify the base code and perform these steps, you see how quickly you can get your changes into the cloud environment. This capability is applicable for IBM watsonx Orchestrate and the watsonx.ai platform. The power of this feature enables innumerable capabilities that are limited only by practicality and imagination.
Next steps
You might want to experiment with the code-to-deployment feature that handles the container creation piece for you. Also, there existing extensions that you might find useful. See the documentation for more information.