Databases AWS

Apache AGE Graph Database on AWS User Guide

| Product: Apache AGE on AWS

Overview

This image runs Apache AGE, the open source extension that adds graph database functionality and the openCypher query language to PostgreSQL, so a single database engine serves both relational and graph workloads. 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, and a small demo graph is seeded during the build so MATCH queries return data straight away.

This is a headless database image. There is no web interface; you administer it over SSH with the psql client. PostgreSQL binds to loopback only on 127.0.0.1:5432 and is never exposed to the network, so the database engine stays private. Password authentication uses scram-sha-256, and a dedicated non-superuser application role named age owns the demo database.

On the first boot of your instance a one-shot service generates a fresh, strong password for the age role, unique to that instance, applies it to the database, proves that the new password authenticates while the build placeholder is refused, and only then writes 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.

PostgreSQL's data directory lives on a dedicated, independently resizable EBS data volume mounted at /var/lib/age-postgres, so 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 you need:

  • 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 allowing 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. PostgreSQL benefits from additional memory for larger graphs and heavier query workloads.

Step 1: 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 cloudimg 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, and either 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 PostgreSQL data volume is attached automatically from the image.

Select Launch instance. First boot initialisation 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 on first boot.

Step 2: Launch the Instance from the AWS CLI

The following block launches an instance from the cloudimg Apache AGE Marketplace AMI into an existing subnet and security group. Replace <ami-id> with the AMI ID shown on the Marketplace listing, <key-name> with your EC2 key pair name, <subnet-id> with your subnet ID, and <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 from the EC2 console or with aws ec2 describe-instances.

Step 3: Connect to Your Instance

Connect over SSH using your key pair and the login user for your operating system variant.

OS variant SSH login user
Ubuntu 24.04 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.txt

That file contains the AGE_PASSWORD value for this instance along with 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.

Step 4: Verify PostgreSQL and the AGE Extension

PostgreSQL runs as a system service that is enabled and started on boot. Confirm the service is active:

systemctl is-active postgresql@16-main.service

That prints active. Confirm the PostgreSQL server version:

sudo -u postgres /usr/lib/postgresql/16/bin/postgres --version

Confirm the listener is bound to loopback only, so the database is never exposed to the network:

ss -tln | grep 5432

The address is 127.0.0.1:5432, never 0.0.0.0. Now confirm the Apache AGE extension is installed in the demo database. This 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';"

That prints age|1.6.0.

PostgreSQL 16 reporting its version, the postgresql service active, the loopback-only listener on 127.0.0.1:5432, and the installed Apache AGE 1.6.0 extension

Step 5: 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 on its default search path for the demo database; passing it explicitly makes these one-off commands self-contained.

An authenticated psql session running openCypher MATCH queries against the seeded demo graph, returning the seeded people with their roles and traversing the relationships between them

Step 6: 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. Keep your real graphs, of course; this final step just 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.

Creating a new graph with create_graph, inserting a Product node through openCypher, reading it back, and dropping the example graph on the running Apache AGE database

Step 7: 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 that 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. This keeps all database traffic inside the encrypted SSH channel. Do not open port 5432 to the internet without TLS in front of it.

Step 8: The Data Volume

PostgreSQL's data directory lives on a dedicated EBS volume mounted at /var/lib/age-postgres. The cluster is initialised directly into this path, so every table, index and graph sits off the operating system disk. Confirm the mount:

df -h /var/lib/age-postgres

To 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 use logical backups with pg_dump against the demo database.

Step 9: Connect Applications

Point your applications at the database through the SSH tunnel from step 7, or 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

This image is published and supported by cloudimg. Support covers deployment, PostgreSQL and Apache AGE configuration, openCypher query design and graph data modelling, performance tuning, user and privilege management, EBS volume resizing and snapshot-based backups, and upgrade planning. Contact cloudimg 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.