Aa
Developer Tools Azure

Apache AGE on Ubuntu 24.04 on Azure User Guide

| Product: Apache AGE on Ubuntu 24.04 LTS on Azure

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. A single engine serves both your relational tables and your graph workloads, with no separate database to operate. The cloudimg 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, runs PostgreSQL as the dedicated postgres system user, stores the cluster (PGDATA) on a dedicated Azure data disk, seeds a small example graph named demo, and rotates a unique password into the image on first boot. Backed by 24/7 cloudimg support.

What is included:

  • 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 cloudimg 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

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point. NSG inbound: allow 22/tcp from your management network. No inbound application ports are needed because PostgreSQL is reached over the SSH tunnel.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Apache AGE by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) only. Then Review + create -> Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name apache-age \
  --image <marketplace-image-urn> \
  --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

ssh azureuser@<vm-public-ip>

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

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

You should see PostgreSQL 16 and active:

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

PostgreSQL listens on loopback only - confirm 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:*

PostgreSQL 16 and Apache AGE version and service status

Step 5 - Retrieve your per-VM password

Each VM generates its own unique password on first boot for the age application role and writes it to a root-only credentials file:

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 via shared_preload_libraries, so the openCypher functions are available to the application role with no LOAD required. 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)

A wrong password is rejected by scram-sha-256 authentication, 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:

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)

create_graph and a Cypher CREATE statement

Step 8 - Query the graph with openCypher

Run a MATCH to traverse the seeded relationships - for example, everyone who works at cloudimg, then 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"    | "cloudimg"
 "Charles Babbage" | "cloudimg"
(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" | "cloudimg"
 "Charles Babbage" | "WORKS_AT" | "cloudimg"
(3 rows)

Cypher MATCH over the seeded graph

Step 9 - Confirm authentication and loopback-only binding

The age role authenticates with scram-sha-256. A wrong password is rejected; 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)

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

Per-VM password authentication success and failure

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 production remote access without a tunnel, attach a private NIC 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 the volume is independently resizable.
  • Password: rotate the application role password with 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-cloudimg-age.conf (and the rest of /etc/postgresql/16/main/) and run sudo systemctl restart postgresql@16-main.service.
  • Security patches: unattended-upgrades remains enabled so the OS continues to receive 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

cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.

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 cloudimg and is not affiliated with or endorsed by the Apache Software Foundation or the PostgreSQL Global Development Group.