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
ageextension created and loaded viashared_preload_librariesin ademodatabase - 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
5432withscram-sha-256password 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/tcpfrom 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.
- 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.
- 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 StandardStep 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.serviceThe output shows PostgreSQL 16 and active:
psql (PostgreSQL) 16.14 (Ubuntu 16.14-1.pgdg24.04+1) activePostgreSQL listens on loopback only. Confirm that the listener is bound to 127.0.0.1:
ss -tln | grep 5432LISTEN 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.txtThe 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 demoFor 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 withcypher('<name>', $$ ... $$). - Tuning: edit
/etc/postgresql/16/main/conf.d/10-sac-age.conf(and the rest of/etc/postgresql/16/main/), then runsudo 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
Overview
Apache AGE is an open-source extension that adds graph database functionality and the openCypher query language to PostgreSQL, so one database engine serves both relational and graph workloads. This SecureAnyCloud image gives you AGE ready to query on Amazon EC2, with a unique database password generated for each instance.
The image ships PostgreSQL 16 from the official PostgreSQL (PGDG) package repository, with Apache AGE 1.6.0 built from the official Apache source release and installed as an extension. AGE is loaded through shared_preload_libraries, so Cypher queries work the moment you connect. A small demo graph is seeded during the build, so MATCH queries return data straight away.
What you get:
- A headless database image. There is no web interface. You administer it over SSH with the
psqlclient. - A private listener. PostgreSQL binds to loopback only on
127.0.0.1:5432and is never exposed to the network. Password authentication usesscram-sha-256, and a dedicated non-superuser application role namedageowns the demo database. - No shared credentials. On the first boot of your instance, a one-shot service generates a fresh, strong password for the
agerole that is unique to that instance. It applies the password to the database and proves that the new password authenticates while the build-time password is refused. Only then does it write the password to/root/age-credentials.txt, a file that only the root user can read. No shared or default database credentials ship in the image. - A dedicated data volume. PostgreSQL's data directory lives on a dedicated, independently resizable EBS data volume mounted at
/var/lib/age-postgres. Your graph data sits on durable storage that you can grow, snapshot and back up independently of the operating system disk.
Prerequisites
Before you deploy this image, make sure you have:
- An Amazon Web Services account where you can launch EC2 instances.
- IAM permissions to launch instances, create security groups and subscribe to AWS Marketplace products.
- An EC2 key pair in the target Region for SSH access to the instance.
- A VPC and subnet in the target Region, with a security group that allows inbound port 22 from your management network.
- The AWS CLI (version 2) installed locally, if you plan to deploy from the command line.
Recommended instance type: m5.large (2 vCPU, 8 GB RAM) or larger. Additional memory helps PostgreSQL with larger graphs and heavier query workloads.
Launch the Instance from the AWS Marketplace
Sign in to the AWS Management Console, open the EC2 service and select Launch instance. Under Application and OS Images, choose AWS Marketplace AMIs and search for Apache AGE. Select the SecureAnyCloud listing and choose Select, then Continue on the subscription summary.
Pick an instance type of m5.large or larger. Choose your EC2 key pair under Key pair (login).
Under Network settings, select your VPC and subnet. Create or select a security group that allows inbound port 22 from your management network. Leave the root volume at the default size or larger. The image attaches the PostgreSQL data volume automatically.
Select Launch instance. First boot initialization runs once, after the instance state becomes Running and the status checks pass. It brings up PostgreSQL, generates the per-instance database password, proves the rotation, then writes the credentials file. This takes a minute or two.
Launch the Instance from the AWS CLI
This command launches an instance from the SecureAnyCloud Apache AGE Marketplace AMI into an existing subnet and security group. Make these replacements:
- Replace <ami-id> with the AMI ID shown on the Marketplace listing.
- Replace <key-name> with your EC2 key pair name.
- Replace <subnet-id> with your subnet ID.
- Replace <security-group-id> with a security group that opens port 22 from your management network.
aws ec2 run-instances \
--image-id <ami-id> \
--instance-type m5.large \
--key-name <key-name> \
--subnet-id <subnet-id> \
--security-group-ids <security-group-id> \
--metadata-options HttpTokens=required \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=apache-age}]'When the instance reaches the Running state and its status checks pass, note its public IP address or DNS name. You can find them in the EC2 console or with aws ec2 describe-instances.
Connect to Your Instance
Connect over SSH with your key pair. On the Ubuntu 24.04 variant, the SSH login user is ubuntu.
ssh -i <key-name>.pem ubuntu@<public-ip>A welcome banner prints the most useful commands. Retrieve the per-instance database password that first boot generated:
sudo cat /root/age-credentials.txtThe file contains the AGE_PASSWORD value for this instance, the username (age), the database (demo), the seeded graph name (demo) and ready-to-paste connection examples. The commands in the following steps read the password straight from this file, so you can run them as written.
Verify PostgreSQL and the AGE Extension
PostgreSQL runs as a system service that starts on boot. Confirm that the service is active:
systemctl is-active postgresql@16-main.serviceThe command prints active. Confirm the PostgreSQL server version:
sudo -u postgres /usr/lib/postgresql/16/bin/postgres --versionConfirm that the listener is bound to loopback only, so the database is never exposed to the network:
ss -tln | grep 5432The address is 127.0.0.1:5432, never 0.0.0.0.
Now confirm that the Apache AGE extension is installed in the demo database. The command reads the per-instance password from the credentials file:
PW=$(sudo grep '^AGE_PASSWORD=' /root/age-credentials.txt | cut -d= -f2-) PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo -tAc "SELECT extname, extversion FROM pg_extension WHERE extname='age';"The command prints age|1.6.0.
Query the Seeded Demo Graph with openCypher
The image seeds a small demo graph named demo, so you have data to query immediately. AGE is preloaded, so the age role runs openCypher directly through the cypher() function without any LOAD statement. Return the seeded people and their roles:
PW=$(sudo grep '^AGE_PASSWORD=' /root/age-credentials.txt | cut -d= -f2-) PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo \
-c "SELECT * FROM cypher('demo', \$\$ MATCH (n:Person) RETURN n.name, n.role \$\$) as (name agtype, role agtype);"Traverse the relationships between the seeded nodes:
PW=$(sudo grep '^AGE_PASSWORD=' /root/age-credentials.txt | cut -d= -f2-) PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" 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 (a agtype, rel agtype, b agtype);"The PGOPTIONS setting puts the AGE catalog schema (ag_catalog) on the search path, so the cypher() function resolves. The age role already has this schema on its default search path for the demo database. Passing it explicitly makes these one-off commands self-contained.
Create and Query Your Own Graph
Create a new graph, add a node with openCypher, read it back, then remove the example graph. Every statement reads the per-instance password from the credentials file:
PW=$(sudo grep '^AGE_PASSWORD=' /root/age-credentials.txt | cut -d= -f2-) PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo \
-c "SELECT create_graph('myapp');" PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo \
-c "SELECT * FROM cypher('myapp', \$\$ CREATE (p:Product {name:'widget', price:9}) RETURN p \$\$) as (p agtype);" PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo \
-c "SELECT * FROM cypher('myapp', \$\$ MATCH (p:Product) RETURN p.name, p.price \$\$) as (name agtype, price agtype);" PGOPTIONS='-c search_path=ag_catalog,public' PGPASSWORD="$PW" psql -h 127.0.0.1 -U age -d demo \
-c "SELECT drop_graph('myapp', true);"create_graph provisions a new graph, the CREATE Cypher statement inserts a node, MATCH reads it back, and drop_graph('myapp', true) removes the example graph and its data. Your real graphs are not affected. This final step only tidies up the walkthrough.
You can also open an interactive session with PGPASSWORD='<password>' psql -h 127.0.0.1 -U age -d demo and run relational SQL and openCypher side by side in the same connection.
Remote Access over an SSH Tunnel
PostgreSQL listens on loopback only and the security group opens port 22 only, so the database is not reachable from the network.
To connect a local client such as psql, a BI tool or an application, open an SSH tunnel from your workstation. The tunnel forwards a local port to 127.0.0.1:5432 on the instance:
ssh -i <key-name>.pem -L 5432:127.0.0.1:5432 ubuntu@<public-ip>With the tunnel open, point your local client at 127.0.0.1:5432, database demo, user age, and the password from the credentials file. All database traffic stays inside the encrypted SSH channel. Do not open port 5432 to the internet without TLS in front of it.
The Data Volume
PostgreSQL's data directory lives on a dedicated EBS volume mounted at /var/lib/age-postgres. The cluster is initialized directly into this path, so every table, index and graph sits off the operating system disk. Confirm the mount:
df -h /var/lib/age-postgresTo grow the volume, expand the EBS volume in the AWS console, then grow the filesystem on the instance with sudo resize2fs on the underlying device.
Snapshot the volume for point-in-time backups of the database, or take logical backups with pg_dump against the demo database.
Connect Applications
Point your applications at the database through the SSH tunnel from step 7. Alternatively, open port 5432 to trusted clients in the instance security group once you have TLS in place.
Any PostgreSQL driver works. Run relational SQL as usual, and run graph queries through the cypher() function after putting ag_catalog on the search path. Create additional roles and grant fine-grained privileges with CREATE ROLE and GRANT from psql.
Because AGE stores graphs inside PostgreSQL, your graph and relational data share one database, one connection, one backup and one operational surface.
Support
SecureAnyCloud publishes and supports this image. Support covers:
- Deployment.
- PostgreSQL and Apache AGE configuration.
- openCypher query design and graph data modeling.
- Performance tuning.
- User and privilege management.
- EBS volume resizing and snapshot-based backups.
- Upgrade planning.
Contact SecureAnyCloud through the support channel listed on the AWS Marketplace listing.
All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.
Deploy on AWS
Launch Apache AGE on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com