IBM Developer

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 specification

By David Bacarella

To 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.

  1. Highly available containerized operations
  2. Asynchronous operations where possible
  3. 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.

  1. Build a REST API by using Python Flask.
  2. Build and test a Docker container (Postman is an alternative to Docker).
  3. Push the container to the IBM Cloud Registry.
  4. Deploy and manage the application by using Code Engine.
  5. 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

  1. Create a folder for the new application. Call it byoai.
  2. 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.route decorator 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)
    
  3. Create a file called requirements.txt and 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.5
    
  4. Create a file called runapp.sh and paste in the following code. This file is used by the Docker container to start the Flask service.

    export FLASK_APP=app  
    flask run
    
  5. Create a file called Dockerfile and paste in the following code. Note the use of requirements.txt within 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

  1. The workspace that is created should look like the list of folders as shown in the following image.

    Workspace creation

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

    Run the application

  3. 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.

    Terminal response

  4. 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.

  1. Run the following command to build the Docker image.

    docker build -t byoai-rest-api .
    
  2. Run the container image using the following command.

    docker run -p 5000:5000 -it byoai-rest-api
    
  3. Open a browser and test by using the following two URLs. Notice that I mapped the localhost port 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.

  1. Log in to the IBM Cloud, and select the Container Registry resource.

    Container Registry

  2. 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.

    Container Registry namespace

  3. Find the local image to be pushed by using the following command on the local machine.

    docker images | grep byoai-rest-api
    

    The output should be similar to the following output.

    byoai-rest-api           latest                     b8d9a08180e5   2 days ago      922MB
    
  4. Tag the image for the destination repository. Set up environment variables for convenience (information is from Step 3).

    export REGISTRY=us.icr.io
    
    export NAMESPACE=djb-ns
    

    Note: 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.0
    
  5. Rerun the following command and observe the tagged image.

    docker images | grep byoai-rest-api
    
    byoai-rest-api                 latest  b8d9a08180e5   2 days ago      922MB
    icr.io/djb-ns/byoai-rest-api     1.0   b8d9a08180e5   2 days ago      922MB
    
  6. Log 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
    

    Namespaces

    b. Select the appropriate region from the list.

    c. Set the target resource group.

       ibmcloud target -g david-bacarella-rg
    

    d. Ensure that you are targeting the correct IBM Cloud Container Registry region.

       ibmcloud cr region-set us-south
    

    e. Log in to the IBM Cloud Container Registry.

       ibmcloud cr login
    
  7. Push 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.0
    
  8. Check the IBM Cloud Registry. The uploaded container should be visible.

    Uploaded container

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.

  1. Open the IBM Cloud Code Engine, and select Overview.

    Open Cloud Engine

  2. Select Start creating.

    Start creating option

  3. 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.

    Creating application

  4. 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.

  5. 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.

      Configure image

  6. 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.

    Creating access key

  7. 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.

    Response

  8. Click Open URL.

    Opening the URL

  9. 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.

  1. 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_filename
    

    The original code is provided at https://github.com/ibm-build-lab/Custom-Extensions/blob/main/BYO-AI/app-simple.py.

  2. Change the app variable, and add the spec variable, 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()]
        )
    
  3. 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))
    
  4. 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()
    
  5. Add a class definition for the language route response.

    class LanguageResponse(Schema):
        language_list = fields.List(fields.Nested(LanguageResponseSchema))
    
  6. 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})
    
  7. 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"
            })
    
  8. Add the following code that registers the API. Note that the view variable 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)
    
  9. Add the following code to the bottom of the file.

        if __name__ == "__main__":
            app.run(debug=True, host="0.0.0.0", port=PORT)
    
  10. 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.

    App definition

  11. Copy the files within the static and templates directories from this GitHub repository.

  12. Modify the ./swagger/static/initializer.js file, 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>
        };
    
  13. Modify the ./swagger/templates/index.html file 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>
    
  14. 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.

    Route unknown

    b. <http://localhost:5000/languages>

    This link provides the results for the main route of the application.

    Main route

    c. http://localhost:5000/docs

    Enter the world of swagger.

    Swagger

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

    Executing

  15. 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.