Deployment guide

Apache Age

Overview

Apache AGE (A Graph Extension) is an Apache-2.0 licensed PostgreSQL extension that adds graph database functionality and the openCypher query language to PostgreSQL. One engine serves both your relational tables and your graph workloads, so there is no separate database to operate. This SecureAnyCloud image gives you a ready-to-query AGE database on Microsoft Azure, backed by 24/7 SecureAnyCloud support.

The image installs PostgreSQL 16 from the official PGDG repository and builds Apache AGE 1.6.0 from the official Apache source release against the PostgreSQL 16 server headers. PostgreSQL runs as the dedicated postgres system user, and the cluster (PGDATA) is stored on a dedicated Azure data disk. The image seeds a small example graph named demo and rotates a unique password into the VM on first boot.

What you get:

  • PostgreSQL 16 (from the official PGDG apt repository) with Apache AGE 1.6.0 built from the Apache source release
  • The age extension created and loaded via shared_preload_libraries in a demo database
  • A seeded example graph (demo) with nodes and relationships so Cypher queries return data immediately
  • A unique per-VM password generated on first boot for a dedicated non-superuser application role (age)
  • PGDATA on a dedicated 30 GiB Azure data disk at /var/lib/age-postgres
  • Loopback-only listener on 5432 with scram-sha-256 password authentication (not exposed publicly)
  • 24/7 SecureAnyCloud support

This is a datastore product: PostgreSQL listens on 127.0.0.1:5432 only. The port is not opened to the internet. Connect locally on the VM or over an SSH tunnel.

Prerequisites

Before you deploy, make sure you have:

  • An active Azure subscription.
  • An SSH key pair.
  • A virtual network and subnet in the target region.
  • A VM size of Standard_B2s (2 vCPU / 4 GiB RAM) or larger. Standard_B2s is a good starting point.
  • A network security group (NSG) rule that allows inbound 22/tcp from your management network.

No inbound application ports are needed, because you reach PostgreSQL over the SSH tunnel.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal and choose Create a resource. Search the Marketplace for Apache AGE by SecureAnyCloud, and select Create.

  1. On Basics, pick your subscription, resource group, region and size.
  2. Under Administrator account, choose SSH public key and paste your key.
  3. Under Inbound port rules, allow SSH (22) only.
  4. Choose Review + create, then Create.

Step 2 - Deploy from the Azure CLI

To deploy from the command line instead, run:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:apache-age-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name apache-age \
  --image sac1647283583153:apache-age-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

Step 3 - Connect to your VM

Connect to the VM over SSH:

ssh azureuser@<vm-public-ip>

Step 4 - Confirm PostgreSQL and Apache AGE are installed and running

Check the PostgreSQL version and service state:

psql --version sudo systemctl is-active postgresql@16-main.service

The output shows PostgreSQL 16 and active:

psql (PostgreSQL) 16.14 (Ubuntu 16.14-1.pgdg24.04+1) active

PostgreSQL listens on loopback only. Confirm that the listener is bound to 127.0.0.1:

ss -tln | grep 5432
LISTEN 0 200 127.0.0.1:5432 0.0.0.0:*

Step 5 - Retrieve your per-VM password

Each VM generates its own unique password on first boot for the age application role. It writes the password to a credentials file that only root can read:

sudo cat /root/age-credentials.txt

The file contains AGE_PASSWORD, the username (age), the database (demo), the connection details and the SSH tunnel instructions. Store the password in your secrets manager.

In the commands below, <AGE_PASSWORD> stands for the value of AGE_PASSWORD.

Step 6 - Confirm the AGE extension is loaded

The age extension is created in the demo database and loaded automatically through shared_preload_libraries. The openCypher functions are available to the application role with no LOAD statement. List the installed extension:

PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "\dx age"
List of installed extensions Name | Version | Schema | Description ------+---------+------------+------------------------ age | 1.6.0 | ag_catalog | AGE database extension (1 row)

scram-sha-256 authentication rejects a wrong password, so the database is never reachable without the per-VM credential.

Step 7 - Inspect and extend the seeded graph

The image ships a seeded graph named demo. List the graphs, then add a node with a Cypher CREATE statement:

PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "SELECT graphid, name FROM ag_catalog.ag_graph;"
graphid | name ---------+------ 16986 | demo (1 row)
PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "SELECT * FROM cypher('demo', \$\$ CREATE (p:Person {name:'Grace Hopper', role:'Computer Scientist'}) RETURN p \$\$) as (p agtype);"
p -------------------------------------------------------------------------------------------------------------------------- {"id": 844424930131971, "label": "Person", "properties": {"name": "Grace Hopper", "role": "Computer Scientist"}}::vertex (1 row)

Step 8 - Query the graph with openCypher

Run a MATCH query to traverse the seeded relationships. The first query returns everyone who works at SecureAnyCloud, and the next returns every relationship in the graph:

PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "SELECT * FROM cypher('demo', \$\$ MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name \$\$) as (person agtype, company agtype);"
person | company -------------------+------------ "Ada Lovelace" | "sac" "Charles Babbage" | "sac" (2 rows)
PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "SELECT * FROM cypher('demo', \$\$ MATCH (a)-[r]->(b) RETURN a.name, type(r), b.name \$\$) as (src agtype, rel agtype, dst agtype);"
src | rel | dst -------------------+------------+------------------- "Ada Lovelace" | "KNOWS" | "Charles Babbage" "Ada Lovelace" | "WORKS_AT" | "sac" "Charles Babbage" | "WORKS_AT" | "sac" (3 rows)

Step 9 - Confirm authentication and loopback-only binding

The age role authenticates with scram-sha-256. A wrong password is rejected, and the per-VM password is accepted:

PGPASSWORD=<AGE_PASSWORD> psql -h 127.0.0.1 -U age -d demo -c "SELECT current_user, current_database();"
current_user | current_database --------------+------------------ age | demo (1 row)

The listener is bound to 127.0.0.1 only, so a connection to the VM's private or public IP on 5432 is refused. The database is never open to the network.

Step 10 - Connect remotely over an SSH tunnel

PostgreSQL listens on 127.0.0.1:5432 only and is not exposed publicly. This is the secure default.

To reach it from your workstation, open an SSH tunnel and point a local client at localhost:5432:

# On your workstation:
ssh -L 5432:127.0.0.1:5432 azureuser@<vm-public-ip>
# In another terminal, with a local psql: PGPASSWORD=<the password from /root/age-credentials.txt> psql -h 127.0.0.1 -p 5432 -U age -d demo

For remote access without a tunnel, attach a private network interface and require TLS on the PostgreSQL listener. Never expose 5432 to the internet without transport encryption.

Maintenance

  • Persistence: PGDATA lives on the dedicated data disk at /var/lib/age-postgres/16/main, so your graphs survive reboots and you can resize the volume independently.
  • Password: rotate the application role password with this command: sudo -u postgres psql -c "ALTER USER age WITH PASSWORD '<new-password>';".
  • Graphs: create more graphs with SELECT create_graph('<name>'); and query them with cypher('<name>', $$ ... $$).
  • Tuning: edit /etc/postgresql/16/main/conf.d/10-sac-age.conf (and the rest of /etc/postgresql/16/main/), then run sudo systemctl restart postgresql@16-main.service.
  • Security patches: unattended-upgrades stays enabled, so the OS keeps receiving security updates automatically.
  • Compatibility: Apache AGE 1.6.0 supports PostgreSQL 14-17; this image pins PostgreSQL 16. Standard PostgreSQL clients, drivers and tooling work unchanged against 5432.

Support

SecureAnyCloud provides 24/7 expert support for this image. Contact sales@secureanycloud.com.

Apache AGE, Apache and the Apache feather logo are trademarks of the Apache Software Foundation. PostgreSQL is a trademark of the PostgreSQL Community Association. This image is provided by SecureAnyCloud and is not affiliated with or endorsed by the Apache Software Foundation or the PostgreSQL Global Development Group.

Deploy on Azure

Launch Apache AGE on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure