Azure
Availability
[SINCE Orbeon Forms 2024.1]
This is an Orbeon Forms PE feature.
Overview
This guide walks you through deploying Orbeon Forms on Microsoft Azure using:
Entra ID for user/group management
Azure Storage for configuration files
PostgreSQL for the database
Kubernetes for container orchestration
Azure Virtual Network for database-cluster communication
Azure Container Registry 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 oforbeon-user
)testuser2
(member oforbeon-user
andorbeon-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).
A more complete example Bash script is available on GitHub. 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.
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 '[email protected]' \
--password 'CHANGEME0!' \
--display-name 'Test User 1'
The user principal name (UPN) follows the format prefix@domain
.
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 '[email protected]' --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>
Extract the web.xml
file from the Orbeon Forms WAR file or download web.template.xml
from GitHub, and make sure the following lines are present:
<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.
Download standalone.postgresql.azure.xml
from GitHub and make sure the following lines are present:
<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&characterEncoding=UTF8&socketTimeout=30&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
license.xml
(get a free trial license here if needed)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;
The SQL files needed to create the Orbeon Forms database schema can be downloaded from PostgreSQL database setup.
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
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 this issue on GitHub 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.
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.
Last updated