Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions appsync-aurora-serverless-v2-rds-data-api-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# AWS AppSync to Amazon Aurora Serverless v2 via the RDS Data API (zero AWS Lambda)

This pattern deploys an AWS AppSync GraphQL API that reads and writes an Amazon Aurora Serverless v2 (PostgreSQL) database directly through the RDS Data API, with no AWS Lambda functions in the request path. AWS AppSync runs JavaScript (APPSYNC_JS) resolvers that issue parameterised SQL over the RDS Data API's HTTPS endpoint, so there is no VPC-attached compute layer to manage.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-aurora-serverless-v2-rds-data-api-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js 20+](https://nodejs.org/en/download/) installed
* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (AWS CDK) v2 installed
* An AWS account [bootstrapped for AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html)

## Deployment Instructions

1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
```
git clone https://github.com/aws-samples/serverless-patterns
```
2. Change directory to the pattern directory:
```
cd serverless-patterns/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk
```
3. Install dependencies:
```
npm install
```
4. Deploy the stack to your default AWS account and region:
```
npx cdk deploy
```
5. Note the outputs from the AWS CDK deployment process. They contain the GraphQL endpoint URL, the API key, the API ID, and the Amazon Aurora Serverless v2 cluster and secret ARNs used for testing.

## How it works

AWS AppSync exposes a GraphQL schema with a `listTodos` query and a `createTodo` mutation. Both fields are wired to an RDS Data API data source rather than to an AWS Lambda function. When a request arrives, AWS AppSync runs a JavaScript resolver that builds a parameterised SQL statement and sends it to the Amazon Aurora Serverless v2 cluster over the RDS Data API. The Data API authenticates using a generated credential in AWS Secrets Manager and returns the result set, which the resolver maps back into the GraphQL response shape.

Each service is load-bearing. AWS AppSync terminates the GraphQL request and runs the resolver. The RDS Data API turns that resolver into an HTTPS SQL call, which is what removes the need for an AWS Lambda function or a VPC-attached compute layer. Amazon Aurora Serverless v2 is the relational store that scales capacity down when idle and holds the data the resolvers query. Removing any one of the three breaks the architecture.

## Architecture

```
GraphQL client
|
v
AWS AppSync GraphQL API (API key auth, APPSYNC_JS resolvers)
|
| RDS Data API data source (HTTPS, no AWS Lambda, no VPC compute)
v
Amazon Aurora Serverless v2 (PostgreSQL, Data API enabled, encrypted at rest)
^
| admin credential
AWS Secrets Manager
```

## Testing

The database starts empty. Before the first query, create the `todos` table using the RDS Data API from your terminal (this uses the cluster and secret ARNs shown in the stack outputs). Replace `<ClusterArn>` and `<ClusterSecretArn>` with the deployment output values and set your region:

```
aws rds-data execute-statement \
--resource-arn "<ClusterArn>" \
--secret-arn "<ClusterSecretArn>" \
--database "appsyncdemo" \
--sql "CREATE TABLE IF NOT EXISTS todos (id SERIAL PRIMARY KEY, title TEXT NOT NULL, completed BOOLEAN NOT NULL DEFAULT false);" \
--region "<region>"
```

1. Create a todo through the GraphQL API. Replace `<GraphQLApiUrl>` and `<GraphQLApiKey>` with the stack output values:

```
curl -X POST "<GraphQLApiUrl>" \
-H "Content-Type: application/json" \
-H "x-api-key: <GraphQLApiKey>" \
-d '{"query":"mutation { createTodo(title: \"Try the RDS Data API\") { id title completed } }"}'
```

Expected response (the `id` will vary):

```json
{"data":{"createTodo":{"id":"1","title":"Try the RDS Data API","completed":false}}}
```

2. List todos through the GraphQL API to confirm the row round-tripped from Amazon Aurora Serverless v2:

```
curl -X POST "<GraphQLApiUrl>" \
-H "Content-Type: application/json" \
-H "x-api-key: <GraphQLApiKey>" \
-d '{"query":"query { listTodos { id title completed } }"}'
```

Expected response:

```json
{"data":{"listTodos":[{"id":"1","title":"Try the RDS Data API","completed":false}]}}
```

The round trip proves the headline behaviour: a GraphQL request served straight from a relational database through the RDS Data API, with no AWS Lambda function involved.

## Cleanup

1. Delete the stack from the `cdk` directory:
```
npx cdk destroy
```
2. Confirm the stack has been deleted:
```
aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'AppSyncAuroraServerlessStack')].StackStatus"
```

Warning: `npx cdk destroy` removes the Amazon Aurora Serverless v2 cluster and all data it contains. The cluster is configured with `RemovalPolicy.DESTROY` for this sample so cleanup leaves nothing behind and stops incurring charges. Do not use this removal policy for production data.

----
Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
12 changes: 12 additions & 0 deletions appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
cdk.context.json

# Build
*.tsbuildinfo
11 changes: 11 additions & 0 deletions appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import * as cdk from 'aws-cdk-lib';
import { AppSyncAuroraServerlessStack } from '../lib/appsync-aurora-serverless-v2-rds-data-api-stack';

const app = new cdk.App();
new AppSyncAuroraServerlessStack(app, 'AppSyncAuroraServerlessStack', {
description:
'AWS AppSync GraphQL API backed by Amazon Aurora Serverless v2 via the RDS Data API, with zero AWS Lambda functions (uses-appsync-aurora-serverless-v2-rds-data-api-cdk)',
});
22 changes: 22 additions & 0 deletions appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"node_modules"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import * as path from 'path';
import {
Stack,
StackProps,
RemovalPolicy,
CfnOutput,
aws_appsync as appsync,
aws_ec2 as ec2,
aws_rds as rds,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

/**
* AWS AppSync GraphQL API backed by an Amazon Aurora Serverless v2
* (PostgreSQL) cluster through the RDS Data API. There are no AWS Lambda
* functions in this pattern: AppSync talks to the database directly using an
* RDS Data API data source and JavaScript (APPSYNC_JS) resolvers.
*
* Each service is load-bearing:
* - AWS AppSync terminates the GraphQL request and runs the resolver.
* - The RDS Data API data source turns a resolver into an HTTPS SQL call,
* which is what removes the need for an AWS Lambda function or a VPC-attached
* compute layer.
* - Amazon Aurora Serverless v2 is the relational store that scales its
* capacity to zero-ish when idle and holds the data the resolvers query.
*/
export class AppSyncAuroraServerlessStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const databaseName = 'appsyncdemo';

// Minimal VPC for the Aurora cluster. The RDS Data API is reached over
// HTTPS from AppSync, so no NAT gateway or public subnet is required.
const vpc = new ec2.Vpc(this, 'AuroraVpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{
name: 'isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
cidrMask: 24,
},
],
});

// Amazon Aurora Serverless v2 PostgreSQL cluster with the RDS Data API
// enabled. A generated admin secret in AWS Secrets Manager is what the
// Data API uses to authenticate; storage is encrypted at rest.
const cluster = new rds.DatabaseCluster(this, 'AuroraServerlessV2Cluster', {
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_16_8,
}),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
writer: rds.ClusterInstance.serverlessV2('writer'),
serverlessV2MinCapacity: 0.5,
serverlessV2MaxCapacity: 2,
defaultDatabaseName: databaseName,
enableDataApi: true,
storageEncrypted: true,
credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'),
removalPolicy: RemovalPolicy.DESTROY,
});

// The generated admin secret backing the cluster. The Data API data
// source reads this to authenticate SQL calls.
const secret = cluster.secret!;

// AWS AppSync GraphQL API. API key auth keeps the sample self-contained;
// production workloads should use Amazon Cognito, OIDC, or IAM auth.
const api = new appsync.GraphqlApi(this, 'GraphqlApi', {
name: 'appsync-aurora-serverless-v2-rds-data-api',
definition: appsync.Definition.fromFile(
path.join(__dirname, '..', 'schema', 'schema.graphql'),
),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
description: 'Demo API key for appsync-aurora-serverless-v2-rds-data-api',
expires: undefined,
},
},
},
xrayEnabled: true,
});

// RDS Data API data source for Amazon Aurora Serverless v2. This is the
// v2 variant (addRdsDataSourceV2) that binds an IDatabaseCluster; the
// grant of rds-data:* and the Secrets Manager read on the cluster secret
// are scoped to those specific resource ARNs by the L2 construct.
const dataSource = api.addRdsDataSourceV2(
'AuroraDataSource',
cluster,
secret,
databaseName,
);

// JavaScript (APPSYNC_JS) resolvers issue parameterised SQL through the
// Data API. Parameters are bound, never string-concatenated, to avoid
// SQL injection.
dataSource.createResolver('ListTodosResolver', {
typeName: 'Query',
fieldName: 'listTodos',
runtime: appsync.FunctionRuntime.JS_1_0_0,
code: appsync.Code.fromInline(`
import { select, createPgStatement, toJsonObject } from '@aws-appsync/utils/rds';
import { util } from '@aws-appsync/utils';
export function request(ctx) {
return createPgStatement(
select({
table: 'todos',
columns: ['id', 'title', 'completed'],
orderBy: [{ column: 'id' }],
}),
);
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return toJsonObject(result)[0];
}
`),
});

dataSource.createResolver('CreateTodoResolver', {
typeName: 'Mutation',
fieldName: 'createTodo',
runtime: appsync.FunctionRuntime.JS_1_0_0,
code: appsync.Code.fromInline(`
import { insert, createPgStatement, toJsonObject } from '@aws-appsync/utils/rds';
import { util } from '@aws-appsync/utils';
export function request(ctx) {
const { title } = ctx.args;
return createPgStatement(
insert({
table: 'todos',
values: { title, completed: false },
returning: ['id', 'title', 'completed'],
}),
);
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
return util.appendError(error.message, error.type, result);
}
return toJsonObject(result)[0][0];
}
`),
});

new CfnOutput(this, 'GraphQLApiUrl', {
value: api.graphqlUrl,
description: 'AWS AppSync GraphQL endpoint URL',
});

new CfnOutput(this, 'GraphQLApiKey', {
value: api.apiKey ?? 'n/a',
description: 'API key for the demo GraphQL API',
});

new CfnOutput(this, 'GraphQLApiId', {
value: api.apiId,
description: 'AWS AppSync GraphQL API ID',
});

new CfnOutput(this, 'ClusterSecretArn', {
value: secret.secretArn,
description: 'Secrets Manager ARN for the Aurora cluster admin credentials',
});

new CfnOutput(this, 'ClusterArn', {
value: cluster.clusterArn,
description: 'Amazon Aurora Serverless v2 cluster ARN (used by the RDS Data API)',
});
}
}
25 changes: 25 additions & 0 deletions appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "appsync-aurora-serverless-v2-rds-data-api-cdk",
"version": "1.0.0",
"description": "AWS AppSync GraphQL API backed by Amazon Aurora Serverless v2 via the RDS Data API (zero AWS Lambda)",
"bin": {
"app": "bin/app.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"synth": "cdk synth",
"deploy": "cdk deploy",
"destroy": "cdk destroy"
},
"devDependencies": {
"@types/node": "^20.11.0",
"aws-cdk": "^2.1000.0",
"ts-node": "^10.9.2",
"typescript": "~5.4.5"
},
"dependencies": {
"aws-cdk-lib": "^2.185.0",
"constructs": "^10.3.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Todo {
id: ID!
title: String!
completed: Boolean!
}

type Query {
listTodos: [Todo!]!
}

type Mutation {
createTodo(title: String!): Todo!
}
Loading