InnerLoop Development with Python

1. Overview

This lab demonstrates features and capabilities designed to streamline the development workflow for software engineers tasked with developing Python applications in a containerized environment. Typical container development requires the user to understand details of containers and the container build process. Additionally, developers typically have to break their flow, moving out of their IDE to test and debug their applications in remote environments. With the tools and technologies mentioned in this tutorial, developers can work effectively with containerized applications without leaving their IDE.

What you will learn

In this lab you will learn methods for developing with containers in GCP including:

  • Creating a new Python starter application
  • Walk through the development process
  • Develop a simple CRUD rest service

2. Setup and Requirements

Self-paced environment setup

  1. Sign-in to the Google Cloud Console and create a new project or reuse an existing one. If you don't already have a Gmail or Google Workspace account, you must create one.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • The Project name is the display name for this project's participants. It is a character string not used by Google APIs, and you can update it at any time.
  • The Project ID must be unique across all Google Cloud projects and is immutable (cannot be changed after it has been set). The Cloud Console auto-generates a unique string; usually you don't care what it is. In most codelabs, you'll need to reference the Project ID (and it is typically identified as PROJECT_ID), so if you don't like it, generate another random one, or, you can try your own and see if it's available. Then it's "frozen" after the project is created.
  • There is a third value, a Project Number which some APIs use. Learn more about all three of these values in the documentation.
  1. Next, you'll need to enable billing in the Cloud Console in order to use Cloud resources/APIs. Running through this codelab shouldn't cost much, if anything at all. To shut down resources so you don't incur billing beyond this tutorial, follow any "clean-up" instructions found at the end of the codelab. New users of Google Cloud are eligible for the $300 USD Free Trial program.

Start Cloudshell Editor

This lab was designed and tested for use with Google Cloud Shell Editor. To access the editor,

  1. access your google project at https://console.cloud.google.com.
  2. In the top right corner click on the cloud shell editor icon

8560cc8d45e8c112.png

  1. A new pane will open in the bottom of your window
  2. Click on the Open Editor button

9e504cb98a6a8005.png

  1. The editor will open with an explorer on the right and editor in the central area
  2. A terminal pane should also be available in the bottom of the screen
  3. If the terminal is NOT open use the key combination of `ctrl+`` to open a new terminal window

Environment Setup

In Cloud Shell, set your project ID and the project number for your project. Save them as PROJECT_ID and PROJECT_ID variables.

export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID \
    --format='value(projectNumber)')

Get the source code

  1. The source code for this lab is located in the container-developer-workshop in GoogleCloudPlatform on GitHub. Clone it with the command below then change into the directory.
git clone https://github.com/cgrant/container-developer-workshop.git -b innerloop-python &&
cd container-developer-workshop/labs/python
mkdir music-service && cd music-service && cloudshell workspace .

If the terminal is NOT open use the key combination of `ctrl+`` to open a new terminal window

Provision the infrastructure used in this lab

In this lab you will deploy code to GKE and access data stored in a Spanner database. The setup script below prepares this infrastructure for you. The provisioning process will take over 10 minutes. You can continue with the next few steps while the setup is processing.

../setup.sh

3. Create a new Python starter application

  1. Create a file called requirements.txt and copy the following contents into it
Flask
gunicorn
google-cloud-spanner
ptvsd==4.3.2
  1. Create a file named app.py and paste the following code into it
import os
from flask import Flask, request, jsonify
from google.cloud import spanner

app = Flask(__name__)

@app.route("/")
def hello_world():
    message="Hello, World!"
    return message

if __name__ == '__main__':
    server_port = os.environ.get('PORT', '8080')
    app.run(debug=False, port=server_port, host='0.0.0.0')

  1. Create a file named Dockerfile and paste the following into it
FROM python:3.8
WORKDIR /app
COPY requirements.txt .
RUN pip install --trusted-host pypi.python.org -r requirements.txt
COPY . .
ENTRYPOINT ["python", "app.py"]

Generate Manifests

In your terminal execute the following command to generate a default skaffold.yaml and deployment.yaml

  1. Initialize Skaffold with the following command
skaffold init --generate-manifests

When prompted use the arrows to move your cursor and the spacebar to select the options.

Choose:

  • 8080 for the port
  • y to save the configuration

Update Skaffold Configurations

  1. Change default application name
  • Open skaffold.yaml
  • Select the image name currently set as dockerfile-image
  • Right click and choose Change All Occurrences
  • Type in the new name as python-app

Modify Kubernetes Configuration File

  1. Change the default Name
  • Open deployment.yaml file
  • Select the image name currently set as dockerfile-image
  • Right click and choose Change All Occurrences
  • Type in the new name as python-app

4. Walking through the development process

With the business logic added you can now deploy and test your application. The following section will highlight the use of the Cloud Code plugin. Among other things, this plugin integrates with skaffold to streamline your development process. When you deploy to GKE in the following steps, Cloud Code and Skaffold will automatically build your container image, push it to a Container Registry, and then deploy your application to GKE. This happens behind the scenes abstracting the details away from the developer flow.

Deploy to Kubernetes

  1. In the pane at the bottom of Cloud Shell Editor, select Cloud Code 

fdc797a769040839.png

  1. In the panel that appears at the top, select Run on Kubernetes.. If prompted, select Yes to use the current Kubernetes context.

cfce0d11ef307087.png

This command starts a build of the source code and then runs the tests. The build and tests will take a few minutes to run. These tests include unit tests and a validation step that checks the rules that are set for the deployment environment. This validation step is already configured, and it ensures that you get warning of deployment issues even while you're still working in your development environment.

  1. The first time you run the command a prompt will appear at the top of the screen asking if you want the current kubernetes context, select "Yes" to accept and use the current context.
  2. Next a prompt will be displayed asking which container registry to use. Press enter to accept the default value provided
  3. Select the Output tab in the lower pane to view progress and notifications

f95b620569ba96c5.png

  1. Select "Kubernetes: Run/Debug - Detailed" in the channel drop down to the right to view additional details and logs streaming live from the containers

94acdcdda6d2108.png

When the build and tests are done, the Output tab says: Attached debugger to container "python-app-8476f4bbc-h6dsl" successfully., and the URL http://localhost:8080 is listed.

  1. In the Cloud Code terminal, hover over the first URL in the output (http://localhost:8080), and then in the tool tip that appears select Open Web Preview.
  2. A new browser tab will open and display the message Hello, World!

Hot Reload

  1. Open the app.py file
  2. Change the greeting message to Hello from Python

Notice immediately that in the Output window, Kubernetes: Run/Debug view, the watcher syncs the updated files with the container in Kubernetes

Update initiated
Build started for artifact python-app
Build completed for artifact python-app

Deploy started
Deploy completed

Status check started
Resource pod/python-app-6f646ffcbb-tn7qd status updated to In Progress
Resource deployment/python-app status updated to In Progress
Resource deployment/python-app status completed successfully
Status check succeeded
...
  1. If you switch to Kubernetes: Run/Debug - Detailed view, you will notice it recognizes file changes then builds and redeploys the app
files modified: [app.py]
Generating tags...
 - python-app -> gcr.io/crg-2022-04-11-python/python-app:a164811-dirty
Checking cache...
 - python-app: Not found. Building
Starting build...
  1. Refresh your browser to see the updated results.

Debugging

  1. Go to the Debug view and stop the current thread 647213126d7a4c7b.png.
  2. Click on Cloud Code in the bottom menu and select Debug on Kubernetes to run the application in debug mode.
  • In the Kubernetes Run/Debug - Detailed view of Output window, notice that skaffold will deploy this application in debug mode.
  1. The first time this is run a prompt will ask where the source is inside the container. This value is related to the directories in the Dockerfile.

Press Enter to accept the default

583436647752e410.png

It will take a couple of minutes for the application to build and deploy.

  1. When the process completes. You'll notice a debugger attached.
Port forwarding pod/python-app-8bd64cf8b-cskfl in namespace default, remote port 5678 -> http://127.0.0.1:5678
  1. The bottom status bar changes its color from blue to orange indicating that it is in Debug mode.
  2. In the Kubernetes Run/Debug view, notice that a Debuggable container is started
**************URLs*****************
Forwarded URL from service python-app: http://localhost:8080
Debuggable container started pod/python-app-8bd64cf8b-cskfl:python-app (default)
Update succeeded
***********************************

Utilize Breakpoints

  1. Open the app.py file
  2. Locate the statement which reads return message
  3. Add a breakpoint to that line by clicking the blank space to the left of the line number. A red indicator will show to note the breakpoint is set
  4. Reload your browser and note the debugger stops the process at the breakpoint and allows you to investigate the variables and state of the application which is running remotely in GKE
  5. Click down into the VARIABLES section
  6. Click Locals there you'll find the "message" variable.
  7. Double click on the variable name "message" and in the popup, change the value to something different like "Greetings from Python"
  8. Click the Continue button in the debug control panel 607c33934f8d6b39.png
  9. Review the response in your browser which now shows the updated value you just entered.
  10. Stop the "Debug" mode by pressing the stop button 647213126d7a4c7b.png and remove the breakpoint by clicking on the breakpoint again.

5. Developing a Simple CRUD Rest Service

At this point your application is fully configured for containerized development and you've walked through the basic development workflow with Cloud Code. In the following sections you practice what you've learned by adding rest service endpoints connecting to a managed database in Google Cloud.

Code the rest service

The code below creates a simple rest service that uses Spanner as the database backing the application. Create the application by copying the following code into your application.

  1. Create the main application by replacing app.py with the following contents
import os
from flask import Flask, request, jsonify
from google.cloud import spanner


app = Flask(__name__)


instance_id = "music-catalog"

database_id = "musicians"

spanner_client = spanner.Client()
instance = spanner_client.instance(instance_id)
database = instance.database(database_id)


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

@app.route('/singer', methods=['POST'])
def create():
    try:
        request_json = request.get_json()
        singer_id = request_json['singer_id']
        first_name = request_json['first_name']
        last_name = request_json['last_name']
        def insert_singers(transaction):
            row_ct = transaction.execute_update(
                f"INSERT Singers (SingerId, FirstName, LastName) VALUES" \
                f"({singer_id}, '{first_name}', '{last_name}')"
            )
            print("{} record(s) inserted.".format(row_ct))

        database.run_in_transaction(insert_singers)

        return {"Success": True}, 200
    except Exception as e:
        return e



@app.route('/singer', methods=['GET'])
def get_singer():

    try:
        singer_id = request.args.get('singer_id')
        def get_singer():
            first_name = ''
            last_name = ''
            with database.snapshot() as snapshot:
                results = snapshot.execute_sql(
                    f"SELECT SingerId, FirstName, LastName FROM Singers " \
                    f"where SingerId = {singer_id}",
                    )
                for row in results:
                    first_name = row[1]
                    last_name = row[2]
                return (first_name,last_name )
        first_name, last_name = get_singer()  
        return {"first_name": first_name, "last_name": last_name }, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['PUT'])
def update_singer_first_name():
    try:
        singer_id = request.args.get('singer_id')
        request_json = request.get_json()
        first_name = request_json['first_name']
        
        def update_singer(transaction):
            row_ct = transaction.execute_update(
                f"UPDATE Singers SET FirstName = '{first_name}' WHERE SingerId = {singer_id}"
            )

            print("{} record(s) updated.".format(row_ct))

        database.run_in_transaction(update_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e


@app.route('/singer', methods=['DELETE'])
def delete_singer():
    try:
        singer_id = request.args.get('singer')
    
        def delete_singer(transaction):
            row_ct = transaction.execute_update(
                f"DELETE FROM Singers WHERE SingerId = {singer_id}"
            )
            print("{} record(s) deleted.".format(row_ct))

        database.run_in_transaction(delete_singer)
        return {"Success": True}, 200
    except Exception as e:
        return e

port = int(os.environ.get('PORT', 8080))
if __name__ == '__main__':
    app.run(threaded=True, host='0.0.0.0', port=port)

Add Database Configurations

To connect to Spanner securely, set the application up to use Workload Identities. This enables your application to act as its own service account and have individual permissions when accessing the database.

  1. Update deployment.yaml. Add the following code at the end of the file (ensure you keep the tab indents in the example below)
      serviceAccountName: python-ksa
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true" 

Deploy and Validate Application

  1. In the pane at the bottom of Cloud Shell Editor, select Cloud Code then select Debug on Kubernetes at the top of the screen.
  2. When the build and tests are done, the Output tab says: Resource deployment/python-app status completed successfully, and a url is listed: "Forwarded URL from service python-app: http://localhost:8080"
  3. Add a couple of entries.

From cloudshell Terminal, run the command below

curl -X POST http://localhost:8080/singer -H 'Content-Type: application/json' -d '{"first_name":"Cat","last_name":"Meow", "singer_id": 6}'
  1. Test the GET by running the command below in the terminal
curl -X GET http://localhost:8080/singer?singer_id=6
  1. Test Delete: Now try to delete an entry by running the following command. Change the value of item-id if required.
curl -X DELETE http://localhost:8080/singer?singer_id=6
    This throws an error message
500 Internal Server Error

Identify and fix the issue

  1. Debug mode and find the issue. Here are some tips:
  • We know something is wrong with the DELETE as it is not returning the desired result. So you would set the breakpoint in app.js in the delete_singer method.
  • Run step by step execution and watch the variables at each step to observe the values of local variables in the left window.
  • To observe specific values such as singer_id and request.args in the add these variables to the Watch window.
  1. Notice that the value assigned to singer_id is None. Change the code to fix the issue.

The fixed code snippet would look like this.

@app.route('/delete-singer', methods=['DELETE', 'GET'])
def delete_singer():
    try:
        singer_id = request.args.get('singer_id')
  1. Once the application is restarted, test again by trying to delete.
  2. Stop the debugging session by clicking on the red square in the debug toolbar 647213126d7a4c7b.png

6. Cleanup

Congratulations! In this lab you've created a new Python application from scratch and configured it to work effectively with containers. You then deployed and debugged your application to a remote GKE cluster following the same developer flow found in traditional application stacks.

To clean up after completing the lab:

  1. Delete the files used in the lab
cd ~ && rm -rf container-developer-workshop
  1. Delete the project to remove all related infrastructure and resources