DEV Community

loading...
Cover image for Deploy a GraphQL API with Prisma, AWS AppSync, Aurora Serverless & CDK
Prisma

Deploy a GraphQL API with Prisma, AWS AppSync, Aurora Serverless & CDK

ryands17 profile image Ryan Dsouza ・8 min read

Intro

Recently, Nader Dabit performed a livestream of using AWS AppSync with an Amazon Aurora PostgreSQL Serverless database.

I replicated the same setup using Prisma as the developer experience that you get while writing queries is quite amazing and also fits in perfectly with TypeScript. The entire stack is composed of the following components:

Here is a high level overview of the architecture that we will be deploying:
Architecture diagram of the stack deployed

Before we move forward...

Here's the entire repository for those who directly want to dive in! To follow along with this blog post, open the link to the repo below and implement points 1-4.

GitHub logo ryands17 / graphql-api-cdk-serverless-postgres

A basic example of AppSync + Lambda resolvers with AWS Aurora Serverless and Prisma

This repo also contains a frontend that uses Next.js and AWS Amplify, but this article is just about deploying the backend.

This tutorial is an explanation for all the resources created. You do not need to create any of these snippets as I have provided a working repo above that you can directly deploy. You can clone the repo, follow along, and also make changes as you see fit.

The following stack that we will be deploying is NOT FREE. This stack will incur costs so please do not leave this running and delete it after you're done. If you do not want to incur any charges then feel free just to follow along by cloning the repo and understanding how this works rather than deploying the entire stack.

To delete the stack, run the following command at the end of the tutorial.

yarn cdk destroy
Enter fullscreen mode Exit fullscreen mode

Prerequisites

This post assumes a little bit of familiarity with Prisma and the AWS CDK.

Also it's required to install the aws-cli and creating a default profile by running aws configure.

Once the repo has been cloned, create a cdk.context.json file in the root of this project and specify region and accountID which will contain the region and AWS account respectively where the stack will be deployed.as follows:

{
  "region": "us-east-1",
  "accountID": "123456789012"
}
Enter fullscreen mode Exit fullscreen mode

The accountID is a placeholder value so it needs to be replaced with your AWS Account ID.

Getting started

Let's start by creating the basic resources we require for deploying our application. All the snippets for creating resources are located in lib/appsync-cdk-rds-stack.ts.

Here we create an AppSync GraphQL API from a schema.graphql file. This contains our Query, Mutation and Subscription types, basically CRUD for posts.

// lib/appsync-cdk-rds-stack.ts

const api = new appsync.GraphqlApi(this, 'Api', {
  name: 'cdk-blog-appsync-api',
  schema: appsync.Schema.fromAsset('graphql/schema.graphql'),
  authorizationConfig: {
    defaultAuthorization: {
      authorizationType: appsync.AuthorizationType.API_KEY,
      apiKeyConfig: {
        expires: cdk.Expiration.after(cdk.Duration.days(365)),
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

And this is the referenced GraphQL file:

# graphql/schema.graphql

type Post {
  id: String!
  title: String!
  content: String!
}

input CreatePostInput {
  id: String
  title: String!
  content: String!
}

input UpdatePostInput {
  id: String!
  title: String
  content: String
}

type Query {
  listPosts: [Post]
  getPostById(postId: String!): Post
}

type Mutation {
  createPost(post: CreatePostInput!): Post
  deletePost(postId: String!): Post
  updatePost(post: UpdatePostInput!): Post
}

type Subscription {
  onCreatePost: Post @aws_subscribe(mutations: ["createPost"])
  onUpdatePost: Post @aws_subscribe(mutations: ["updatePost"])
  onDeletePost: Post @aws_subscribe(mutations: ["deletePost"])
}
Enter fullscreen mode Exit fullscreen mode

The following snippet creates a VPC, and a SecurityGroup and a SubnetGroup for our Aurora database cluster. This database will not be publicly accessible so only resources created inside the VPC (like Lambda functions) will be able to access the database.

// lib/appsync-cdk-rds-stack.ts

const vpc = new ec2.Vpc(this, 'BlogAppVPC', {
  cidr: '10.0.0.0/20',
  natGateways: 0,
  maxAzs: 2,
  enableDnsHostnames: true,
  enableDnsSupport: true,
  subnetConfiguration: [
    {
      cidrMask: 22,
      name: 'public',
      subnetType: ec2.SubnetType.PUBLIC,
    },
    {
      cidrMask: 22,
      name: 'private',
      subnetType: ec2.SubnetType.ISOLATED,
    },
  ],
})

const privateSg = new ec2.SecurityGroup(this, 'private-sg', {
  vpc,
  securityGroupName: 'private-sg',
})
privateSg.addIngressRule(
  privateSg,
  ec2.Port.allTraffic(),
  'allow internal SG access'
)

const subnetGroup = new rds.SubnetGroup(this, 'rds-subnet-group', {
  vpc,
  subnetGroupName: 'aurora-subnet-group',
  vpcSubnets: { subnetType: ec2.SubnetType.ISOLATED },
  removalPolicy: cdk.RemovalPolicy.DESTROY,
  description: 'An all private subnets group for the DB',
})
Enter fullscreen mode Exit fullscreen mode

Next, lets create the database.

This following is an Aurora Serverless PostgreSQL database cluster with Postgres 10. Here, we pass the database name as BlogDB, and we have specified the vpc, subnetGroup and securityGroup that we created above.

// lib/appsync-cdk-rds-stack.ts

const cluster = new rds.ServerlessCluster(this, 'AuroraBlogCluster', {
  engine: rds.DatabaseClusterEngine.AURORA_POSTGRESQL,
  // Set the engine to Postgres
  parameterGroup: rds.ParameterGroup.fromParameterGroupName(
    this,
    'ParameterGroup',
    'default.aurora-postgresql10'
  ),
  defaultDatabaseName: 'BlogDB',
  enableDataApi: true,
  vpc,
  subnetGroup,
  securityGroups: [privateSg],
  removalPolicy: cdk.RemovalPolicy.DESTROY,
})
Enter fullscreen mode Exit fullscreen mode

This database also comes with the credentials created in an AWS service named SecretsManger. We will be needing to fetch the database credentials from this service when we interact with the database from the AWS Console as well as from the Lambda function that we will be creating next.

Finally, let's create the Lambda function that will interact with this database that we created.

// lib/appsync-cdk-rds-stack.ts

const postFn = new lambda.Function(this, 'MyFunction', {
  vpc,
  vpcSubnets: { subnetType: ec2.SubnetType.ISOLATED },
  securityGroups: [privateSg],
  runtime: lambda.Runtime.NODEJS_12_X,
  code: new lambda.AssetCode('lambda-fns'),
  handler: 'index.handler',
  memorySize: 1024,
  timeout: cdk.Duration.seconds(10),
  environment: {
    SECRET_ID: cluster.secret?.secretArn || '',
    AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
  },
})

// Grant access to Secrets manager to fetch the secret
postFn.addToRolePolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ['secretsmanager:GetSecretValue'],
    resources: [cluster.secret?.secretArn || ''],
  })
)

// An endpoint to fetch the secret from Secrets Manager
new ec2.InterfaceVpcEndpoint(this, 'secrets-manager', {
  service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
  vpc,
  privateDnsEnabled: true,
  subnets: { subnetType: ec2.SubnetType.ISOLATED },
  securityGroups: [privateSg],
})
Enter fullscreen mode Exit fullscreen mode

Let's look at the three snippets of code that we created above.

The first snippet is a basic Node.js 12 Lambda function that takes a directory for code named lambda-fns which we will discuss further in this post.

The second snippet provides permissions via IAM for Secrets Manager to allow Lambda to read the credentials i.e. host, port, username, password from the secret.

The final snippet is an Interface Endpoint. By default, our Lambda function doesn't have access to the internet as it's deployed in the VPC we created, which is why we need this endpoint to fetch database credentials from Secrets Manager.

Lambda function setup with Prisma

The last thing left before deploying our app is setting up our Lambda function code with Prisma.

The setup that we have in the folder lambda-fns is a basic Prisma setup, but we need to add a couple of things to make it work.

Firstly, we need is to setup our schema.prisma in the following manner.

// prisma/schema.prisma

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

datasource db {
  provider = "postgresql"
  url      = env("DB_URL")
}

model Post {
  id      String  @id
  title   String?
  content String?

  @@map("posts")
}
Enter fullscreen mode Exit fullscreen mode

We created the schema for the table here manually and added the rhel-openssl-1.0.x target which is required to make Prisma run on Lambda.

The second thing is fetching the database credentials from Secrets Manager and providing it to Prisma.

// lambda-fns/db.ts

const sm = new SecretsManager()
let db: PrismaClient
export const getDB = async () => {
  if (db) return db

  const dbURL = await sm
    .getSecretValue({
      SecretId: process.env.SECRET_ID || '',
    })
    .promise()

  const secretString = JSON.parse(dbURL.SecretString || '{}')
  const url = `postgresql://${secretString.username}:${secretString.password}@${secretString.host}:${secretString.port}/${secretString.dbname}?connection_limit=1`

  db = new PrismaClient({ datasources: { db: { url } } })
  return db
}
Enter fullscreen mode Exit fullscreen mode

Here, we've created a function named getDB that fetches the DB credentials, constructs the URL and passes that to Prisma.

Another thing to note here is that the PrismaClient instance is declared outside the function to reuse the execution environment. There are two reasons for this.

The first being that we do not create multiple connections to the DB on each call to the API. Secondly, we limit the API calls made to Secrets Manager to fetch secrets.

Here Lambda will reuse the same variable db created outside the function for multiple invocations of our GraphQL API.

Finally lets deploy the application using the following command:

yarn cdk deploy
Enter fullscreen mode Exit fullscreen mode

This will deploy all the resources to the specified account and region specified in cdk.context.json and we will be able to see the stack in Cloudformation.

Cloudformation stack displayed in the AWS Console

One step that's remaining is to create the table in our database.

On opening RDS from the Services menu and navigating to the Query Editor, we should be greeted with a popup like this:

Dialog window to connect to our Aurora DB

We can get fetch secret ARN from the Secrets Manager service here:

Fetch the Secret ARN here

On entering the ARN, we will get a query window where we can run the SQL to create the table. This SQL can be obtained from the commands.sql file in the repo.

The posts table will be created when we click on the Run button and see the success message that pops up below.

Successfully create the posts table

Testing our app

Finally, we are all set to test our application! We can select AppSync from the Services menu and select our created API from the list. On selecting the API and navigating to Queries, we will see a Query Editor.

The Query Editor for our AppSync API

Let's create a post by firing a mutation.

mutation MyMutation {
  createPost(
    post: { title: "Title", content: "Some content" }
  ) {
    id
    title
    content
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating a post via a Mutation

And voila! It works!

To test if this is actually persisted in the DB, let's fire a query to fetch all the posts.

query MyQuery {
  listPosts {
    id
    title
    content
  }
}
Enter fullscreen mode Exit fullscreen mode

Querying for all the posts

And it does return the created post!

Conclusion

In this entire stack, we deployed a GraphQL API with AppSync and created Lambda resolvers for our Queries/Mutations in which we queried our Aurora Serverless Postgres database using Prisma.

Here's the link to the repo again for those who want to dive in.

GitHub logo ryands17 / graphql-api-cdk-serverless-postgres

A basic example of AppSync + Lambda resolvers with AWS Aurora Serverless and Prisma

Currently we need to create our tables and edit our schema.prisma manually which is not ideal. We will be adding another post on this soon where we will see how we can use a Bastion Host with SSH port forwarding to interact with our serverless database locally and create our tables using the Prisma CLI!

The reason we cannot use Prisma CLI to generate tables currently is because Aurora Serverless is not publicly accessible, which is why we needed to create a bridge (Bastion Host) with SSH port forwarding to access our database locally.

Thanks a lot for reading this post, and if you have implemented this stack, please DO NOT FORGET TO DELETE THIS STACK via

yarn cdk destroy
Enter fullscreen mode Exit fullscreen mode

Discussion (3)

pic
Editor guide
Collapse
codemochi profile image
Code Mochi • Edited

I love the post! One question that came to my mind after watching your deploy nextjs with serverless containers is that since Nextjs is so great at creating api functions, why not just create an api graphQL function with Nextjs and have that talk to the aurora using the prisma client?

Then you'd only deploy the Nextjs instance and you'd have a fullstack web app. Is there a benefit to using appsync in between the lambda and the frontend? performance or security perhaps?

Collapse
jimingeorge profile image
jimingeorge

Great post. Eagerly waiting on the next post on how to use Bastion Host with prisma.

Collapse
cmonteiro128 profile image
Chris Monteiro

Also eager!