Tutorial: Connect to PostgreSQL Database from a Java Quarkus Container App without secrets using a managed identity

Azure Container Apps provides a managed identity for your app, which is a turn-key solution for securing access to Azure Database for PostgreSQL and other Azure services. Managed identities in Container Apps make your app more secure by eliminating secrets from your app, such as credentials in the environment variables.

This tutorial walks you through the process of building, configuring, deploying, and scaling Java container apps on Azure. At the end of this tutorial, you'll have a Quarkus application storing data in a PostgreSQL database with a managed identity running on Container Apps.

Note

Azure Active Directory Authentication for PostgreSQL Flexible Server is currently in preview.

What you will learn:

  • Configure a Quarkus app to authenticate using Azure Active Directory (Azure AD) with a PostgreSQL Database.
  • Create an Azure container registry and push a Java app image to it.
  • Create a Container App in Azure.
  • Create a PostgreSQL database in Azure.
  • Connect to a PostgreSQL Database with managed identity using Service Connector.

If you don't have an Azure subscription, create an Azure free account before you begin.

1. Prerequisites

2. Create a container registry

Create a resource group with the az group create command. An Azure resource group is a logical container into which Azure resources are deployed and managed.

The following example creates a resource group named myResourceGroup in the East US Azure region.

az group create --name myResourceGroup --location eastus

Create an Azure container registry instance using the az acr create command. The registry name must be unique within Azure, contain 5-50 alphanumeric characters. All letters must be specified in lower case. In the following example, mycontainerregistry007 is used. Update this to a unique value.

az acr create \
    --resource-group myResourceGroup \
    --name mycontainerregistry007 \
    --sku Basic

3. Clone the sample app and prepare the container image

This tutorial uses a sample Fruits list app with a web UI that calls a Quarkus REST API backed by Azure Database for PostgreSQL. The code for the app is available on GitHub. To learn more about writing Java apps using Quarkus and PostgreSQL, see the Quarkus Hibernate ORM with Panache Guide and the Quarkus Datasource Guide.

Run the following commands in your terminal to clone the sample repo and set up the sample app environment.

git clone https://github.com/quarkusio/quarkus-quickstarts
cd quarkus-quickstarts/hibernate-orm-panache-quickstart

Modify your project

  1. Add the required dependencies to your project's BOM file.

    <dependency>
        <groupId>com.azure</groupId>
        <artifactId>azure-identity-providers-jdbc-postgresql</artifactId>
        <version>1.0.0-beta.1</version>
    </dependency>
    
  2. Configure the Quarkus app properties.

    The Quarkus configuration is located in the src/main/resources/application.properties file. Open this file in your editor, and observe several default properties. The properties prefixed with %prod are only used when the application is built and deployed, for example when deployed to Azure App Service. When the application runs locally, %prod properties are ignored. Similarly, %dev properties are used in Quarkus' Live Coding / Dev mode, and %test properties are used during continuous testing.

    Delete the existing content in application.properties and replace with the following to configure the database for dev, test, and production modes:

    quarkus.package.type=uber-jar
    
    quarkus.hibernate-orm.database.generation=drop-and-create
    quarkus.datasource.db-kind=postgresql
    quarkus.datasource.jdbc.max-size=8
    quarkus.datasource.jdbc.min-size=2
    quarkus.hibernate-orm.log.sql=true
    quarkus.hibernate-orm.sql-load-script=import.sql
    quarkus.datasource.jdbc.acquisition-timeout = 10
    
    %dev.quarkus.datasource.username=${AZURE_CLIENT_NAME}
    %dev.quarkus.datasource.jdbc.url=jdbc:postgresql://${DBHOST}.postgres.database.azure.com:5432/${DBNAME}?\
    authenticationPluginClassName=com.azure.identity.providers.postgresql.AzureIdentityPostgresqlAuthenticationPlugin\
    &sslmode=require\
    &azure.clientId=${AZURE_CLIENT_ID}\
    &azure.clientSecret=${AZURE_CLIENT_SECRET}\
    &azure.tenantId=${AZURE_TENANT_ID}
    
    %prod.quarkus.datasource.username=${AZURE_MI_NAME}
    %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DBHOST}.postgres.database.azure.com:5432/${DBNAME}?\
    authenticationPluginClassName=com.azure.identity.providers.postgresql.AzureIdentityPostgresqlAuthenticationPlugin\
    &sslmode=require
    
    %dev.quarkus.class-loading.parent-first-artifacts=com.azure:azure-core::jar,\
    com.azure:azure-core-http-netty::jar,\
    io.projectreactor.netty:reactor-netty-core::jar,\
    io.projectreactor.netty:reactor-netty-http::jar,\
    io.netty:netty-resolver-dns::jar,\
    io.netty:netty-codec::jar,\
    io.netty:netty-codec-http::jar,\
    io.netty:netty-codec-http2::jar,\
    io.netty:netty-handler::jar,\
    io.netty:netty-resolver::jar,\
    io.netty:netty-common::jar,\
    io.netty:netty-transport::jar,\
    io.netty:netty-buffer::jar,\
    com.azure:azure-identity::jar,\
    com.azure:azure-identity-providers-core::jar,\
    com.azure:azure-identity-providers-jdbc-postgresql::jar,\
    com.fasterxml.jackson.core:jackson-core::jar,\
    com.fasterxml.jackson.core:jackson-annotations::jar,\
    com.fasterxml.jackson.core:jackson-databind::jar,\
    com.fasterxml.jackson.dataformat:jackson-dataformat-xml::jar,\
    com.fasterxml.jackson.datatype:jackson-datatype-jsr310::jar,\
    org.reactivestreams:reactive-streams::jar,\
    io.projectreactor:reactor-core::jar,\
    com.microsoft.azure:msal4j::jar,\
    com.microsoft.azure:msal4j-persistence-extension::jar,\
    org.codehaus.woodstox:stax2-api::jar,\
    com.fasterxml.woodstox:woodstox-core::jar,\
    com.nimbusds:oauth2-oidc-sdk::jar,\
    com.nimbusds:content-type::jar,\
    com.nimbusds:nimbus-jose-jwt::jar,\
    net.minidev:json-smart::jar,\
    net.minidev:accessors-smart::jar,\
    io.netty:netty-transport-native-unix-common::jar
    

Build and push a Docker image to the container registry

  1. Build the container image.

    Run the following command to build the Quarkus app image. You must tag it with the fully qualified name of your registry login server. The login server name is in the format <registry-name>.azurecr.io (must be all lowercase), for example, mycontainerregistry007.azurecr.io. Replace the name with your own registry name.

    mvnw quarkus:add-extension -Dextensions="container-image-jib"
    mvnw clean package -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=true -Dquarkus.container-image.registry=mycontainerregistry007 -Dquarkus.container-image.name=quarkus-postgres-passwordless-app -Dquarkus.container-image.tag=v1
    
  2. Log in to the registry.

    Before pushing container images, you must log in to the registry. To do so, use the [az acr login][az-acr-login] command. Specify only the registry resource name when signing in with the Azure CLI. Don't use the fully qualified login server name.

    az acr login --name <registry-name>
    

    The command returns a Login Succeeded message once completed.

  3. Push the image to the registry.

    Use [docker push][docker-push] to push the image to the registry instance. Replace mycontainerregistry007 with the login server name of your registry instance. This example creates the quarkus-postgres-passwordless-app repository, containing the quarkus-postgres-passwordless-app:v1 image.

    docker push mycontainerregistry007/quarkus-postgres-passwordless-app:v1
    

4. Create a Container App on Azure

  1. Create a Container Apps instance by running the following command. Make sure you replace the value of the environment variables with the actual name and location you want to use.

    RESOURCE_GROUP="myResourceGroup"
    LOCATION="eastus"
    CONTAINERAPPS_ENVIRONMENT="my-environment"
    
    az containerapp env create \
        --resource-group $RESOURCE_GROUP \
        --name $CONTAINERAPPS_ENVIRONMENT \
        --location $LOCATION
    
  2. Create a container app with your app image by running the following command. Replace the placeholders with your values. To find the container registry admin account details, see Authenticate with an Azure container registry

    CONTAINER_IMAGE_NAME=quarkus-postgres-passwordless-app:v1
    REGISTRY_SERVER=mycontainerregistry007
    REGISTRY_USERNAME=<REGISTRY_USERNAME>
    REGISTRY_PASSWORD=<REGISTRY_PASSWORD>
    
    az containerapp create \
        --resource-group $RESOURCE_GROUP \
        --name my-container-app \
        --image $CONTAINER_IMAGE_NAME \
        --environment $CONTAINERAPPS_ENVIRONMENT \
        --registry-server $REGISTRY_SERVER \
        --registry-username $REGISTRY_USERNAME \
        --registry-password $REGISTRY_PASSWORD
    

5. Create and connect a PostgreSQL database with identity connectivity

Next, create a PostgreSQL Database and configure your container app to connect to a PostgreSQL Database with a system-assigned managed identity. The Quarkus app will connect to this database and store its data when running, persisting the application state no matter where you run the application.

  1. Create the database service.

    DB_SERVER_NAME='msdocs-quarkus-postgres-webapp-db'
    ADMIN_USERNAME='demoadmin'
    ADMIN_PASSWORD='<admin-password>'
    
    az postgres flexible-server create \
        --resource-group $RESOURCE_GROUP \
        --name $DB_SERVER_NAME \
        --location $LOCATION \
        --admin-user $DB_USERNAME \
        --admin-password $DB_PASSWORD \
        --sku-name GP_Gen5_2
    

The following parameters are used in the above Azure CLI command:

  • resource-group → Use the same resource group name in which you created the web app, for example msdocs-quarkus-postgres-webapp-rg.

  • name → The PostgreSQL database server name. This name must be unique across all Azure (the server endpoint becomes https://<name>.postgres.database.azure.com). Allowed characters are A-Z, 0-9, and -. A good pattern is to use a combination of your company name and server identifier. (msdocs-quarkus-postgres-webapp-db)

  • location → Use the same location used for the web app.

  • admin-user → Username for the administrator account. It can't be azure_superuser, admin, administrator, root, guest, or public. For example, demoadmin is okay.

  • admin-password → Password of the administrator user. It must contain 8 to 128 characters from three of the following categories: English uppercase letters, English lowercase letters, numbers, and non-alphanumeric characters.

    Important

    When creating usernames or passwords do not use the $ character. Later in this tutorial, you will create environment variables with these values where the $ character has special meaning within the Linux container used to run Java apps.

  • public-accessNone which sets the server in public access mode with no firewall rules. Rules will be created in a later step.

  • sku-name → The name of the pricing tier and compute configuration, for example GP_Gen5_2. For more information, see Azure Database for PostgreSQL pricing.

  1. Create a database named fruits within the PostgreSQL service with this command:

    az postgres flexible-server db create \
        --resource-group $RESOURCE_GROUP \
        --server-name $DB_SERVER_NAME \
        --database-name fruits
    
  2. Connect the database to the container app with a system-assigned managed identity, using the connection command.

    az containerapp connection create postgres-flexible \
        --resource-group $RESOURCE_GROUP \
        --name my-container-app \
        --target-resource-group $RESOURCE_GROUP \
        --server $DB_SERVER_NAME \
        --database fruits \
        --managed-identity
    

6. Review your changes

You can find the application URL(FQDN) by using the following command:

az containerapp list --resource-group $RESOURCE_GROUP

When the new webpage shows your list of fruits, your app is connecting to the database using the managed identity. You should now be able to edit fruit list as before.

Clean up resources

In the preceding steps, you created Azure resources in a resource group. If you don't expect to need these resources in the future, delete the resource group by running the following command in the Cloud Shell:

az group delete --name myResourceGroup

This command may take a minute to run.

Next steps

Learn more about running Java apps on Azure in the developer guide.