Orbeon Forms
  • Getting started
  • Installation
    • Logging
    • Configuration banner
    • Docker
    • Azure
    • Tomcat
    • WildFly
    • WebSphere
    • WebLogic
    • GlassFish
    • Caches
    • Replication
    • Upgrading
  • Configuration
    • Properties
      • General
        • HTTP client
      • Form Runner
        • Detail page
          • Attachments
          • Email properties
          • PDF
          • Table of contents
        • Persistence
        • Summary page
      • Form Builder
      • XForms
    • Advanced
      • Workflows
      • Session management
      • State handling
      • Client-side error handling
      • Clustering and High Availability
      • Configuring a Form Runner eXist database
      • Creating a production WAR
      • Environments
      • JavaScript and CSS assets
      • Limiter filter
      • Run modes
      • Security
        • Content-Security-Policy header
      • SAP Hybris Module
      • XForms logging
    • Troubleshooting
      • Troubleshooting with the orbeon.log
      • Memory and threads
      • Relational database logging
      • Misc
  • Form Builder
    • Form settings
      • Time window
    • Form editor
      • Form area
      • Toolbox
      • Buttons bar
      • Control settings
      • Dependent fields and sections
      • Validation
      • Choices editor
      • Publishing
      • Cut, copy and paste
      • Section and grid settings
      • Section settings
      • Grid settings
      • Quick control search
      • Repeat settings
      • Repeated grids
      • Undo and redo
      • Keyboard shortcuts
    • Formulas
      • Examples of formulas
      • Formulas inspector
      • Formulas console
    • Summary page
    • Form localization
    • Advanced
      • Edit source
      • Services and actions
        • HTTP services
        • Database services
        • Simple Actions
        • Action Syntax
        • Action Syntax examples
        • Synchronizing repeated content
      • Testing a form in web mode
      • Testing PDF production
      • Testing offline functionality
      • Email Settings dialog
      • Field-level encryption
      • Messages
      • Section templates
      • Template syntax
      • XML Schemas support
      • Extensibility
        • Extension API
        • Integration
        • Toolbox component metadata
  • Form Runner
    • Overview
      • Terminology
    • Pages
      • Landing page
      • Published Forms page
      • Forms Admin page
      • Summary page
    • Components
      • Alert dialog
      • Attachment
      • Autocomplete
      • Captcha
      • Character counter
      • Checkbox input
      • Currency
      • Date
      • Dropdown date
      • Static and dynamic dropdown
      • Error summary
      • Grid
      • Handwritten signature
      • Hidden field
      • Image
      • Image annotation
      • Image attachment
      • Number
      • Open selection
      • Repeater
      • Formatted Text / Rich Text Editor
      • Section
      • Single-selection tree
      • Source code editor
      • Time
      • US phone
      • US state
      • Video
      • Video attachment
      • Wizard
      • XForms inspector
      • Yes/No answer
    • Features
      • Automatic calculations dependencies
      • Datasets
      • Excel and XML import
      • Excel and XML export
      • Summary page Excel Export
      • Form definitions and form data Zip Export
      • Purging historical data
      • Lease
      • Localization
      • Supported languages
      • Mobile support
      • Multitenancy
      • Form Runner navigation bar
      • PDF production
        • Automatic PDF
        • Automatic PDF header and footer configuration
        • PDF templates
      • Responsive design
      • Revision history
      • S3 storage
      • Simple data migration
      • TIFF production
      • Versioning
      • Wizard view
      • Workflow stage
    • Persistence
      • Using a relational database
      • Relational database schema
      • Purging old data using SQL
      • Auditing
      • Autosave
      • Database support
      • Flat view
    • Linking and embedding
      • Linking
      • Java Embedding API
      • JavaScript Embedding API
      • Liferay full portlet
      • Liferay proxy portlet
      • Securing Form Runner access
      • Form Runner offline embedding API
      • Angular component
      • React component
    • Access control and permissions
      • Users
      • Login & Logout
      • Deployed forms
      • Form fields
      • Editing forms
      • Owner and group member
      • Organizations
      • Scenarios
      • Token-based permissions
    • Styling
      • CSS
      • Grids CSS
      • Automatic PDF styling and CSS
    • APIs
      • Authentication of server-side service APIs
      • Persistence API
        • CRUD API
        • Search API
        • List form data attachments API
        • Form Metadata API
        • Lease API
        • Reindexing API
        • Caching
        • Versioning
        • Revision History API
        • Zip Export API
        • Custom persistence providers
      • Other APIs
        • Connection context API
        • Duplicate form data API
        • File scan API
        • Form Runner JavaScript API
        • Generate XML Schema API
        • PDF API
        • Publish form definition API
        • Run form in the background API
      • Data formats
        • Form data
        • Date and time
        • Form definition
    • Architecture and integration
      • Architecture
      • Access form data
      • Integration
    • Advanced
      • Buttons and processes
        • Simple process syntax
        • Core actions
        • Form Runner actions
          • Save action
          • Send action
          • Email action
        • XForms actions
        • Predefined buttons, processes and dialogs
        • Summary page buttons and processes
      • Custom dialogs/model logic
      • Services
      • Singleton form
      • Monitoring HTTP requests
  • XForms
    • Core
      • Attribute Value Templates (AVTs)
      • Binds
      • Validation
      • Variables
      • Keyboard focus
      • XForms JavaScript API
      • Error handling
        • Detailed behavior
      • Model-Bind variables
      • XForms 2.0 support
    • Events
      • Standard support
      • UI refresh events
      • Keyboard events
      • Extension events
      • Extension context information
      • Other event extensions
    • Actions
      • Repeat, insert and delete
      • Scripting actions
      • Extensions
    • Controls
      • Label, hint, help
      • Input
      • Output
      • Text area
      • Button
      • Upload
      • Dialog
    • Submission
      • Standard support
      • JSON support
      • Asynchronous submissions
      • Caching extension
      • Other submission extensions
    • XPath
      • Type annotations
      • Expression analysis
      • Tips
      • Compatibility
      • Standard functions
      • Maps and arrays Functions
      • Extension functions
        • Core functions
        • Utility functions
        • Model functions
        • Controls functions
        • XML functions
        • JSON functions
        • HTTP functions
        • Form Runner functions
        • Other functions
        • Deprecated functions
    • XBL components
      • FAQ
      • Guide
        • XBL Tutorial
        • Bindings
        • XForms models
        • Including content
        • Event handling
        • Conventions
        • Map XBL example
        • Learning from existing components
      • Advanced topics
        • XBL Modes
        • JavaScript companion classes
        • XBL library
        • Extensions
        • Attachment controls
    • XForms tutorial
      • Introduction
      • Installation
      • The Hello application
      • The Bookcast application
        • The basic app
        • Database access
        • Polishing the app
        • Adding a feed
    • Using XForms from Java apps
  • XML Platform
    • Page Flow Controller
      • Basics
      • XML submission
      • Navigating between pages
      • Paths and matchers
      • Other configuration elements
      • Typical combinations of page model and page view
      • Examples
      • Authorizing pages and services
    • Processors
      • URL generator
      • Request generator
      • PDF to image converter
    • Resources
      • Resource managers
      • Setting up an external resources directory
    • Other
      • Binary and text documents
  • FAQ
    • Licensing
    • PE and Dev Support
    • Form Builder and Form Runner
    • Resources and support
    • Other technical questions
  • Contributors
    • Automated tests
    • Building Orbeon Forms
    • Localizing Orbeon Forms
    • Validation functions
    • Contributor License Agreement
  • Release notes
    • Orbeon Forms 2022.1.9
    • Orbeon Forms 2024.1.1
    • Orbeon Forms 2023.1.7
    • Orbeon Forms 2024.1
    • Orbeon Forms 2023.1.6
    • Orbeon Forms 2023.1.5
    • Orbeon Forms 2021.1.11
    • Orbeon Forms 2022.1.8
    • Orbeon Forms 2023.1.4
    • Orbeon Forms 2023.1.3
    • Orbeon Forms 2023.1.2
    • Orbeon Forms 2022.1.7
    • Orbeon Forms 2023.1.1
    • Orbeon Forms 2023.1
    • Orbeon Forms 2022.1.6
    • Orbeon Forms 2021.1.10
    • Orbeon Forms 2022.1.5
    • Orbeon Forms 2021.1.9
    • Orbeon Forms 2022.1.4
    • Orbeon Forms 2022.1.3
    • Orbeon Forms 2021.1.8
    • Orbeon Forms 2022.1.2
    • Orbeon Forms 2022.1.1
    • Orbeon Forms 2022.1
    • Orbeon Forms 2021.1.7
    • Orbeon Forms 2021.1.6
    • Orbeon Forms 2021.1.5
    • Orbeon Forms 2021.1.4
    • Orbeon Forms 2021.1.3
    • Orbeon Forms 2021.1.2
    • Orbeon Forms 2021.1.1
    • Orbeon Forms 2021.1
    • Orbeon Forms 2020.1.6
    • Orbeon Forms 2019.2.4
    • Orbeon Forms 2019.1.2
    • Orbeon Forms 2018.2.5
    • Orbeon Forms 2018.1.4
    • Orbeon Forms 2020.1.5
    • Orbeon Forms 2020.1.4
    • Orbeon Forms 2020.1.3
    • Orbeon Forms 2020.1.2
    • Orbeon Forms 2019.2.3
    • Orbeon Forms 2020.1.1
    • Orbeon Forms 2020.1
    • Orbeon Forms 2019.2.2
    • Orbeon Forms 2019.2.1
    • Orbeon Forms 2019.1.1
    • Orbeon Forms 2019.2
    • Orbeon Forms 2019.1
    • Orbeon Forms 2018.2.4
  • Release history
  • Use cases
  • Product roadmap
  • Index of features
Powered by GitBook
On this page
  • Availability
  • Overview
  • Requirements
  • Login and general configuration
  • Self-signed certificate
  • Entra ID
  • Users
  • Groups
  • Application
  • Configuration files
  • Resource group
  • Storage
  • Database
  • Container registry
  • Kubernetes
  • Private network
  • Accessing the application and its logs
  • Limitations
  1. Installation

Azure

PreviousDockerNextTomcat

Last updated 3 months ago

Availability

  • [SINCE Orbeon Forms 2024.1]

  • This is an feature.

Overview

This guide walks you through deploying Orbeon Forms on Microsoft Azure using:

  • for user/group management

  • for configuration files

  • for the database

  • for container orchestration

  • for database-cluster communication

  • for custom Docker images (optional)

We will use a self-signed certificate and a single-node Kubernetes cluster for demonstration purposes. In production, you would likely use a certificate signed by a trusted certificate authority (CA) and a multi-node cluster.

The users/groups will be accessed via the OpenID Connect (OIDC) protocol and WildFly's native OIDC support implementation. Entra ID groups will be mapped to WildFly roles, which will be used to control access to Orbeon Forms.

We will create two groups/roles:

  • orbeon-user

  • orbeon-admin

As well as two test users:

  • testuser1 (member of orbeon-user)

  • testuser2 (member of orbeon-user and orbeon-admin)

Orbeon Forms will be accessible only to users from the orbeon-user group (i.e. to both testuser1 and testuser2). Form Builder and Forms Admin pages will be accessible only to users from the orbeon-admin group (i.e. to testuser2 only).

Some values in the commands used in this guide will be hardcoded for simplicity. In a real-world scenario, you would likely want to parameterize them, for example by using environment variables.

Requirements

The main requirement is an account with an Azure subscription.

The following utilities will be used during the installation process:

  • az (Azure CLI)

  • psql (PostgreSQL client)

  • kubectl (Kubernetes deployment)

  • jq (JSON manipulation)

  • base64 (Kubernetes passwords encoding)

  • core Linux utilities such as cat, curl, echo, etc.

All steps described below can also be done manually via the Azure UI.

Login and general configuration

The very first step is to login to Azure and set the Microsoft Graph API scope:

az login --scope https://graph.microsoft.com/.default

The following providers need to be registered:

az provider register --namespace Microsoft.Compute
az provider register --namespace Microsoft.ContainerRegistry
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.DBforPostgreSQL
az provider register --namespace Microsoft.Storage

Self-signed certificate

Generate a self-signed certificate for the application:

keytool \
  -genkey \
  -alias server \
  -keyalg RSA \
  -validity 3650 \
  -keysize 2048 \
  -keystore application.keystore \
  -storepass password \
  -keypass password \
  -dname "CN=localhost, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"

This command uses the keystore filename (application.keystore) and passwords (password) used by default in WildFly's configuration (standalone.xml file). Using a stronger password is recommended.

Entra ID

Retrieve the Entra ID domain:

ENTRA_ID_DOMAIN=$(az rest \
                  --method get \
                  --url 'https://graph.microsoft.com/v1.0/domains' \
                  --query 'value[0].id' \
                  -o tsv)

It should look like contoso.onmicrosoft.com.

Users

Create two test users testuser1 and testuser2 (replace contoso.onmicrosoft.com with your domain):

az ad user create \
  --user-principal-name 'testuser1@contoso.onmicrosoft.com' \
  --password 'CHANGEME0!' \
  --display-name 'Test User 1'

Optionally, you can associate an email address with the user. For this, we use the Microsoft Graph API:

az rest \
  --method patch \
  --url "https://graph.microsoft.com/v1.0/users/$user_id" \
  --body "{\"mail\":\"$email\"}"

To retrieve the user ID:

az ad user show --id 'testuser1@contoso.onmicrosoft.com' --query id -o tsv

Groups

Create two groups orbeon-user and orbeon-admin:

az ad group create --display-name 'orbeon-user' --mail-nickname 'orbeon-user'
az ad group create --display-name 'orbeon-admin' --mail-nickname 'orbeon-admin'

Add users to groups:

az ad group member add --group 'orbeon-user' --member-id "$user1_id"
az ad group member add --group 'orbeon-user' --member-id "$user2_id"
az ad group member add --group 'orbeon-admin' --member-id "$user2_id"

Application

Create an application called Orbeon Forms. This is needed to expose our users and groups to Orbeon Forms via the OIDC protocol.

az ad app create --display-name 'Orbeon Forms' --sign-in-audience 'AzureADMyOrg'

Retrieve the application ID:

ENTRA_ID_APP_ID=$(az ad app list --query "[?displayName=='Orbeon Forms'].appId" -o tsv)

You can also retrieve it from the output of the previous command.

Add an API identifier URI to the application:

az ad app update --id "$ENTRA_ID_APP_ID" --identifier-uris "api://$ENTRA_ID_APP_ID"

Retrieve the application object ID:

ENTRA_ID_APP_OBJECT_ID=$(az ad app list --query "[?displayName=='Orbeon Forms'].id" -o tsv)

Note that this is not the same as the application ID (id vs appId).

To have access to the groups/roles in both the OIDC ID and access tokens, we need to add a scope to the application. If we don't do this, WildFly's OIDC implementation will be unable to retrieve the groups/roles.

Add a scope called groups.access to the application:

az rest \
  --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" \
  --headers 'Content-Type=application/json' \
  --body "$(jq -n \
    --arg scope_id "$(uuidgen)" \
    '{
      api: {
        oauth2PermissionScopes: [{
          adminConsentDescription: "Allow the application to access groups on behalf of the signed-in user.",
          adminConsentDisplayName: "Access groups",
          id: $scope_id,
          isEnabled: true,
          type: "User",
          userConsentDescription: "Allow the application to access groups on your behalf.",
          userConsentDisplayName: "Access groups",
          value: "groups.access"
        }]
      }
    }')"

The scope ID is generated using uuidgen. Note that the jq command above is used to inject the scope ID into the JSON body. This can be done manually or in other ways as well.

For an existing scope, you can retrieve the scope ID from its name using the following command:

ENTRA_ID_SCOPE_ID=$(az ad app show \
                    --id "$ENTRA_ID_APP_ID" \
                    --query "api.oauth2PermissionScopes[?value=='groups.access'].id" \
                    -o tsv)

Pre-authorize the application, so that the users won't have to explicitly consent to the permissions:

az ad app show --id "$ENTRA_ID_APP_ID" | \
  jq --arg app_id "$ENTRA_ID_APP_ID" \
     --arg scope_id "$ENTRA_ID_SCOPE_ID" \
     '.api.preAuthorizedApplications = [{
       "appId": $app_id,
       "delegatedPermissionIds": [$scope_id]
     }]' | \
  az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" --body @-

Add a client secret:

ENTRA_ID_CREDENTIAL_SECRET=$(az ad app credential reset \
                             --id "$ENTRA_ID_APP_ID" \
                             --display-name 'Orbeon Forms Credential' \
                             --years 2 | jq -r '.password')

Beware: the command above will update/overwrite any existing secret with the same name.

Only security group membership claims will be included as group IDs:

az ad app show --id "$ENTRA_ID_APP_ID" | \
  jq '.groupMembershipClaims = "SecurityGroup"' | \
  az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" --body @-

Add optional OIDC claims (groups included as roles, email):

az ad app show --id "$ENTRA_ID_APP_ID" | \
  jq '.optionalClaims = {
    "accessToken": [{
      "additionalProperties": ["emit_as_roles"],
      "essential": false,
      "name": "groups",
      "source": null
    }],
    "idToken": [{
      "additionalProperties": ["emit_as_roles"],
      "essential": false,
      "name": "groups",
      "source": null
    },
    {
      "additionalProperties": [],
      "essential": false,
      "name": "email",
      "source": null
    }]
  }' | \
  az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$ENTRA_ID_APP_OBJECT_ID" --body @-

Note that we have updated the application manifest multiple times, using the az rest --method PATCH command, but this was mainly done for demonstration purposes. In a real-world scenario, you would likely update the application manifest only once, with all the changes.

Grant Microsoft Graph permissions:

# Azure API constants
API_MICROSOFT_GRAPH='00000003-0000-0000-c000-000000000000'
API_PERMISSION_OPENID='37f7f235-527c-4136-accd-4a02d197296e'
API_PERMISSION_EMAIL='64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'

az ad app permission add \
  --id "$ENTRA_ID_APP_ID" \
  --api "$API_MICROSOFT_GRAPH" \
  --api-permissions "$API_PERMISSION_OPENID=Scope"
  
az ad app permission add \
  --id "$ENTRA_ID_APP_ID" \
  --api "$API_MICROSOFT_GRAPH" \
  --api-permissions "$API_PERMISSION_EMAIL=Scope"

Grant admin consent for permissions above:

az ad app permission admin-consent --id "$ENTRA_ID_APP_ID"

Configuration files

Get the tenant ID:

ENTRA_ID_TENANT_ID=$(az account show --query tenantId -o tsv)

Build the OIDC provider URL:

ENTRA_ID_PROVIDER_URL="https://login.microsoftonline.com/$ENTRA_ID_TENANT_ID/v2.0"

Build the scope API URL:

ENTRA_ID_API_SCOPE_URL="api://$ENTRA_ID_APP_ID/$ENTRA_ID_SCOPE_VALUE"

Generate the OIDC configuration file:

cat << EOF > oidc.json
{
  "client-id": "$ENTRA_ID_APP_ID",
  "provider-url": "$ENTRA_ID_PROVIDER_URL",
  "credentials": {
    "secret": "$ENTRA_ID_CREDENTIAL_SECRET"
  },
  "principal-attribute": "oid",
  "scope": "profile $ENTRA_ID_API_SCOPE_URL"
}
EOF

The above configuration will return users as IDs. If you want to return users as emails, you can use email instead of oid as the value for principal-attribute.

The oidc.json file will look like this:

{
  "client-id": "4a3e3344-7f1b-4aea-b5d4-a18705757270",
  "provider-url": "https://login.microsoftonline.com/9eacdffb-6700-4a98-a9a7-507d898fdfa8/v2.0",
  "credentials": {
    "secret": "BP28Q~XQ68YAyh_N2vr1vw8EPeaGKAwRUTXGJb3N"
  },
  "principal-attribute": "oid",
  "scope": "profile api://4a3e3344-7f1b-4aea-b5d4-a18705757270/groups.access"
}

In OIDC, we will refer to the groups by their IDs (not their display names):

ENTRA_ID_USER_GROUP_ID=$(az ad group show --group 'orbeon-user' --query id -o tsv)
ENTRA_ID_ADMIN_GROUP_ID=$(az ad group show --group 'orbeon-admin' --query id -o tsv)

Generate the Form Builder permissions file:

cat << EOF > form-builder-permissions.xml
<roles>
  <role name="$ENTRA_ID_ADMIN_GROUP_ID" app="*" form="*"/>
</roles>
EOF

The form-builder-permissions.xml file will look like this:

<roles>
  <role name="79390699-2df8-4110-b0b3-2b97c3db1821" app="*" form="*"/>
</roles>
<web-app>
    <!-- PostgreSQL configuration -->
    <resource-ref>
        <description>DataSource</description>
        <res-ref-name>jdbc/postgresql</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
    <!-- Restrict Orbeon Forms to the orbeon-user group/role -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Form Runner</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>ENTRA_ID_USER_GROUP_ID</role-name>
        </auth-constraint>
    </security-constraint>
    <!-- Restrict Form Builder and Forms Admin pages to the orbeon-admin group/role -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Form Builder</web-resource-name>
            <url-pattern>/fr/orbeon/builder/*</url-pattern>
            <url-pattern>/fr/admin</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>ENTRA_ID_ADMIN_GROUP_ID</role-name>
        </auth-constraint>
    </security-constraint>
    <!-- Use OIDC for authentication -->
    <login-config>
        <auth-method>OIDC</auth-method>
    </login-config>
    <security-role>
        <role-name>ENTRA_ID_USER_GROUP_ID</role-name>
        <role-name>ENTRA_ID_ADMIN_GROUP_ID</role-name>
    </security-role>
</web-app>

Replace ENTRA_ID_USER_GROUP_ID and ENTRA_ID_ADMIN_GROUP_ID with the actual group IDs.

<server>
    <profile>
        <subsystem xmlns="urn:jboss:domain:datasources:7.2">
            <datasources>
                <!-- PostgreSQL configuration -->
                <datasource jndi-name="java:/jdbc/postgresql" pool-name="postgresql" enabled="true" use-java-context="true">
                    <connection-url>jdbc:postgresql://DATABASE_SERVER.postgres.database.azure.com:5432/orbeon?useUnicode=true&amp;characterEncoding=UTF8&amp;socketTimeout=30&amp;tcpKeepAlive=true</connection-url>
                    <driver>postgresql-42.7.3.jar</driver>
                    <security user-name="orbeon@DATABASE_SERVER" password="orbeon"/>
                </datasource>
            </datasources>
        </subsystem>
    </profile>
</server>

Replace DATABASE_SERVER with your database server name, which must be unique across Azure.

We need jboss-web-xml to be configured for PostgreSQL:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
    <resource-ref>
        <res-ref-name>jdbc/postgresql</res-ref-name>
        <jndi-name>java:/jdbc/postgresql</jndi-name>
    </resource-ref>
</jboss-web>

We will also configure the Orbeon Forms properties in properties-local.xml to use PostgreSQL instead of SQLite:

<properties xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <!-- Change the following values for production use -->
    <property as="xs:string" name="oxf.crypto.password"              value="J$WYh!Ltgg4Q^jnR"/>
    <property as="xs:string" name="oxf.fr.field-encryption.password" value=""/>
    <property as="xs:string" name="oxf.fr.access-token.password"     value=""/>

    <property as="xs:string"  name="oxf.fr.persistence.provider.*.*.*" value="postgresql"/>
    <property as="xs:boolean" name="oxf.fr.persistence.sqlite.active"  value="false"/>
</properties>

Resource group

Create a resource group:

az group create \
  --name 'orbeon-forms-resource-group' \
  --location 'westus'

It will be used by all the resources we create (Azure Storage, PostgreSQL, etc.).

Storage

Create a storage account:

az storage account create \
  --name "$STORAGE_ACCOUNT" \
  --resource-group 'orbeon-forms-resource-group' \
  --location 'westus' \
  --sku Standard_LRS

The storage account name must be unique across Azure.

Create a storage share:

az storage share create \
  --name 'orbeon-forms-share' \
  --account-name "$STORAGE_ACCOUNT"

To upload a file to the storage share, use the following command:

az storage file upload \
  --account-name "$STORAGE_ACCOUNT" \
  --share-name 'orbeon-forms-share' \
  --source "$source" \
  --path "$destination"

We will need to upload the following configuration files:

  • application.keystore

  • form-builder-permissions.xml

  • jboss-web.xml

  • oidc.json

  • properties-local.xml

  • standalone.xml

  • web.xml

Retrieve the storage account access key:

STORAGE_ACCOUNT_SECRET_KEY=$(az storage account keys list \
                             --account-name "$STORAGE_ACCOUNT" \
                             --resource-group 'orbeon-forms-resource-group' \
                             --query '[0].value' \
                             --output tsv)

Alternatively, all configuration files could be included in a custom Docker image, but this is less flexible when configuration needs to be changed.

Database

Create a PostgreSQL database server:

az postgres flexible-server create \
  --name "$DATABASE_SERVER" \
  --resource-group 'orbeon-forms-resource-group' \
  --location 'westus' \
  --admin-user "$DATABASE_ADMIN_USERNAME" \
  --admin-password "$DATABASE_ADMIN_PASSWORD" \
  --sku-name standard_d2ds_v4 \
  --version 16 \
  --public-access None

The database server name must be unique across Azure.

Retrieve your client's public IP address:

DATABASE_PUBLIC_IP=$(curl -s 'https://api.ipify.org')

This will allow you to configure a firewall rule to allow access to the database server only from your client's IP address.

az postgres flexible-server firewall-rule create \
  --rule-name 'local-ip-allowed' \
  --name "$DATABASE_SERVER" \
  --resource-group 'orbeon-forms-resource-group' \
  --start-ip-address "$DATABASE_PUBLIC_IP" \
  --end-ip-address "$DATABASE_PUBLIC_IP"

Alternatively, you can call az postgres flexible-server create without --public-access None. This will automatically create the firewall rule above.

Create the orbeon database:

az postgres flexible-server db create \
  --database-name 'orbeon' \
  --server-name "$DATABASE_SERVER" \
  --resource-group 'orbeon-forms-resource-group';

Make the database administrator password available to the psql command:

export PGPASSWORD="$DATABASE_ADMIN_PASSWORD"

Create the orbeon database user:

psql \
  --host "$DATABASE_SERVER.postgres.database.azure.com" \
  --username "$DATABASE_ADMIN_USERNAME" \
  --dbname 'orbeon' \
  --command "CREATE USER \"orbeon@$DATABASE_SERVER\" WITH PASSWORD '$password';"

Note that Azure PostgreSQL users need to follow the format username@servername.

Grant privileges to the orbeon database user:

psql \
  --host "$DATABASE_SERVER.postgres.database.azure.com" \
  --username "$DATABASE_ADMIN_USERNAME" \
  --dbname 'orbeon' \
  --command "GRANT ALL PRIVILEGES ON DATABASE orbeon TO \"orbeon@$DATABASE_SERVER\";" \
  --command "GRANT ALL PRIVILEGES ON SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
  --command "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
  --command "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"orbeon@$DATABASE_SERVER\";" \
  --command "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"orbeon@$DATABASE_SERVER\";" \
  --command "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \"orbeon@$DATABASE_SERVER\";";

Create the Orbeon Forms database schema:

psql \
  --host "$DATABASE_SERVER.postgres.database.azure.com" \
  --username "$DATABASE_ADMIN_USERNAME" \
  --dbname 'orbeon' \
  --file ./postgresql-2024_1.sql;

You can then delete the local-ip-allowed firewall rule:

az postgres flexible-server firewall-rule delete \
  --rule-name 'local-ip-allowed' \
  --name "$DATABASE_SERVER" \
  --resource-group 'orbeon-forms-resource-group' \
  --yes;

Alternatively, you can use other databases, such as Azure SQL Database or Azure Database for MySQL.

Container registry

If you need to use a custom Docker image, create an Azure Container Registry:

az acr create --name "$CONTAINER_REGISTRY" --resource-group 'orbeon-forms-resource-group' --sku Basic

The container registry name must be unique across Azure.

Then login to the Azure Container Registry:

az acr login --name "$CONTAINER_REGISTRY"

Retrieve the Azure Container Registry ID:

CONTAINER_REGISTRY_ID=$(az acr show --name "$CONTAINER_REGISTRY" --resource-group 'orbeon-forms-resource-group' --query id -o tsv)

Create a Dockerfile to customize the Orbeon Forms Docker image:

cat > Dockerfile << EOF
FROM orbeon/orbeon-forms:2024.1-pe-wildfly
# TODO: customize your image here
EOF

Build the Docker image:

docker build --platform 'linux/amd64' -t 'orbeon-forms-custom:2024.1-pe-wildfly' .

Tag the Docker image with the full Azure Container Registry URL

docker tag 'orbeon-forms-custom:2024.1-pe-wildfly' "$CONTAINER_REGISTRY.azurecr.io/orbeon-forms-custom:2024.1-pe-wildfly"

Push the Docker image to the Azure Container Registry:

docker push "$CONTAINER_REGISTRY.azurecr.io/orbeon-forms-custom:2024.1-pe-wildfly"

Kubernetes

Create an Azure Kubernetes Service (AKS) cluster:

az aks create \
  --name 'orbeon-forms-cluster' \
  --resource-group 'orbeon-forms-resource-group' \
  --node-count 1 \
  --network-plugin azure \
  --generate-ssh-keys

The AKS cluster name must be unique across Azure.

Retrieve AKS credentials, save them locally to ~/.kube/config, and set the AKS cluster as the current context:

az aks get-credentials \
  --name 'orbeon-forms-cluster' \
  --resource-group 'orbeon-forms-resource-group' \
  --overwrite-existing

If you use a custom Docker image, you need to grant permission to the cluster to pull images from the Azure Container Registry.

# Retrieve the cluster's client ID
K8S_CLIENT_ID=$(az aks show \
                --name 'orbeon-forms-cluster' \
                --resource-group 'orbeon-forms-resource-group' \
                --query 'identityProfile.kubeletidentity.clientId' \
                -o tsv)

az role assignment create --assignee "$K8S_CLIENT_ID" --role AcrPull --scope "$CONTAINER_REGISTRY_ID"

Generate the storage account name/key secret file

cat > storage-secret.yaml << EOF
apiVersion: v1
kind: Secret
metadata:
  name: storage-secret
type: Opaque
data:
  azurestorageaccountname: $(echo -n "$STORAGE_ACCOUNT" | base64)
  azurestorageaccountkey: $(echo -n "$STORAGE_ACCOUNT_SECRET_KEY" | base64)
EOF

The Azure Storage account name and key need to be encoded in Base64.

Import the storage account name/key secret

kubectl apply -f storage-secret.yaml

Generate the persistence volume configuration file:

cat > orbeon-forms-pv.yaml << EOF
apiVersion: v1
kind: PersistentVolume
metadata:
  name: orbeon-forms-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  storageClassName: azure-file
  azureFile:
    secretName: storage-secret
    shareName: orbeon-forms-share
    readOnly: false
EOF

Import the persistence volume configuration:

kubectl apply -f orbeon-forms-pv.yaml

Generate the persistence volume claim configuration file:

cat > orbeon-forms-pvc.yaml << EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: orbeon-forms-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: azure-file
  resources:
    requests:
     storage: 5Gi
EOF

Import the persistence volume claim configuration:

kubectl apply -f orbeon-forms-pvc.yaml

Depending on whether you use the default Orbeon Forms image or a custom one, you will need to set the image name as follows:

# Unmodified Orbeon Forms image
K8S_IMAGE='orbeon/orbeon-forms:2024.1-pe-wildfly'

# Custom Orbeon Forms image
K8S_IMAGE="$CONTAINER_REGISTRY.azurecr.io/orbeon-forms-custom:2024.1-pe-wildfly"

Generate the deployment configuration file:

cat > orbeon-forms-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orbeon-forms-deployment
  labels:
  app: orbeon-forms
spec:
  replicas: 1
  selector:
    matchLabels:
     app: orbeon-forms
  template:
    metadata:
      labels:
        app: orbeon-forms
    spec:
      containers:
      - name: orbeon-forms
        image: $K8S_IMAGE
        ports:
        - containerPort: 8443
        volumeMounts:
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/resources/config/license.xml
            subPath: license.xml
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/resources/config/form-builder-permissions.xml
            subPath: form-builder-permissions.xml
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/resources/config/properties-local.xml
            subPath: properties-local.xml
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/jboss-web.xml
            subPath: jboss-web.xml
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/oidc.json
            subPath: oidc.json
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/deployments/orbeon.war/WEB-INF/web.xml
            subPath: web.xml
          - name: azure-volume
            mountPath: /opt/jboss/wildfly/standalone/configuration/application.keystore
            subPath: application.keystore
          - name: azure-volume
            mountPath: /docker-entrypoint-wildfly.d/standalone.xml
            subPath: standalone.xml
      volumes:
        - name: azure-volume
          persistentVolumeClaim:
          claimName: orbeon-forms-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: orbeon-forms-service
spec:
  type: LoadBalancer
  selector:
    app: orbeon-forms
  ports:
    - protocol: TCP
      port: 443
      targetPort: 8443
EOF

Import the deployment configuration:

kubectl apply -f orbeon-forms-deployment.yaml

You can display information about you Kubernetes contexts, cluster, pods, nodes, and service with the following commands:

# Contexts information
kubectl config get-contexts

# Cluster information
kubectl cluster-info

# Pods information
kubectl describe pods

# Nodes information
kubectl get nodes -o wide

# Service information
kubectl get service 'orbeon-forms-service'

Retrieve the cluster's external/public IP:

K8S_EXTERNAL_IP=$(kubectl get service 'orbeon-forms-service' --output jsonpath='{.status.loadBalancer.ingress[0].ip}')

Orbeon Forms will be available from the following URL:

K8S_APP_URL="https://$K8S_EXTERNAL_IP/orbeon"

Update the Entra ID redirect URIs with the actual Orbeon Forms URL:

az ad app update --id "$ENTRA_ID_APP_ID" --web-redirect-uris "$K8S_APP_URL/*"

Retrieve the Kubernetes pod name, which can be used to retrieve the logs (see below):

K8S_POD=$(kubectl get pod -o name | head -1)

Alternatively, you can use Azure Container Instances (ACI) for a simpler deployment, but with limited support for file mounts, port mappings, etc.

Private network

The last step is to configure a private network to allow the Kubernetes cluster to access the PostgreSQL database server.

Create a private DNS zone:

az network private-dns zone create \
  --resource-group 'orbeon-forms-resource-group' \
  --name 'private.postgres.database.azure.com'

The network resource group we will use below is the Kubernetes node resource group. It follows the following format:

K8S_NODE_RESOURCE_GROUP="MC_${RESOURCE_GROUP}_${K8S_CLUSTER}_${AZURE_LOCATION}"

Retrieve the virtual network name:

NETWORK_VNET_NAME=$(az network vnet list \
                    --resource-group 'MC_orbeon-forms-resource-group_orbeon-forms-cluster_westus' \
                    --query '[0].name' \
                    --output tsv)

Retrieve the virtual network ID:

NETWORK_VNET_ID=$(az network vnet show \
                  --resource-group 'MC_orbeon-forms-resource-group_orbeon-forms-cluster_westus' \
                  --name "$NETWORK_VNET_NAME" \
                  --query 'id' \
                  --output tsv)

Link the private DNS zone to the Kubernetes cluster virtual network:

az network private-dns link vnet create \
  --resource-group 'orbeon-forms-resource-group' \
  --zone-name 'private.postgres.database.azure.com' \
  --name 'MyDNSLink' \
  --virtual-network "$NETWORK_VNET_ID" \
  --registration-enabled false

Retrieve the subnet name:

NETWORK_SUBNET_NAME=$(az network vnet subnet list \
                      --resource-group 'MC_orbeon-forms-resource-group_orbeon-forms-cluster_westus' \
                      --vnet-name "$NETWORK_VNET_NAME" \
                      --query '[0].name' \
                      --output tsv)

Retrieve the subnet ID:

NETWORK_SUBNET_ID=$(az network vnet subnet show \
                    --resource-group 'MC_orbeon-forms-resource-group_orbeon-forms-cluster_westus' \
                    --vnet-name "$NETWORK_VNET_NAME" \
                    --name "$NETWORK_SUBNET_NAME" \
                    --query 'id' \
                    --output tsv)

Retrieve the database server ID:

DATABASE_SERVER_ID=$(az postgres flexible-server show \
                     --resource-group 'orbeon-forms-resource-group' \
                     --name "$DATABASE_SERVER" \
                     --query 'id' \
                     --output tsv)

Create a private endpoint:

az network private-endpoint create \
  --resource-group 'orbeon-forms-resource-group' \
  --name 'postgres-pe' \
  --subnet "$NETWORK_SUBNET_ID" \
  --private-connection-resource-id "$DATABASE_SERVER_ID" \
  --connection-name 'postgres-connection' \
  --group-id 'postgresqlServer'

Retrieve the private IP:

NETWORK_PRIVATE_IP=$(az network private-endpoint show \
                     --resource-group 'orbeon-forms-resource-group' \
                     --name 'postgres-pe' \
                     --query 'customDnsConfigs[0].ipAddresses[0]' \
                     --output tsv)

Create a private DNS record:

az network private-dns record-set a add-record \
  --resource-group 'orbeon-forms-resource-group' \
  --zone-name 'private.postgres.database.azure.com' \
  --record-set-name "$DATABASE_SERVER" \
  --ipv4-address "$NETWORK_PRIVATE_IP"

The database server will now be reachable from the Kubernetes cluster as $DATABASE_SERVER.private.postgres.database.azure.com.

Accessing the application and its logs

Orbeon Forms is now available from the following URL:

K8S_APP_URL="https://$K8S_EXTERNAL_IP/orbeon"

You can display and follow the Orbeon Forms logs using the following command:

kubectl logs $K8S_POD -f

Limitations

Here is a list of limitations and possible improvements:

  • Groups/roles are returned by Entra ID as IDs, not names.

  • The example Bash script has been tested on macOS only, but should work on Linux and Windows, using the Windows Subsystem for Linux (WSL).

  • No actual load balancing is configured in the Kubernetes cluster.

  • The TLS/SSL configuration should be done at the Application Gateway Ingress Controller level and not at the WildFly level.

  • Azure Resource Manager (ARM) templates could be used to automate the deployment.

A more complete example Bash script is available on . It includes more commands, which will check if the resources already exist, among other things, but it follows roughly the same steps as described here.

The follows the format prefix@domain.

Extract the web.xml file from the or download , and make sure the following lines are present:

Download and make sure the following lines are present:

license.xml (get a free trial license if needed)

The SQL files needed to create the Orbeon Forms database schema can be downloaded from .

Note that the standalone.xml file is mounted in the container as docker-entrypoint-wildfly.d/standalone.xml. This is because WildFly needs to move/rename that file, so it needs to be copied to instead of mounted directly in the /opt/jboss/wildfly/standalone/configuration configuration directory. See for more information. The WildFly version of Orbeon Forms will copy any standalone.xml found in docker-entrypoint-wildfly.d to the WildFly configuration directory.

Orbeon Forms PE
Entra ID
Azure Storage
PostgreSQL
Kubernetes
Azure Virtual Network
Azure Container Registry
GitHub
user principal name (UPN)
Orbeon Forms WAR file
web.template.xml from GitHub
standalone.postgresql.azure.xml from GitHub
here
this issue on GitHub
PostgreSQL database setup