Developing a Simple Python Application and Automating the CI/CD and Containerization Process [Part 1]
Setting up the Python Application
We’re living in a world where cars are learning to drive by themselves and robots are performing intricate surgeries with one hand. Automation and easing manual labor have been made top priority by industries everywhere now, but this idea has been blooming in the IT industry for at least the past couple of decades, enhancing itself into so many different tools that are widely used by Devops engineers and developers today. Not only does automation help reduce the physical effort required from IT personnel, it also defines a way in which processes should be set up to make them execute in the most efficient manner.
There are many steps involved in a development and devops lifecycle — from writing the code, testing it, building it into a container image, containerizing it, to finally orchestrating the container workload onto a Kubernetes cluster. Doing all these steps manually in order to implement the continuous integration and continuous deployment (CI/CD) workflow would be tedious for the developer, and additionally a waste of time given that there are a vast set of tools out there to automate this process now. In this series of articles, I’ll be walking you through a quick tutorial on how to use some of these tools to integrate a simple application built in Python into a comprehensive CI/CD framework, that will orchestrate the application workload onto an Azure Kubernetes Service managed cluster. Just a quick disclaimer here — these tutorials are meant to serve as a reference for junior developers to set up simple applications and integrate them into IAC, the cloud, and Kubernetes. That by all means doesn’t exclude experienced software engineers from the reading community; feel free to scan these tutorials if they interest you!
This first article will focus on developing the actual application using Python, detailing on the classes and methods used to accomplish a highly scaled down online banking system functionality. When we think of a simple online banking system, we imagine that it would have some basic features to deposit and withdraw money, check balance, add new customers, offer customer support, etc. In this application, we will be creating methods to accomplish each of these functionalities, in addition to showing transaction history for the chosen customer, under the umbrella of a bankAccount class. We will also create another class called debitCard which provisions debit cards to the customer as soon as their account has been created anew and entered into the system. The purpose of this tutorial will be to show the overall CI/CD workflow, so we will execute this application as a terminal app with an interactive interface, instead of adopting the more intricate approach of setting up a web application on the cloud and using API calls. Wait for my next series of articles to learn how that stuff works 😉
Let’s start with setting up a class called banking_app that can offer an interactive user interface so that the customer using this banking system will be able to choose from a set of menu options. Figure 1 below represents the main code that will be executed within this class to accept user input and redirect to the appropriate option.
Now, let’s break this code down. I’ve set up a while loop that uses a boolean flag called validate_user_choice_flag to run a block of code that offers a welcome statement as well as six options to the customer to choose from. As long as this flag has a value of True, the option that the customer enters is saved into the user_choice variable as an integer by using the Python input and int functions. We will perform a simple check on this variable to make sure that the input entered was a number within the range of 1–6. If it is not within this range or not a number, then we will keep the invalid_choice flag as True and provide the customer two more chances to rectify their selection. This is done through the use of the count variable, that increments everytime the user enters a new input. If the user has entered an integer input, but the choice was option 6, then the flag validate_user_choice_flag gets set to False, ending the while loop and exiting out of the interactive menu. If the user has entered a valid integer input that is not option 6, then we can continue the while loop, setting the invalid_choice flag to False. Lastly, we can see the addition of two functions in this block of code: validate_user_choice() and connect_to_sql(). We will delve further into the purpose of these two functions now.
Every time the while loop discussed above is executed, a call to the validate_user_choice() function is made. This method does exactly what its name suggests: validates the user choice so it can either be redirected to the correct sub function or exit from the application code (if the choice was invalid). Figure 2 below shows a snippet of this function’s code, which we will give an overview about now.
The first segment of this code performs a check to make sure that the user had entered a valid choice in the previous while loop, by assessing the value of the invalid_choice flag. If this flag is set to False, the application proceeds to step through the function code, else it exits out as the number of attempts to access the system have been exhausted. In the event of a positive response, the code continues to validate the user’s choice by checking the number entered against each option. In Figure 2, two out of the six possible choices have been shown, pertaining to deposits and withdrawals. Let’s peruse through one of these for better clarity. If the user entered option 1, corresponding to depositing money, we will present them with a message that will acknowledge this fact and request their debit card number. We will discuss more about the debit card class in just a little bit, but for now let’s march forward with the user providing this card number as an input. The code creates a standard message that assumes there is no account registered to this card. To alter this message and possibly deposit money upon finding a bank account associated with this card, we must step through a for loop that goes through a list of bank accounts and compares each one’s debit card number property against the card number entered by the user. If a match is found, the message is altered to reflect this change and a correspondence asking the user to enter the amount of money to be deposited is displayed. The amount entered by the user is then converted to an integer and a function to deposit money in the bank account registered to the user is executed. The loop through the list of bank accounts is also hereby broken. The message is printed to the screen and the code heads back to the previous while loop to gather user input and determine the next course of action.
Let’s go back to the list of bank accounts just for a second. How are we creating this list, and where are we creating it? By definition, a class is a container or template to create and house objects of a certain type. In our case, we want to create bank account objects for each customer that is using our bank. So, the logical approach would be to create a class to set up a bank account template that we can proceed to use to create our bank account objects. Figures 3 and 4 shows fragments of code from the bankAccount class, set up with methods to create a new account (for new customers), deposit/withdraw money, and check account balance.
Most of the code shown in the above two images is pretty self-explanatory, so we’re going to focus on the main concepts behind setting up the class and methods. Right underneath the class definition, there are a list of variables (or properties) corresponding to the class objects. Every bank account object will have these properties defined upon creation with the values specified in the class definition, but these values are subject to change as different methods are invoked on them. The next segment of this code deals with the different functions, the first of which is creating a new account. This function takes in the name, social security number, address, and salary of the new customer, performs a check to make sure the ssn entered is valid, creates a debit card for the customer, and assigns the new values to all the properties within the created bank account object for the customer. The next few functions are for depositing and withdrawing money, which both take in the amount to deposit or withdraw as an input and adjust the account balance of the chosen bank account accordingly. These two functions are invoked on the bank account object pertaining to that particular customer, as we observed in the previous section where we discussed the validating_user_input() function. And last but not least, we have a function to check the account balance, which requires no user input, but again is invoked on the bank account selected to find how much money is currently stored in the customer’s account. After each one of these functions have finished their primary purpose, the transaction is added to two lists — one that shows records as text which can be viewed upon request (if the user selects option 5), and the other which saves the amounts involved as well as type of transaction as a list element to upload to a database table later on.
Now, let’s revisit that debit card that was provisioned within this class. If we look at the code closely, we can see that the debit card was instantiated with a user input (the account holder name) and a method to print the card’s details was invoked on this object. Additionally, the new card’s properties are saved into the bank account object’s properties so the account can be validated and found using the card number. All of this seems straightforward, but where are we actually defining the template for this debit card? Here’s where we require another class (which I happen to have conveniently named debitCard being the genius that I am 😁), to define the properties associated with the card. Figure 5 shows the debitCard class definition.
The four main attributes of the debit card (cardNumber, cardHolderName, cardCVV, and cardExpiryDate) are initialized in the class and redefined in the constructor, which takes the customer name as an input. Here, the card number and card cvv are randomly generated and the card’s expiry date is determined by obtaining the current date (month and year) and adding five years to it. An additional method to print these card details is also included so that the customer can see them on the interactive screen as soon as the card is set up under their account.
That sums up all the classes and methods required to set up the bank accounts and debit cards, get the interactive screen running based on user input, and perform and record the appropriate transactions based on that input. If the user selects option 5, they will be shown the list of transactions that have been performed on their account. Option 6 defaults to customer service, which also ends the main while loop and exits out of the application code. But what happens as soon as the program has finished with its execution? None of the data that we created, including the bank accounts, associated debit cards, customers with all their biodata, etc. will be saved! Essentially everything gets wiped out on a new application code run, and we start with a blank slate. This is definitely NOT the behavior we want from our code, as we need it to remember and make use of previously saved accounts and customers so we aren’t duplicating and recreating data that we already added in to the online banking system. So how can we prevent this from happening? The simplest option that presents itself is uploading the data to a persistent data store, such as a SQL database. We will be using the Microsoft Azure Cloud Platform for all resources that we provision as part of this project, so our SQL database and table will be maintained on this provider.
Figure 6 above shows methods designed to load secrets from an Azure Key Vault (specifically the database server password), create a table on the SQL instance, and connect to it in order to insert all the transaction data records for the bank accounts created from the application execution. This uses the pyodbc package to establish the db connection when supplied the database server username and password. The queries presented above are relatively straightforward — a basic create table statement and insert into query with the values pertaining to customer name and transaction record details. In order to get this connection to work, we had to set up some firewall rules and install a few packages to get the ODBC Driver for SQL up and running. The former will be discussed in my next article where we will layout the infrastructure that was necessary to get this application connected with the cloud. Please refer to the ODBC_README file on my code repository for more information on the latter. The create table() function will be run at the start of every application execution, to ensure that if the table doesn’t already exist, it is immediately created and ready to receive a new set of transaction records.
Phew! That was quite a bit of code to get through and understand, but absolutely necessary for the end to end operation of this application. We saw how classes and methods were integrated to set up a fully interactive online banking system, that takes in user input, uses it to fulfill requests on customer accounts, creates new accounts and debit cards, and saves all the transaction data as records into a SQL table for persistent recovery. You might be wondering why I haven’t included a demo of how this application works just yet. That’s the best part, so I’m saving it for the end when we get this incorporated into an automated CI/CD framework that can run the application on a Kubernetes cluster!
We’ve officially wrapped up the development portion of this project, but if we want to automate the operation of this code instead of manually triggering it every time, we need to get the resources necessary for this application onto the cloud. And that’s where Terraform, the leading Infrastructure as Code tool in the industry right now, comes into play. My next article will provide a deep dive on how we can use Terraform to set up resource definitions necessary for this application before we deploy the infrastructure onto the cloud. Until then, stay tuned folks! Happy coding 😁
Find my code here 👉 https://abujji.visualstudio.com/banking-app-python/_git/python_scripts
Resources:
- https://stackoverflow.com/ (If you’re in Tech, then you know how big of a lifesaver this forum is 😂)
- https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver16&tabs=debian18-install%2Calpine17-install%2Cdebian8-install%2Credhat7-13-install%2Crhel7-offline
- Last but not least, my Brain 😎