Deploy a GraphQL API with Prisma, AWS AppSync, Aurora Serverless & CDK
Tags: prisma, serverless, aws, typescript
Published: 2020-12-21 10:18:16

## Intro

Recently, [Nader Dabit](https://twitter.com/dabit3) performed a livestream of using AWS AppSync with an Amazon Aurora PostgreSQL Serverless database.

{% youtube pXd9BCwpjhA %}

I replicated the same setup using [Prisma](https://www.prisma.io/) 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:

- [CDK](https://aws.amazon.com/cdk/)
- [Prisma](https://www.prisma.io/)
- [AWS AppSync](https://aws.amazon.com/appsync/)
- [Amazon Aurora PostgreSQL Serverless](https://aws.amazon.com/rds/aurora/serverless/)
- [TypeScript](https://www.typescriptlang.org/)

Here is a high level overview of the architecture that we will be deploying:
![Architecture diagram of the stack deployed](https://dev-to-uploads.s3.amazonaws.com/i/wkdeg906pzou9nhgo7nu.png)

## 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 ryands17/graphql-api-cdk-serverless-postgres no-readme %}

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.

```sh
yarn cdk destroy
```

## Prerequisites

This post assumes a little bit of familiarity with [Prisma](https://www.prisma.io/) and the [AWS CDK](https://aws.amazon.com/cdk/).

Also it's required to install the [aws-cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) 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:

```json
{
  "region": "us-east-1",
  "accountID": "123456789012"
}
```

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](https://github.com/ryands17/graphql-api-cdk-serverless-postgres/blob/main/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.

```ts
// 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)),
      },
    },
  },
})
```

And this is the referenced GraphQL file:

```graphql
# 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"])
}
```

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.

```ts
// 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',
})
```

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.

```ts
// 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,
})
```

This database also comes with the credentials created in an AWS service named [SecretsManger](https://aws.amazon.com/secrets-manager/). 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.

```ts
// 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],
})
```

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](https://docs.aws.amazon.com/vpc/latest/userguide/vpce-interface.html). 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.

```groovy
// 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")
}
```

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.

```ts
// 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
}
```

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:

```sh
yarn cdk deploy
```

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](https://dev-to-uploads.s3.amazonaws.com/i/umheddb9r9w0kizyqe9y.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/3tewwxzdts5m89scg0wp.png)

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

![Fetch the Secret ARN here](https://dev-to-uploads.s3.amazonaws.com/i/jorxseqov70no1lu4kom.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/3q2fs195x4a7rj8v7qcc.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/i/1qjsahpof2g7ne8d6d21.png)

Let's create a post by firing a mutation.

```graphql
mutation MyMutation {
  createPost(
    post: { title: "Title", content: "Some content" }
  ) {
    id
    title
    content
  }
}
```

![Creating a post via a Mutation](https://dev-to-uploads.s3.amazonaws.com/i/2w8chmjpeeimpjptn51i.png)

And voila! It works!

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

```graphql
query MyQuery {
  listPosts {
    id
    title
    content
  }
}
```

![Querying for all the posts](https://dev-to-uploads.s3.amazonaws.com/i/k5ro3wpquw68u9vu2c1c.png)

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 ryands17/graphql-api-cdk-serverless-postgres no-readme %}

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](https://en.wikipedia.org/wiki/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
```