In the previous article we showed you how to set up an automated CI/CD pipeline using Azure DevOps Pipelines integrated with GitHub. The goal of this article is to develop a simple but complete working application that uses a cloud native microservice implemented using three Azure Function apps. You can use the complete application to explore the Azure platform. The application provides simple polls like the ones you see on Facebook pages. One Azure Function app returns an HTML page that presents the ballot, a second function app accepts the vote and the third function app provides a report of the votes cast. Each function app is in a separate project to show how separate lifecycles deliver the various parts of the entire application.
For data handling we’re going to use Azure Database for MySQL. Microsoft provides an example data project that is similar to this at Quickstart: Use Java and JDBC with Azure Database for MySQL. This article is useful for reference, but we're going to adapt it to use the Azure Portal, MySQL Workbench and Eclipse because these tools you’re familiar with from our previous article.
We build a serverless app because topics like Kubernetes are beyond the article’s scope. Microsoft provides some books on containerization that cover everything you need to know to deploy and manage containerized applications on Azure.
For example: The Kubernetes Bundle and Hands-On Kubernetes on Azure.
We rely on the infrastructure we built in the previous article, so you should review that before starting this project. In this project, we create individual resources for each of our three function apps. This emulates a real-life situation where three separate groups build and deploy the various parts of a complete enterprise application.
We created three projects in Azure DevOps. Each project has its own service connector and pipeline, which we discuss below.
User Interface
The HTML we use is adapted from Bootstrap’s Get Started Page. Bootstrap is a widely used framework that supports responsive web applications. We assume that you are familiar with it.
The application user interface (UI) is very simple. Auser opens a browser to retrieve a URL (potentially using a link on another page), which returns the poll page. The link, https://azureexamplepollingui.azurewebsites.net/api/GetVotingUI, references the AzureExamplePollingUI
function app that responds to an HTTP trigger. Note that this app is registered with Azure Active Directory (AAD). It specifies that the user must authenticate using AAD. Azure takes the user through the OAuth 2 authentication and authorization process before calling the function. When the function is called, it returns the following HTML page.
The user selects an option and clicks Submit to enter their vote. The form action sends a GET
request to the CastVote
function app at: https://azurecastvote.azurewebsites.net/api/CastVote?vote=<voteId>. The function app records the vote and returns an acknowledgement like the one below.
The screen displays the value of the Principal ID
variable provided by OAuth 2. This version of the app doesn’t do anything with this value. It’s displayed to let you know the user’s ID is available for other purposes, such as authorizing access or checking for duplicate votes.
You can obtain the current vote results at any time by calling the RetrieveVoteReport
function app by retrieving this URL: https://azureexamplepollingreports.azurewebsites.net/api/RetrieveVoteReport. After authenticating the user, the report returned looks like this.
The function includes the code that obtains the Principal ID
value. You can use it to determine if this user is authorized to retrieve this report, although our implementation does not use it.
Database
Microsoft provides Quickstart documentation for creating a server and configuring a firewall. The process described here skips the points in this article that are unnecessary for us.
Create a Server
In the Azure portal, type "Azure Database" in the search field. In the search results, click Azure Database for MySQL Servers.
Click Create to start the server creation process.
Click Create in the "Single server" pane.
Select your Subscription and Resource group. Click Configure Server.
For this example, select a "Basic," 1v Core server with 5GB of storage. Select the Basic tab and move the sliders to set the above parameters. Click OK.
Back on the Create MySQL Server page, set the Admin username and Password fields. Click Review + create.
On the review panel, click Create to create the server.
Define Database Firewall Rules
The Azure firewall blocks all access to the new database by default. We use methods described in the Quickstart document to provide access to our local machine.
Find the resource in the portal and open the Overview page. Find and click Connection security.
Click Add current client IP address. Click Save to allow your current client to connect to the server.
For more on firewall rules, see Create and manage Azure Database for MySQL firewall rules by using the Azure portal.
Implement Schema and Data
We assume that you’re familiar with MySQL Workbench (or a similar tool), and that you can implement the database from the following scripts. The first script creates the schema. You must know the name of the database that you created in the previous section because you add that value to the url
secret.
For this example, we created a database named "votingdb."
-- MySQL Workbench Forward Engineering
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
-- -----------------------------------------------------
-- Schema votingDB
-- -----------------------------------------------------
-- -----------------------------------------------------
-- Schema votingDB
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `votingDB` ;
USE `votingDB` ;
-- -----------------------------------------------------
-- Table `votingDB`.`electionDetails`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `votingDB`.`electionDetails` ;
CREATE TABLE IF NOT EXISTS `votingDB`.`electionDetails` (
`idElection` INT NOT NULL AUTO_INCREMENT,
`electionTitle` VARCHAR(255) NOT NULL,
`startDate` DATE NOT NULL,
`endDate` DATE NOT NULL,
PRIMARY KEY (`idElection`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `votingDB`.`voteTypes`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `votingDB`.`voteTypes` ;
CREATE TABLE IF NOT EXISTS `votingDB`.`voteTypes` (
`idVoteType` INT NOT NULL,
`voteName` VARCHAR(45) NOT NULL,
`electionId` INT NOT NULL,
PRIMARY KEY (`idVoteType`),
INDEX `fk_election_idx` (`electionId` ASC) ,
CONSTRAINT `fk_election`
FOREIGN KEY (`electionId`)
REFERENCES `votingDB`.`electionDetails` (`idElection`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `votingDB`.`votes`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `votingDB`.`votes` ;
CREATE TABLE IF NOT EXISTS `votingDB`.`votes` (
`idVotes` INT NOT NULL AUTO_INCREMENT,
`voter` VARCHAR(255) NOT NULL,
`vote` INT NOT NULL,
`idElection` INT NOT NULL,
PRIMARY KEY (`idVotes`),
INDEX `fk_voteId_idx` (`vote` ASC) ,
UNIQUE INDEX `idVotes_UNIQUE` (`idVotes` ASC) ,
CONSTRAINT `fk_voteId`
FOREIGN KEY (`vote`)
REFERENCES `votingDB`.`voteTypes` (`idVoteType`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
This script inserts the initial data:
INSERT INTO electionDetails (electionTitle, startDate, endDate) VALUES ('Peas Please', '2021/3/1', '2021/4/1');
INSERT INTO votetypes (idVoteType, voteName, electionId) values (1, 'Absolutely', 1);
INSERT INTO votetypes (idVoteType, voteName, electionId) values (2, 'No Thank You', 1);
Functions
Our example application provides simple polls like those that appear on Facebook pages. The application provides an election title, start and end dates for the voting period, a set of voting options, and a report that contains the results.
To build this application, we create three functions: one to allow the user to vote, one to return a report of the current vote totals, and one to return an HTML page that implements the UI. We assume that an administrator can set the election title and voting options in the database.
All the code is in GitHub. You are welcome to clone the repositories to save a few steps.
In this article, we include only the code relevant to the discussion.
Create Azure Function Apps
You must create three function apps. Here are the function apps we created for this implementation.
In all these functions apps, you must change properties in the src/main/resources/application.properties file as follows:
url =jdbc:mysql://azure-function-voting.mysql.database.azure.com:3306/votingdb?serverTimezone=UTC&verifyServerCertificate=true&useSSL=true&requireSSL=true
user=<admin user id>@azure-function-voting
password=<admin password>
The url
property has the database name (votingdb
), as well as the user ID and administrator password.
Cast Vote
This function app provides the ability to cast a vote. After you create the DevOps project, you must add to it a service connector and a pipeline. You can replace the standard code in the pipeline with this code:
variables:
serviceConnectionToAzure: 'AzureCastVote'
appName: 'AzureCastVote'
functionAppName: 'castVote'
POM_XML_Directory: 'castVote'
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: Maven@3
inputs:
mavenPomFile: '$(POM_XML_Directory)/pom.xml'
mavenOptions: '-Xmx3072m'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.11'
jdkArchitectureOption: 'x64'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
goals: 'package'
- task: CopyFiles@2
displayName: Copy Files
inputs:
SourceFolder: $(system.defaultworkingdirectory)/$(POM_XML_Directory)/target/azure-functions/$(functionAppName)/
Contents: '**'
TargetFolder: $(build.artifactstagingdirectory)
- task: PublishBuildArtifacts@1
displayName: Publish Artifact
inputs:
PathtoPublish: $(build.artifactstagingdirectory)
- task: AzureFunctionApp@1
displayName: Azure Function App deploy
inputs:
azureSubscription: $(serviceConnectionToAzure)
appType: 'functionAppLinux'
appName: $(appName)
package: $(build.artifactstagingdirectory)
runtimeStack: 'JAVA|11'
Create the Eclipse project and your repositories, like this:
There are five classes in this function:
Class File | Purpose |
Function.java | Implements the run method for the HTTP triggered function |
DatabaseConnection.java | A wrapper class that handles loading of the connection properties and creating of a JDBC connection to the database |
PageBuilder.java | Implements an HTML page to acknowledge the vote |
VoteManager.java | Provides a database interface to record the vote |
VoteModel.java | Data model object class |
The run
method in the Function
class responds to HTTP GET
and POST
requests. For the GET
requests, the vote is passed on the query string as vote=<vote id>
, where <vote id>
is provided by votingDB
, voteTypes
, and idVoteType
. For the POST
requests, the vote is passed in the form-urlencoded
body.
The VoteModel
class provides two primary constructors: one that accepts a query string from the GET request, and one that accepts the post-body from a POST
request. Both constructors also accept Principal Name
and Principal ID
, which they concatenate together to build voter id
.
The run
method creates an instance of the PageBuilder
class to obtain the current electionID
. Then it instantiates VoteModel
and VoteManager
, and calls the VoteManager
’s castVote
method to cast the vote for the current election. Afterwards, it calls several PageBuilder
methods to construct the HTML document that it returns in the response.
Retrieve Vote Report
As with the Cast Vote function app, after you create the DevOps resources, you must change the pipeline YAML:
variables:
serviceConnectionToAzure: 'Reporting Function App'
appName: 'AzureExamplePollingReports'
functionAppName: 'retrieveVoteReport'
POM_XML_Directory: 'retrieveVoteReport'
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: Maven@3
inputs:
mavenPomFile: '$(POM_XML_Directory)/pom.xml'
mavenOptions: '-Xmx3072m'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.11'
jdkArchitectureOption: 'x64'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
goals: 'package'
- task: CopyFiles@2
displayName: Copy Files
inputs:
SourceFolder: $(system.defaultworkingdirectory)/$(POM_XML_Directory)/target/azure-functions/$(functionAppName)/
Contents: '**'
TargetFolder: $(build.artifactstagingdirectory)
- task: PublishBuildArtifacts@1
displayName: Publish Artifact
inputs:
PathtoPublish: $(build.artifactstagingdirectory)
- task: AzureFunctionApp@1
displayName: Azure Function App deploy
inputs:
azureSubscription: $(serviceConnectionToAzure)
appType: 'functionAppLinux'
appName: $(appName)
package: $(build.artifactstagingdirectory)
runtimeStack: 'JAVA|11'
Create the Eclipse project and your repositories, like this:
There are four classes in this function:
Class FIle | Purpose |
Function.java | Implements the run method for the HTTP triggered function |
DatabaseConnection.java | A wrapper class that handles loading of the connection properties and creating of a JDBC connection to the database |
PageBuilder.java | Implements the shell of the report in HTML. |
Reports.java | Obtains voting results from the database and builds the result table portion of the report in HTML |
The Function
class responds only to HTTP GET
requests without any parameters. It creates an instance of the PageBuilder
class. This class loads the HTML template from the baseUi.html resource, builds the "shell" of the response page, determines which election is active (using the current date), and then calls the Reports
class. The getElectionReport
method returns totals as HTML rows that it inserts into the page shell.
Get Application Page
After creating the related resources, you must change the pipeline YAML file as follows:
variables:
serviceConnectionToAzure: 'PollingUIConnection'
appName: 'AzureExamplePollingUI'
functionAppName: 'AzureExamplePollingUI'
POM_XML_Directory: 'AzureExamplePollingUI'
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: Maven@3
inputs:
mavenPomFile: '$(POM_XML_Directory)/pom.xml'
mavenOptions: '-Xmx3072m'
javaHomeOption: 'JDKVersion'
jdkVersionOption: '1.11'
jdkArchitectureOption: 'x64'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
goals: 'package'
- task: CopyFiles@2
displayName: Copy Files
inputs:
SourceFolder: $(system.defaultworkingdirectory)/$(POM_XML_Directory)/target/azure-functions/$(functionAppName)/
Contents: '**'
TargetFolder: $(build.artifactstagingdirectory)
- task: PublishBuildArtifacts@1
displayName: Publish Artifact
inputs:
PathtoPublish: $(build.artifactstagingdirectory)
- task: AzureFunctionApp@1
displayName: Azure Function App deploy
inputs:
azureSubscription: $(serviceConnectionToAzure)
appType: 'functionAppLinux'
appName: $(appName)
package: $(build.artifactstagingdirectory)
runtimeStack: 'JAVA|11'
Create the Eclipse project and your repositories, like this:
There are three classes in this function:
Class File | Purpose |
Function.java | Implements the run method for the HTTP triggered function |
DatabaseConnection.java | A wrapper class that handles loading of the connection properties and creating of a JDBC connection to the database |
PageBuilder.java | Implements the shell of the report in HTML |
The Function
class supports only the HTTP GET
request, and it does not require request data. The run
method instantiates the PageBuilder
class. The PageBuilder
object determines the current election, sets the election title in the page, and builds a radio button list for the vote choices. Afterwards, the Function’s run
method returns the generated HTML.
Security – Application Registration
Registering the function app with Azure Active Directory triggers OAuth 2.0 authorization, so you must register every function app you create. Microsoft provides this process documentation here: Configure your App Service or Azure Functions app to use Microsoft Account login.
On the Azure portal home page, type "Azure Active" in the search field. In the search results, click the Azure Active Directory link.
On the Overview page, make sure you are referencing the correct directory. Click the App registrations link.
Click the New registration link.
Fill out the registration form as shown in the screenshot below.
We’re allowing anyone with an Azure account to call the functions. The callback URI, which is set to https://azurejavademofunction.azurewebsites.net/.auth/login/aad/callback redirects the authorization back to a helper page provided by Azure for our function app.
On the App registrations page, copy aside the value of the Application (client) ID field (you’ll need it later).
You must select the token type for OAuth2 to return.
On the directory’s Overview page, click on the registration you just completed. Select Authentication on the side panel.
When the Authentication tab opens, scroll down to the Implicit Grant and Hybrid flows section, and select both token types (unless you know for sure that your application needs only one of them). Click Save to update the registration.
Now you must configure the function app to use Active Directory Authentication.
On the Azure portal home page, type "function" in the search field. In the search results, click Function App.
Select a function app.
Click Authentication / Authorization.
Enable App Service Authentication. As shown below, set the Action to take when the request is not authorized. Click Not Configured.
Click Advanced. Provide the Client ID (from the App registration’s overview) and the Issuer URI. Use the value shown in the screenshot below: (https://login.microsoft.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0).
When you return to the Authentication / Authorization tab, click Save to complete the registration.
Next Steps
As discussed in the User Interface section, you should now be able to launch the application from the https://azureexamplepollingui.azurewebsites.net/api/GetVotingUI URL. From here, you can cast a vote. Then, you can retrieve the totals from https://azureexamplepollingreports.azurewebsites.net/api/RetrieveVoteReport.
Diagnostics are beyond this article’s scope, but there are three primary places to look in case of trouble:
- If the pipeline isn’t running, make sure your
push
updated GitHub. - Incorrect variables in the pipeline YAML, or the Azure Function App isn’t in the resource group assigned to the service connector cause most problems. Check the job in the pipeline for errors.
- Follow these instructions to set up and use Azure Function Monitoring: Monitoring Azure Functions with Azure Monitor Logs.
While this is a very simple application, it has all the basic parts used in a typical three-tier web app. You should experiment with pushing updates from the Eclipse projects, observe how the pipelines update the function apps, and then run the updated functions.
Here’s a list of application improvements that we suggest:
If you want to explore Azure Functions further, we suggest that you start here: Azure Functions Java Developer Guide.
In the next Java on Azure series, we will further our understanding of Java on Azure by learning how to use Azure Functions to implement a hypothetical temperature monitoring application.