diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/README.md b/appsync-aurora-serverless-v2-rds-data-api-cdk/README.md new file mode 100644 index 000000000..5e9723ec5 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/README.md @@ -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 `` and `` with the deployment output values and set your region: + +``` +aws rds-data execute-statement \ + --resource-arn "" \ + --secret-arn "" \ + --database "appsyncdemo" \ + --sql "CREATE TABLE IF NOT EXISTS todos (id SERIAL PRIMARY KEY, title TEXT NOT NULL, completed BOOLEAN NOT NULL DEFAULT false);" \ + --region "" +``` + +1. Create a todo through the GraphQL API. Replace `` and `` with the stack output values: + + ``` + curl -X POST "" \ + -H "Content-Type: application/json" \ + -H "x-api-key: " \ + -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 "" \ + -H "Content-Type: application/json" \ + -H "x-api-key: " \ + -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 diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/.gitignore b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/.gitignore new file mode 100644 index 000000000..ef13097d8 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/.gitignore @@ -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 diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/bin/app.ts b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/bin/app.ts new file mode 100644 index 000000000..9d84a93c7 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/bin/app.ts @@ -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)', +}); diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/cdk.json b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/cdk.json new file mode 100644 index 000000000..b7b7c3f0b --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/cdk.json @@ -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 + } +} diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/lib/appsync-aurora-serverless-v2-rds-data-api-stack.ts b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/lib/appsync-aurora-serverless-v2-rds-data-api-stack.ts new file mode 100644 index 000000000..913fda438 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/lib/appsync-aurora-serverless-v2-rds-data-api-stack.ts @@ -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)', + }); + } +} diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/package.json b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/package.json new file mode 100644 index 000000000..e7f56a8e1 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/package.json @@ -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" + } +} diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/schema/schema.graphql b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/schema/schema.graphql new file mode 100644 index 000000000..99c00581b --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/schema/schema.graphql @@ -0,0 +1,13 @@ +type Todo { + id: ID! + title: String! + completed: Boolean! +} + +type Query { + listTodos: [Todo!]! +} + +type Mutation { + createTodo(title: String!): Todo! +} diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/tsconfig.json b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/tsconfig.json new file mode 100644 index 000000000..0acc73070 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/cdk/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/appsync-aurora-serverless-v2-rds-data-api-cdk/example-pattern.json b/appsync-aurora-serverless-v2-rds-data-api-cdk/example-pattern.json new file mode 100644 index 000000000..f501f60c7 --- /dev/null +++ b/appsync-aurora-serverless-v2-rds-data-api-cdk/example-pattern.json @@ -0,0 +1,81 @@ +{ + "title": "AWS AppSync to Amazon Aurora Serverless v2 via the RDS Data API", + "description": "Serve a GraphQL API straight from Amazon Aurora Serverless v2 through the RDS Data API using AWS AppSync JavaScript resolvers, with zero AWS Lambda.", + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "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 HTTPS endpoint. The Data API authenticates with a generated AWS Secrets Manager credential, so there is no VPC-attached compute layer to manage.", + "Each service is load-bearing: AWS AppSync terminates the GraphQL request, the RDS Data API turns the resolver into an HTTPS SQL call that removes the need for AWS Lambda, and Amazon Aurora Serverless v2 stores the data and scales capacity down when idle." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/appsync-aurora-serverless-v2-rds-data-api-cdk", + "templateURL": "serverless-patterns/appsync-aurora-serverless-v2-rds-data-api-cdk", + "projectFolder": "appsync-aurora-serverless-v2-rds-data-api-cdk", + "templateFile": "cdk/lib/appsync-aurora-serverless-v2-rds-data-api-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Building an AWS AppSync API with an RDS Data API data source", + "link": "https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-rds-resolvers-js.html" + }, + { + "text": "Using the RDS Data API for Amazon Aurora Serverless v2", + "link": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html" + } + ] + }, + "deploy": { + "text": [ + "cd appsync-aurora-serverless-v2-rds-data-api-cdk/cdk", + "npm install", + "npx cdk deploy" + ] + }, + "testing": { + "text": [ + "See the README for a curl-based test that creates a todo and lists it back through the GraphQL API." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: npx cdk destroy.", + "Confirm removal: aws cloudformation list-stacks --query \"StackSummaries[?contains(StackName,'AppSyncAuroraServerlessStack')].StackStatus\"." + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "image": "", + "bio": "Technical Account Manager at AWS.", + "linkedin": "", + "twitter": "" + } + ], + "patternArch": { + "icon1": "fa:fa-code", + "icon2": "fa:fa-database", + "icon3": "fa:fa-server", + "label1": "GraphQL client", + "label2": "AWS AppSync", + "label3": "Amazon Aurora Serverless v2", + "line1": { + "startLabel": "GraphQL client", + "endLabel": "AWS AppSync", + "label": "GraphQL request" + }, + "line2": { + "startLabel": "AWS AppSync", + "endLabel": "Amazon Aurora Serverless v2", + "label": "RDS Data API (HTTPS SQL)" + } + }, + "patternType": "Building" +}