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.

Introduction

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) 21.
  • You must set the set the JAVA_HOME environment variable. For more information, see this tutorial on how to set JAVA_HOME
  • 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 supported region (e.g., eastus)
      • 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.FUNCTION) 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);
    }
}
  • Next, we need to remove configuration settings that are not compatible with Azure Functions running on Flex consumption plan. Open local.settings.json and replace the content with:
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}
  • Note: This file should never be checked into your git repository. Make sure it’s added to your .gitignore.

  • Make sure that java --version returns version 21. If it doesn’t, go back to the prerequisite section above, install JDK 21, and set JAVA_HOME to point to the installation directory.
  • We also need to make changes to the pom.xml and configure our Azure Function to use the Flex consumption plan with the required settings. Open the pom.xml and update the Azure Functions Maven plugin configuration named azure-functions-maven-plugin as follows:
<plugin>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>azure-functions-maven-plugin</artifactId>
    <version>${azure.functions.maven.plugin.version}</version>
    <configuration>
        <appName>${functionAppName}</appName>
        <resourceGroup>java-functions-group</resourceGroup>
        <!-- function app region-->
              <!-- refers
              https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions
              for all valid values -->
        <region>eastus</region>
        <appServicePlanName>java-functions-app-service-plan</appServicePlanName>
        
        <!-- Add Flex consumption pricing tier -->
        <pricingTier>Flex Consumption</pricingTier>
        <!-- Set minimum instances for Flex -->
        <maximumInstances>40</maximumInstances>
        
        <runtime>
            <!-- Explicitly set OS to linux -->
            <os>linux</os>
            <javaVersion>21</javaVersion>
        </runtime>
    </configuration>
</plugin>
  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 (For Windows Ctrl+Shift+P and macOS ⇧⌘P) and type:
        • Tasks: Run Task and then select 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.

  • We will deploy the project files from the current directory to the <FunctionAppName> as a .zip deployment package. Deployment can be done using either the Azure Functions Core Tools or the Azure Functions VS Code Extension.

  • Option 1: Using Azure Functions Core Tools In the following command, replace <FunctionAppName> with the name you chose on Azure in step 3 (e.g., GeoByIPFunc-YOURNAME):

func azure functionapp publish <FunctionAppName>
  • This should result in a successful deployment:
[2025-10-16T13:36:16.018Z] The deployment was successful!
Functions in geobyipfunc-cpit490:
  • Option 2: Using VS Code Azure Functions Extensions
    • Open the Command Palette (For Windows Ctrl+Shift+P and macOS ⇧⌘P) and enter the command Azure Functions: Deploy to Function App..., press Enter, and then select the function app name from the list.
  1. Testing the Deployed function App With Auth Keys
  • Copy the domain of the function app from the Azure Portal.
    • azure function default domain
  • Testing the deployed function requires authentication keys. This is because in Function.java, we set authLevel = AuthorizationLevel.FUNCTION, which will make the authorization level of our function an AuthLevel.FUNCTION, which means you need to include a function key in your request. Otherwise, the function will return HTTP status code 401, Unauthorized. We can obtain the function auth keys from the Azure portal under or using the Azure CLI.
    • Option 1: Azure Portal: Go to Functions -> App Keys -> Copy the default key:
      • Azure Functions App Auth Keys
    • Option 2: Azure CLI: Run the following command after replacing the function app name and resource group name with your own:
      az functionapp keys list --name geobyipfunc-cpit490 --resource-group cpit490
      
      {
        "functionKeys": {
          "default": "GFme8jfAcZG5aQ7mMEFLJ9aoQg37SG2a1A_-Ce29QxrNAzFuTm6uyg=="
        },
        "masterKey": "abcdefghijkldsmnopqrswvn7hb4Y4jT65zXJ99DAzFuun8tWA==",
        "systemKeys": {}
      }
      
    • Copy the auth key (functionKeys.default) and include it as a query parameter as follows:
    curl "https://<AZURE_FUNCTION_DEFAULT_DOMAIN>?code=<FUNCTION_AUTH_KEY>&ip=<IPv4_ADDRESS>"
    
  • For example:
curl "https://geobyipfunc-cpit490.azurewebsites.net/api/getLocation?code=GFme8jfAcZG5aQ7mMEFLJ9aoQg37SG2a1A_-Ce29QxrNAzFuTm6uyg==&ip192.162.72.233"
  • Testing with Postman
    • Open Postman and create a new GET request

    • Set the URL to your Azure Function endpoint with the auth key as a query parameter:

      https://geobyipfunc-cpit490.azurewebsites.net
      
    • Under Query Params, add

      • Key: code
      • Value: The auth key (functionKeys.default) you obtained from the Azure portal or CLI as described in the previous step:
      • Key: ip
      • Value: Any IPv4 address (e.g., 192.162.72.233)
    • Click Send to test the function.

    • You should receive a JSON response similar to:

      {
        "countryCode": "SA",
        "ip": "192.162.72.23"
      }
      
    • Example Postman configuration:

  1. Tear Down
  • Delete the Function App and any associated resources from the Azure Portal.

References