You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace data/world.json as the system of record for the relationship graph with an Amazon Neptune cluster, provisioned as code in the existing CDK app under infra/.
This issue covers infrastructure and the access path only. Rewriting the inference engine and the probe queries against Neptune is tracked separately.
Why Neptune instead of the JSON file
The in-memory triple store was written so the concepts map 1:1 to a graph database. It stops being viable at three points:
Inference is a full join over every triple. Rules like allied_by_faction and shared_past produce O(n^2) edges, so the whole graph has to be materialized in memory on every load.
There is no concurrent write path. Story writeback rewrites the entire file, so two generation rounds cannot run at once.
khop() and shortest_path() are hand-rolled BFS. Neptune does this natively and keeps working past a few hundred nodes.
Graph model decision
Neptune supports both a property graph (openCypher / Gremlin) and RDF (SPARQL). The two are stored separately and cannot be queried across.
Recommendation: property graph, queried with openCypher.
Edges in the current model carry properties (tension, cause, derived_by, hidden_role). In RDF a triple cannot hold properties without reification, which would roughly triple the statement count and make every query harder to read.
Triple(s, p, o, props) maps directly to (:Node)-[:P {props}]->(:Node).
The inference rules are shaped like SPARQL basic graph patterns, but each one translates mechanically to a Cypher MATCH plus MERGE, so the SPARQL resemblance is not a reason to pick RDF.
Open question: if OWL/RDFS reasoning is wanted later, RDF is the only option. Neptune has no built-in inference engine in either mode, so this is not a strong argument today.
Instance sizing decision
Provisioned instances, not serverless.
Neptune Serverless never pauses. The configured minimum capacity is billed continuously, so a cluster idling at 1.0 NCU costs more per month than a burstable provisioned instance. This workload is queried in bursts during demos and development with long idle stretches in between, which is the case where provisioned wins.
Writer: db.t4g.medium, single instance, no reader
Add a reader in a second AZ only when read load or availability requires it
Resizing is a modify-db-instance plus reboot, acceptable at this stage
Consequence to plan around: a burstable class earns CPU credits. The inference pass is a sustained join, so a full re-materialization of derived edges can exhaust credits and throttle. If that shows up, the fix is db.r6g.large, not serverless.
Verify the class is orderable in the target region before merging. Neptune has only two burstable classes and db.t4g.medium is missing in some regions:
Neptune has no public endpoint and lives only inside a VPC. This has one consequence that shapes the whole change: AppSync JS resolvers cannot reach it. Resolvers run inside the AppSync service, and AppSync HTTP data sources have no VPC attachment.
So the graph has to be fronted by a Lambda function placed in the VPC, exposed to AppSync as a Lambda data source. Today every data source in infra/lib/storyboard-stack.js is a JS resolver over DynamoDB or Bedrock, so this adds the first VPC and the first Lambda to the stack. That is the main cost and complexity increase in this issue, and it is unavoidable if the graph moves to Neptune.
Scope
Infrastructure as code, CDK in JavaScript, in the existing infra/ app:
VPC with two private isolated subnets across two AZs, plus one public subnet for the bastion. The Neptune subnet group needs two AZs even though only one instance is created.
Neptune cluster
Engine version 1.3.x or later
One writer instance, class db.t4g.medium
Instance class exposed as a CDK context value so it can be raised without editing the stack
Backup retention 1 day for the PoC, deletionProtection: false
Encryption at rest with a customer managed KMS key, TLS enforced in transit
IAM database authentication enabled, so no password is stored anywhere. Callers sign requests with SigV4 against neptune-db:* actions scoped to the cluster resource id.
Security group allowing port 8182 only from the Lambda security group and the bastion security group
Graph query Lambda in the private subnets, with an execution role granting neptune-db:ReadDataViaQuery and neptune-db:WriteDataViaQuery
Register the Lambda as an AppSync data source and add the corresponding field to infra/schema.graphql
S3 gateway VPC endpoint. The Neptune bulk loader pulls from S3 over the VPC, and without the endpoint the load fails since there is no NAT gateway.
IAM role attached to the cluster granting read on the load bucket
Parameter group with neptune_enable_audit_log = 1
CloudWatch alarms on CPUUtilization, CPUCreditBalance (burstable class), and MainRequestQueuePendingRequests
Outputs: writer endpoint, reader endpoint, port, cluster resource id, VPC id, Lambda security group id
Local development access:
Bastion host with SSM Session Manager, no SSH key and no inbound rule. Port forward 8182 with aws ssm start-session --document-name AWS-StartPortForwardingSessionToRemoteHost.
Document the tunnel command in the repo README
Cost control:
scripts/stop.sh and scripts/start.sh wrapping stop-db-cluster and start-db-cluster. Stopping halts instance billing while storage keeps accruing.
Note in the README that a stopped Neptune cluster restarts automatically after 7 days, so stopping is not a substitute for teardown
Avoid a NAT gateway. Use gateway and interface VPC endpoints for the services the Lambda needs, since a NAT gateway would cost more than the database instance.
Loading:
Converter from data/world.json to openCypher bulk load CSV (:ID, :LABEL for nodes; :START_ID, :END_ID, :TYPE for edges)
scripts/load.sh that uploads to S3 and calls the loader endpoint
Decide whether derived edges are loaded or recomputed. Proposal: load asserted edges only and recompute derived edges in Neptune, so derived_by provenance stays trustworthy.
Out of scope
Tracked as follow-up issues:
Replace WorldGraph with an openCypher client, keeping match(), khop(), shortest_path(), and apply_writeback() as the interface
Translate the JSON inference rules to Cypher MERGE statements, deciding per rule whether it materializes on write or expands at query time
Multi-world tenancy: a world property on every node, or one database per world
Acceptance criteria
cdk deploy produces a reachable cluster with no manual console steps
data/world.json loads with node and edge counts matching the source file (27 nodes and 52 asserted edges in the current PoC), verified by a count query through the SSM tunnel
The graph query Lambda returns the same result as the in-memory store for at least one khop() and one shortest_path() case
scripts/stop.sh drops instance billing to zero, confirmed in Cost Explorer
Sub-issue of #1.
Goal
Replace
data/world.jsonas the system of record for the relationship graph with an Amazon Neptune cluster, provisioned as code in the existing CDK app underinfra/.This issue covers infrastructure and the access path only. Rewriting the inference engine and the probe queries against Neptune is tracked separately.
Why Neptune instead of the JSON file
The in-memory triple store was written so the concepts map 1:1 to a graph database. It stops being viable at three points:
allied_by_factionandshared_pastproduce O(n^2) edges, so the whole graph has to be materialized in memory on every load.khop()andshortest_path()are hand-rolled BFS. Neptune does this natively and keeps working past a few hundred nodes.Graph model decision
Neptune supports both a property graph (openCypher / Gremlin) and RDF (SPARQL). The two are stored separately and cannot be queried across.
Recommendation: property graph, queried with openCypher.
tension,cause,derived_by,hidden_role). In RDF a triple cannot hold properties without reification, which would roughly triple the statement count and make every query harder to read.Triple(s, p, o, props)maps directly to(:Node)-[:P {props}]->(:Node).MATCHplusMERGE, so the SPARQL resemblance is not a reason to pick RDF.Open question: if OWL/RDFS reasoning is wanted later, RDF is the only option. Neptune has no built-in inference engine in either mode, so this is not a strong argument today.
Instance sizing decision
Provisioned instances, not serverless.
Neptune Serverless never pauses. The configured minimum capacity is billed continuously, so a cluster idling at 1.0 NCU costs more per month than a burstable provisioned instance. This workload is queried in bursts during demos and development with long idle stretches in between, which is the case where provisioned wins.
db.t4g.medium, single instance, no readermodify-db-instanceplus reboot, acceptable at this stageConsequence to plan around: a burstable class earns CPU credits. The inference pass is a sustained join, so a full re-materialization of derived edges can exhaust credits and throttle. If that shows up, the fix is
db.r6g.large, not serverless.Verify the class is orderable in the target region before merging. Neptune has only two burstable classes and
db.t4g.mediumis missing in some regions:Access path
Neptune has no public endpoint and lives only inside a VPC. This has one consequence that shapes the whole change: AppSync JS resolvers cannot reach it. Resolvers run inside the AppSync service, and AppSync HTTP data sources have no VPC attachment.
So the graph has to be fronted by a Lambda function placed in the VPC, exposed to AppSync as a Lambda data source. Today every data source in
infra/lib/storyboard-stack.jsis a JS resolver over DynamoDB or Bedrock, so this adds the first VPC and the first Lambda to the stack. That is the main cost and complexity increase in this issue, and it is unavoidable if the graph moves to Neptune.Scope
Infrastructure as code, CDK in JavaScript, in the existing
infra/app:db.t4g.mediumdeletionProtection: falseneptune-db:*actions scoped to the cluster resource id.neptune-db:ReadDataViaQueryandneptune-db:WriteDataViaQueryinfra/schema.graphqlneptune_enable_audit_log = 1CPUUtilization,CPUCreditBalance(burstable class), andMainRequestQueuePendingRequestsLocal development access:
aws ssm start-session --document-name AWS-StartPortForwardingSessionToRemoteHost.Cost control:
scripts/stop.shandscripts/start.shwrappingstop-db-clusterandstart-db-cluster. Stopping halts instance billing while storage keeps accruing.Loading:
data/world.jsonto openCypher bulk load CSV (:ID,:LABELfor nodes;:START_ID,:END_ID,:TYPEfor edges)scripts/load.shthat uploads to S3 and calls the loader endpointderived_byprovenance stays trustworthy.Out of scope
Tracked as follow-up issues:
WorldGraphwith an openCypher client, keepingmatch(),khop(),shortest_path(), andapply_writeback()as the interfaceMERGEstatements, deciding per rule whether it materializes on write or expands at query timeworldproperty on every node, or one database per worldAcceptance criteria
cdk deployproduces a reachable cluster with no manual console stepsdata/world.jsonloads with node and edge counts matching the source file (27 nodes and 52 asserted edges in the current PoC), verified by a count query through the SSM tunnelkhop()and oneshortest_path()casescripts/stop.shdrops instance billing to zero, confirmed in Cost Explorercdk destroyleaves no billable resources behind