Databases AWS

Apache AGE 1.6.0 on AWS User Guide

| Product: Apache AGE 1.6.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Apache AGE 1.6.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened PostgreSQL 18, PostGIS & Postgres Extension AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other PostgreSQL builds and extensions in the listing are described on the listing's guide hub.

Apache AGE (A Graph Extension) is an Apache-2.0 licensed PostgreSQL extension that adds a graph database and the openCypher query language to PostgreSQL. Your relational tables and your graphs live in one database, and you query graphs with cypher() from ordinary SQL.

The image installs PostgreSQL 16.15 from the official PostgreSQL (PGDG) apt repository and builds Apache AGE 1.6.0 from the checksum-verified Apache source release against the PostgreSQL 16 server headers. AGE 1.6.0 supports PostgreSQL 14 to 17; this image ships PostgreSQL 16. The PostgreSQL packages are not held, so apt upgrade keeps delivering PGDG minor (security) releases, and unattended security upgrades stay on for the operating system.

What is on the image:

  • PostgreSQL 16.15 (postgresql@16-main.service) with Apache AGE 1.6.0 preloaded through shared_preload_libraries
  • A demo database with the age extension created and a small example graph named demo (people, a company and their relationships), so Cypher queries return data straight away
  • A dedicated non-superuser role age that owns the demo database; its password is generated on your instance's first boot
  • TLS with a certificate generated on your instance's first boot (TLS 1.2 minimum)
  • PGDATA on a separate 30 GiB gp3 EBS volume mounted at /var/lib/age-postgres

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3 · Data volume: 30 GiB gp3

Security posture

  • No default password. The image ships with no password on any database role. First boot generates a unique password for the age role and writes it to /root/age-credentials.txt, readable by root only. The postgres superuser has no password and is reachable only as the local postgres system user (peer authentication).
  • Per-instance TLS certificate. First boot creates this instance's own private key and self-signed certificate and turns TLS on. No key ships in the image, so no two instances share one.
  • Localhost only. PostgreSQL listens on 127.0.0.1:5432, and the listing's security group opens SSH (22) only. Reach the database through an SSH tunnel, or open it to your VPC as described in Step 6.
  • Remote connections require TLS. pg_hba.conf accepts non-loopback clients only over hostssl with scram-sha-256; there is no plain-text remote rule and no trust rule.
  • No metadata access for the database. A database user can make PostgreSQL open outbound connections (for example COPY ... FROM PROGRAM as a superuser, or dblink), so every PostgreSQL unit is blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Amazon Time Sync (169.254.169.123) still works. If you add WAL archiving or backups to S3 from inside the database, give them explicit credentials; they cannot use the instance role.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • Fails closed. If first boot cannot finish (certificate or password), it stops PostgreSQL rather than leave it half configured, and tries again on the next boot.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances and create security groups
  2. An EC2 key pair in the Region you launch in
  3. A VPC subnet, with a public IP on the instance if you connect over the internet

Recommended instance type: m5.large. Size up for large graphs or heavy concurrent query load.

Connecting to your instance

Version Operating system SSH user
Apache AGE 1.6.0 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened PostgreSQL 18, PostGIS & Postgres Extension AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Apache AGE 1.6.0 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (22) only. Restrict it to your own IP address. Do not open 5432 to the internet.
  6. Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR with your own:

SG=$(aws ec2 create-security-group --group-name apache-age \
  --description "Apache AGE" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=apache-age}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required. The launch keeps the AMI's 30 GiB data volume; to start with a bigger one, add a --block-device-mappings entry for /dev/sdf with a larger VolumeSize.

Step 2: Connect and read your credentials

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It creates this instance's TLS key and certificate, turns TLS on, sets the age role's password and writes the credentials file. Show the connection details (the password line is left out here; read it with sudo cat /root/age-credentials.txt and keep it in your secrets manager):

sudo grep -v '^AGE_PASSWORD=' /root/age-credentials.txt
# Apache AGE - generated on first boot by apache-age-firstboot.service
# This password is unique to this instance. Store it somewhere safe.

AGE_USERNAME=age
AGE_DATABASE=demo
AGE_GRAPH=demo
AGE_HOST=127.0.0.1
AGE_PORT=5432
AGE_PUBLIC_IP=<public-ip>

# Local connection (on the instance):
#   PGPASSWORD='<AGE_PASSWORD>' psql "host=127.0.0.1 user=age dbname=demo sslmode=require"
...

The commands below read the password from that file at run time, so it never appears on screen. In your own sessions you can instead put it in ~/.pgpass (mode 0600) as 127.0.0.1:5432:demo:age:<AGE_PASSWORD>.

Step 3: Confirm PostgreSQL and Apache AGE are running

psql --version
systemctl is-active postgresql@16-main.service
sudo ss -Hltn
psql (PostgreSQL) 16.15 (Ubuntu 16.15-1.pgdg24.04+2)
active
LISTEN 0      4096      127.0.0.54:53   0.0.0.0:*
LISTEN 0      200        127.0.0.1:5432 0.0.0.0:*
LISTEN 0      4096         0.0.0.0:22   0.0.0.0:*
LISTEN 0      4096   127.0.0.53%lo:53   0.0.0.0:*
LISTEN 0      4096            [::]:22      [::]:*

PostgreSQL listens on 127.0.0.1:5432 only; port 53 on 127.0.0.53/127.0.0.54 is the operating system's local DNS stub. Now connect as the age role over TLS and list the extension:

PGPASSWORD="$(sudo sed -n 's/^AGE_PASSWORD=//p' /root/age-credentials.txt)" \
  psql "host=127.0.0.1 user=age dbname=demo sslmode=require" \
  -c "\dx age" \
  -c "SELECT ssl, version AS tls_version FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
             List of installed extensions
 Name | Version |   Schema   |      Description
------+---------+------------+------------------------
 age  | 1.6.0   | ag_catalog | AGE database extension
(1 row)

 ssl | tls_version
-----+-------------
 t   | TLSv1.3
(1 row)

AGE is preloaded and the age role's search_path starts with ag_catalog, so cypher() works with no LOAD 'age' (which only a superuser may run).

Step 4: Query the example graph with openCypher

The image ships a graph named demo. List the graphs, then find everyone who works at a company, and every relationship in the graph:

export PGPASSWORD="$(sudo sed -n 's/^AGE_PASSWORD=//p' /root/age-credentials.txt)"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT name FROM ag_catalog.ag_graph;"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT * FROM cypher('demo', \$\$ MATCH (p:Person)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name \$\$) AS (person agtype, company agtype);"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT * FROM cypher('demo', \$\$ MATCH (a)-[r]->(b) RETURN a.name, type(r), b.name \$\$) AS (src agtype, rel agtype, dst agtype);"
 name
------
 demo
(1 row)

      person       |  company
-------------------+------------
 "Ada Lovelace"    | "cloudimg"
 "Charles Babbage" | "cloudimg"
(2 rows)

        src        |    rel     |        dst
-------------------+------------+-------------------
 "Ada Lovelace"    | "KNOWS"    | "Charles Babbage"
 "Ada Lovelace"    | "WORKS_AT" | "cloudimg"
 "Charles Babbage" | "WORKS_AT" | "cloudimg"
(3 rows)

Step 5: Add to the graph and create your own

Add a node and a relationship with Cypher CREATE, read them back, then remove them again (DETACH DELETE drops a node with its relationships):

export PGPASSWORD="$(sudo sed -n 's/^AGE_PASSWORD=//p' /root/age-credentials.txt)"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT * FROM cypher('demo', \$\$ MATCH (a:Person {name: 'Ada Lovelace'}) CREATE (g:Person {name: 'Grace Hopper', role: 'Computer Scientist'})-[:KNOWS]->(a) RETURN g.name \$\$) AS (name agtype);"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT * FROM cypher('demo', \$\$ MATCH (g {name: 'Grace Hopper'})-[:KNOWS]->(x) RETURN g.name, x.name \$\$) AS (who agtype, knows agtype);"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT * FROM cypher('demo', \$\$ MATCH (g {name: 'Grace Hopper'}) DETACH DELETE g \$\$) AS (v agtype);"
psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -tAc "SELECT count(*) FROM cypher('demo', \$\$ MATCH (n) RETURN n \$\$) AS (n agtype);"
      name
----------------
 "Grace Hopper"
(1 row)

      who       |     knows
----------------+----------------
 "Grace Hopper" | "Ada Lovelace"
(1 row)

 v
---
(0 rows)

3

To start a graph of your own, run SELECT ag_catalog.create_graph('mygraph'); as the age role (it owns the demo database), then query it with cypher('mygraph', $$ ... $$).

Step 6: Connect from your application

PostgreSQL accepts connections on localhost only. Two ways to reach it:

  • SSH tunnel (no change to the instance). On your workstation run ssh -N -L 5432:127.0.0.1:5432 ubuntu@<public-ip>, then connect your client to 127.0.0.1:5432 as age with sslmode=require.
  • Open it to your VPC. Set the listener and restart, then add an inbound rule for 5432 to the instance's security group from your application subnet or VPC CIDR only (never 0.0.0.0/0). Remote clients must use TLS (sslmode=require, or verify-full with this instance's certificate /etc/postgresql/16/main/cloudimg-server.crt) and the age password:
sudo -u postgres psql -c "ALTER SYSTEM SET listen_addresses = '*';"
sudo systemctl restart postgresql@16-main.service

A plain-text (non-TLS) connection from another host is refused by pg_hba.conf, and a wrong password is refused by scram-sha-256:

OUT="$(PGPASSWORD=wrong-password psql "host=127.0.0.1 user=age dbname=demo sslmode=require" -c "SELECT 1" 2>&1 || true)"
echo "${OUT#*failed: }"
FATAL:  password authentication failed for user "age"

Step 7: Check the security controls

The PostgreSQL units carry the metadata block, the age role is not a superuser, and the data directory is on the data volume:

systemctl show -p IPAddressDeny postgresql@16-main.service
sudo -u postgres psql -tAc "SELECT rolname, rolsuper FROM pg_roles WHERE rolname IN ('age', 'postgres') ORDER BY 1;"
sudo -u postgres psql -tAc "SHOW data_directory;"
df -h /var/lib/age-postgres | tail -1
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32
age|f
postgres|t
/var/lib/age-postgres/16/main
/dev/nvme1n1     30G   47M   28G   1% /var/lib/age-postgres

Maintenance

  • Change the age password. Run sudo -u postgres psql and type \password age at the prompt (it asks for the new password twice and never puts it on a command line), then update /root/age-credentials.txt and your secrets manager.
  • Configuration. cloudimg's settings are in /etc/postgresql/16/main/conf.d/10-cloudimg-age.conf (listener, preload) and 20-cloudimg-tls.conf (this instance's certificate). Apply changes with sudo systemctl restart postgresql@16-main.service.
  • Storage. PGDATA is /var/lib/age-postgres/16/main on its own gp3 volume. Grow it by modifying the EBS volume, then sudo resize2fs on the device shown by df -h /var/lib/age-postgres. Take EBS snapshots or pg_dump backups regularly.
  • Updates. sudo apt update && sudo apt upgrade installs PostgreSQL 16 minor releases from PGDG and Ubuntu security updates. Apache AGE is built from source for PostgreSQL 16; rebuild it from a newer Apache release only when you move to a newer AGE version.
  • Logs. /var/log/postgresql/postgresql-16-main.log and journalctl -u postgresql@16-main.service; first boot logs to journalctl -u apache-age-firstboot.service.

Support

cloudimg supports this image by email at support@cloudimg.co.uk. Include the instance ID, the version (Apache AGE 1.6.0 on Ubuntu 24.04) and what you were doing.

Apache AGE, Apache and the Apache feather logo are trademarks of The Apache Software Foundation. PostgreSQL is a trademark of the PostgreSQL Community Association of Canada. Ubuntu is a trademark of Canonical Ltd. cloudimg is not affiliated with or endorsed by these organisations.