Edit Page

Deploying a Java Application as a FaaS

In this lecture note, we will deploy a sample Java function for IP geocoding (determining the geographic location of an IP address) to demonstrate the use of Java in Azure Functions. The function will be HTTP-triggered, tested locally, and then deployed to serverless Azure Functions.

Geo IPv4 Function

We’ll create a serverless function that accepts an IPv4 address as input and returns the country code. The function will use an open dataset that provides geolocation data for IPv4 addresses worldwide.

Please note that the accuracy of this dataset is limited, and it should not be used in a production environment. For more reliable results, consider using the MaxMind GeoIP2 database, which requires an access key.

Prerequisites

  • Install the Azure CLI
    • Windows: Download and run the Azure CLI from here
    • macOS: Use Homebrew to install the Core Tools on macOS using brew update && brew install azure-cli
    • Linux: use your package manager (e.g., APT) to install it as described here
  • Install Apache Maven
    • Windows
      • Visit the Maven download page and download the binary zip archive of the latest version of Maven (e.g., apache-maven-<version>-bin.zip).
      • Extract/unzip it to a new directory (e.g., C:\Program Files\Maven\).
      • Open the Start menu and search for environment variables.
      • click on Edit the system environment variables.
      • Under the Advanced tab in the System Properties window, click Environment Variables.
      • Click the New… button under the System variables section to add a new system environment variable.
      • Enter MAVEN_HOME as the variable name and the path to the Maven installation directory as the variable value. Click OK.
      • Under the System variables section, click on Path to select it and click the Edit… button.
      • Click the New button in the Edit environment variable window.
      • Enter %MAVEN_HOME%\bin in the new field. Click OK to save changes to the Path variable. Click OK
      • Close and open PowerShell, and run mvn -version to confirm that you have maven installed successfully.
    • macOS
      • Install Homebrew if you do not already have it.
      • brew install maven
    • Linux
      • Use your package manager (e.g., apt install maven)
  • The Java Development Kit (JDK), Java SE 8 or above.
  • Sign in into Azure CLI
    • Close and reopen any active terminal window and run:
      az login
      
      • When you sign in, Azure CLI generates and stores an authentication refresh token, which is used to obtain a new access token when the current one expires:
      az account get-access-token
      

Deploying an Azure Function in Java using Visual Studio Code

We will create and deploy the Java function code to Azure using Visual Studio Code.

  1. Open Visual Studio Code, from the View menu, click Extensions or use the keyboard shortcut Ctrl+Shift+X(Windows/Linux) or Shift+Command+X (macOS), and install the following extensions published by Microsoft:

  2. Sign in to Azure in VS Code

    • In Visual Studio Code, in the Activity bar, select the Azure icon. Then under Resources, select Sign in to Azure. sign into Azure in VS code
  3. Create an Azure Function App at Azure Portal

    • In the Azure Portal, search for Function App and click on it
    • Click + Create
    • From the hosting options, select Flex Consumption plan and click Select.
    • Under Basics:
      • Subscription: Select your subscription and Resource Group
      • Function App name: Choose a globally unique name (e.g., GeoByIPFunc-YOURNAME)
      • Region: Select any region (e.g., Qatar Central)
      • Runtime stack: Java
      • Version: 21
      • Instance Size: Select 512 MB or similar for the amount of memory needs to be allocated to each instance of the function app.
      • Zone redundancy: select Disabled
    • Under Networking: Make sure that Enable public access is turned On, and virtual network integration is Off.
    • Under Monitoring: you may disable Application Insights.
    • Under Resource authentication make sure that the selected Authentication type is Secrets
    • Click Review + create then Create
    • Once your deployment is complete, click on Go to resource.
  4. Create the Function in VS Code

    • Open a new Visual Studio Code window, press F1 on Windows or Command+SHIFT+P on macOS and search for and run the command Azure Functions: Create New Project....
    PromptSelection
    Select the directory location for storing your project workspaceCreate a new empty directory (e.g., GeoByIPFunc) and choose it as the desired project directory
    Select a languageJava
    Select a version of JavaJava 21
    Enter a Maven group idsa.edu.kau.fcit.cpit490
    Enter a Maven artifact idGeoByIPFunc
    Enter a Maven release version1.0-SNAPSHOT
    Enter a Java package namesa.edu.kau.fcit.cpit490
    Enter the globally unique name for the App NameGeoByIPFunc-YOURNAME
    Select build typeMaven
    How would you like to open your project?Open in current window
  5. Write the Function Code

  • We will use a CSV file that contains a list of IPv4 address ranges and the country code. This CSV file is part of this GitHub repository sapics/ip-location-db project.
  • Open a Terminal window on VS Code (Terminal menu -> New Terminal) and download and move the asn-country-ipv4-num.csv file that contains the IPv4 address range and country codes:
curl -LO https://github.com/sapics/ip-location-db/raw/refs/heads/main/asn-country/asn-country-ipv4-num.csv
mkdir -p src/main/resources
mv asn-country-ipv4-num.csv src/main/resources/
  • We will use a CSV parsing library as a dependency for our Maven project. Open the pom.xml file in VS code and add the following to your pom.xml file under the <dependencies> tag:
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-csv</artifactId>
  <version>1.14.1</version>
</dependency>
  • With the pom.xml file open, expand the left sidebar, select Maven, and click on the dual arrow icon to refresh the dependencies as shown below:

maven refresh dependencies in vscode

  • Open the class Function.java, which is typically at src/main/java/sa/edu/kau/fcit/cpit490/Function.java and replace the code with:
package sa.edu.kau.fcit.cpit490;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import java.util.Optional;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

/**
 * Azure Functions with HTTP Trigger.
 */
public class Function {

    /**
     * This function listens at endpoint "/api/getLocation". To invoke it using
     * "curl" command in bash:
     * curl "{your host}/api/getLocation?ip=IPv4_ADDRESS"
     */
    @FunctionName("getLocation")
    public HttpResponseMessage run(
            @HttpTrigger(name = "req", methods = {
                    HttpMethod.GET }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Get the IPv4 address as a query parameter
        final String ipAddress = request.getQueryParameters().get("ip");

        if (ipAddress == null) {
            context.getLogger().warning("Java HTTP trigger processed a request.");
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                    .body("Please provide an IP address using the 'ip' query parameter")
                    .build();
        } else {
            try {
                String countryCode = getCountryFromIP(ipAddress, context);
                if (countryCode != null) {
                    Map<String, String> responseData = new HashMap<>();
                    responseData.put("ip", ipAddress);
                    responseData.put("countryCode", countryCode);
                    return request.createResponseBuilder(HttpStatus.OK).body(responseData).build();
                } else {
                    return request.createResponseBuilder(HttpStatus.NOT_FOUND)
                            .body("Country not found for IP: " + ipAddress)
                            .build();
                }
            } catch (Exception e) {
                context.getLogger().severe("Failed to find country for IP: " + e.getMessage());
                return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body("Failed to find country for IP:  " + e.getMessage())
                        .build();
            }
        }
    }

    /**
     * Gets country code for an IPv4 address
     * 
     * @param ipAddress The IPv4 address to look up
     * @return The two-letter country code or null if not found
     */
    public String getCountryFromIP(String ipAddress, ExecutionContext context) {
        try {
            long ipLong = ipToLong(ipAddress);
            return lookupCountryCode(ipLong, context);
        } catch (Exception e) {
            context.getLogger().warning("Error looking up IP: " + e.getMessage());
            return null;
        }
    }

    /**
     * Converts an IPv4 address to a long integer for range comparison
     * 
     * @param ipAddress The IPv4 address in string format (e.g., "192.168.1.1")
     * @return The IP address as a long integer
     * @throws UnknownHostException if the IP address is invalid
     */
    public long ipToLong(String ipAddress) throws UnknownHostException {
        String[] parts = ipAddress.split("\\.");
        if (parts.length != 4) {
            throw new UnknownHostException("Invalid IPv4 format: " + ipAddress);
        }

        InetAddress inet = InetAddress.getByName(ipAddress);
        byte[] addressBytes = inet.getAddress();

        if (addressBytes.length != 4) {
            throw new UnknownHostException("Not an IPv4 address");
        }

        long result = 0;
        for (byte b : addressBytes) {
            result = (result << 8) | (b & 0xFF);
        }

        return result;
    }

    /**
     * Looks up the country code for an IP address using the CSV dataset
     * Note: The dataset file "asn-country-ipv4-num.csv" should be placed in the
     * resources folder src/main/resources/
     * and have the format: start_ip,end_ip,country_code
     * 
     * @param ipLong  The IP address converted to a long integer
     * @param context The Azure Functions execution context for logging
     * @return The two-letter country code or null if not found
     */
    public String lookupCountryCode(long ipLong, ExecutionContext context) {
        try {
            InputStream is = getResourceAsStream("asn-country-ipv4-num.csv");
            if (is == null) {
                context.getLogger().severe("Error reading CSV file: resource not found");
                return null;
            }

            try (InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
                    CSVParser csvParser = CSVFormat.DEFAULT.builder().get().parse(reader)) {

                for (CSVRecord record : csvParser) {
                    if (record.size() < 3)
                        continue;

                    long startRange = Long.parseLong(record.get(0));
                    long endRange = Long.parseLong(record.get(1));

                    if (ipLong >= startRange && ipLong <= endRange) {
                        return record.get(2);
                    }
                }
                return null;
            }
        } catch (IOException e) {
            context.getLogger().severe("Error reading CSV file: " + e.getMessage());
            return null;
        }
    }

    /**
     * Gets resource as stream - method made accessible for testing
     * 
     * @param resourceName The name of the resource
     * @return InputStream of the resource or null if not found
     */
    public InputStream getResourceAsStream(String resourceName) {
        return getClass().getClassLoader().getResourceAsStream(resourceName);
    }
}
  1. Start the function Locally
  • You can start the function locally using one of the following options:
    • Option 1: Using the VS Code Terminal. TErminal -> New Terminal and run
      mvn clean package
      mvn azure-functions:run
      
    • Option 2: Using the VS Code Maven and Azure Functions extensions
      • From the left-hand side Activity bar, open the project explorer, select Maven -> Lifecycle -> Package as shown below:
        • Package Maven Project in VS Code
      • Open the Command Palette (Ctrl+Shift+X(Windows/Linux) or Shift+Command+X (macOS)) and type:
        • Tasks: Run Task -> func: host start
    • Azure Function running in VS Code
    • The function should be running at http://localhost:7071/api/getLocation. Copy this URL.
      • Note: Please note that any changes made to the source code will require re-packaging the Maven project and re-running the function.

  1. Test the function locally
    • Find your public IP address by either going to a website such as https://whatismyipaddress.com/ or running curl https://ipinfo.io/ip
    • Test the function using:
    curl http://localhost:7071/api/getLocation?ip=192.162.72.233
    
{
"countryCode": "SA",
"ip": "192.162.72.233"
} 
  • You may also test the function using Postman:
  1. Publish the Function to Azure We will deploy the function project files directly to our Azure function app using zip deployment.
  • Stop the function that you ran locally using CTRL-C.
  • Deploy the project files from the current directory to the <FunctionAppName> as a .zip deployment package. Replace <FunctionAppName> with the name you chose on Azure in step 3 (e.g., GeoByIPFunc-YOURNAME).
  • From the VS Code Terminal tab, run:
func azure functionapp publish <FunctionAppName>
  • Copy the domain of the function app from the Azure Portal.
  • Test the function from the same Terminal Window you used to publish it using https://<azure_function_app_domain>/<function_path>?ip=<ip_address>:
curl "https://geobyipfunc-cpit490.azurewebsites.net/api/getLocation?ip=192.162.72.233"
  1. Testing the function With Auth Keys
  • Testing the function from any location where the Azure session token has expired requires authentication keys. Azure Function requires authentication by default. Azure Functions are created with AuthLevel.FUNCTION, which means you need to include a function key in your request.
    • Get the function key using this command after replacing the function app name and resource group name with yours:
az functionapp keys list --name sentiment-function-cpit490 --resource-group cpit490
{
  "functionKeys": {
    "default": "JEpnsadfsdfsdfwlbsWjnp8i5cSWYhF9CDUAXaxk97isI8CAzFupfw_lQ==a"
  },
  "masterKey": "abcdefghijklmnopqrswvn7hb4Y4jT65zXJ99DAzFuun8tWA==",
  "systemKeys": {}
}
  • Copy the auth key (functionKeys.default) and include it as a query parameter as follows:
curl -X POST "<AZURE_FUNCTION_DEFAULT_DOMAIN>?code=<FUNCTION_AUTH_KEY>" \
-H "Content-Type: application/json" \
-d '{"text": "I love this product! It is amazing!"}'
  • For example:
curl -X POST "https://sentiment-function-cpit490-hqebaxexbcead9bs.canadacentral-01.azurewebsites.net/api/http_trigger_sentiment_analysis?code=JEpnsadfsdfsdfwlbsWjnp8i5cSWYhF9CDUAXaxk97isI8CAzFupfw_lQ==a" \
-H "Content-Type: application/json" \
-d '{"text": "I love this product! It is amazing!"}'
  • Testing with Postman
    • Open Postman and create a new POST request
    • Set the URL to your Azure Function endpoint with the auth key as a query parameter:
      https://sentiment-function-cpit490-hqebaxexbcead9bs.canadacentral-01.azurewebsites.net/api/http_trigger_sentiment_analysis
      
    • Under Query Params, add
      • Key: code
      • Value: The auth key (functionKeys.default) you obtained from running az functionapp keys list --name sentiment-function-cpit490 --resource-group cpit490:
    • Under Headers tab, add:
      • Key: Content-Type
      • Value: application/json
    • Under Body tab:
      • Select raw
      • Choose JSON from the dropdown menu
      • Enter the following JSON:
        {"text": "I love this product! It is amazing!"}
        
    • Click Send to test the function.
    • You should receive a JSON response similar to:
      {"sentiment": "positive", "polarity": 0.6875, "subjectivity": 0.75}
      
    • Example Postman configuration: