Create an AppSync API using Terraform
Tags: aws, terraform, graphql, awslambda
Published: 2024-03-10 15:16:16

---
title: Create an AppSync API using Terraform
published: true
date: 2024-03-10 15:11:21 UTC
tags: aws,terraform,graphql,awslambda
canonical_url: https://valdes.com.br/create-an-appsync-api-using-terraform-110af6ec9e17
---

![](https://cdn-images-1.medium.com/max/1024/1*DKGLCp8aqxrzTOxfJ5RqVQ.jpeg)

In this article we are going to dive into the details of creating a graphql api using AWS AppSync, the infrastructure is written using terraform.

The graphql api needs to provide two functionalities, a query to get tasks and a mutation to add tasks, the api needs to be authenticated and the data stored.

In the AppSync context we need to create two resolvers, one for getTasks and one for addTasks

### Requirements

- git
- NodeJS 14 or later, my version is v18.18.0
- An AWS account and configured credentials
- An S3 bucket for the terraform backend
- Install [terraform cli](https://developer.hashicorp.com/terraform/install)

### TL;DR;

Clone [the repo](https://github.com/kavichu/aws-appsync-terraform) and follow the instructions to deploy the project, you can use the gitpod configuration which comes with nodejs, aws cli v2, docker and terraform installed

![](https://cdn-images-1.medium.com/max/160/0*FOWmVZ0BuJtCyT0t)

### Architecture

Here is an overview of the architecture.

![](https://cdn-images-1.medium.com/max/700/0*3EWNqNfqHsD7S-Eq.png)

Now let’s jump into the details of the different services we defined in our architecture.

**AWS Identity and Access Management (IAM)** is a web service that helps you securely control access to AWS resources. With IAM, you can centrally manage permissions that control which AWS resources users can access. You use IAM to control who is authenticated (signed in) and authorized (has permissions) to use resources.

**Amazon Cognito** delivers frictionless customer identity and access management (CIAM) with a cost-effective and customizable platform that allows you to implement secure, frictionless customer identity and access management that scales.

**Amazon DynamoDB** is a serverless, NoSQL, fully managed database service with single-digit millisecond response times at any scale, enabling you to develop and run modern applications while only paying for what you use.

**AWS Lambda** runs code without provisioning or managing servers, creating workload-aware cluster scaling logic, maintaining event integrations, or managing runtimes.

**AWS AppSync** simplifies application development with GraphQL APIs by providing a single endpoint to securely query or update data from multiple databases, microservices, and APIs.

#### Creating the tasks table

To store the data generated by the users of the api, we are going to use DynamoDB, here we are creating the tasks table with a key named id and an attribute named owner, we are also creating a global secondary index to be able to retrieve records by owner

```hcl
resource "aws_dynamodb_table" "tasks_table" {
  name = "TasksTable"
  billing_mode = "PAY_PER_REQUEST"
  hash_key = "id"

  attribute {
    name = "id"
    type = "S"
  }

  attribute {
    name = "owner"
    type = "S"
  }

  global_secondary_index {
    name = "byOwner"
    hash_key = "owner"
    range_key = "id"
    projection_type = "ALL"
  }

}
```

#### Create a Cognito User Pool and User Pool Client

Since we are focusing on learning concepts, our cognito user pool configuration is not for production, for a production deployment you will need to modify the password policy

```hcl
resource "aws_cognito_user_pool" "user_pool" {
  name = "UserPool"
  username_attributes = ["email"]
  auto_verified_attributes = ["email"]
  password_policy {
    minimum_length = 8
    require_lowercase = false
    require_uppercase = false
    require_numbers = false
    require_symbols = false
  }
  admin_create_user_config {
    allow_admin_create_user_only = false # enable self sign in
  }
}

resource "aws_cognito_user_pool_client" "user_pool_client" {
  name = "UserPoolClient"
  user_pool_id = aws_cognito_user_pool.user_pool.id
}
```

#### Creating the AppSync API

To create the graphql api resource we need a schema file, define a visibility, in our scenario is GLOBAL, then we need to set our authentication configuration, we are using cognito user pools and we provide the configuration for the user pool

```hcl
resource "aws_appsync_graphql_api" "graphql_api" {
  name = "terraform-todos-api"

  schema = file("schema.graphql")

  visibility = "GLOBAL"

  authentication_type = "AMAZON_COGNITO_USER_POOLS"

  user_pool_config {
    default_action = "ALLOW"
    user_pool_id = aws_cognito_user_pool.user_pool.id
    app_id_client_regex = aws_cognito_user_pool_client.user_pool_client.id
  }

}
```

I the file schema.graphql, we define the type Task, the query getTasks and the mutation addTask, TaskConnection is a type we are using to iterate the result from getTasks

```graphql
type Task {
  id: ID!
  text: String!
  owner: ID!
  createdAt: String!
}

type TaskConnection {
 items: [Task!]
 nextToken: String
}

input TaskInput {
  text: String!
}

type Query {
  getTasks(limit: Int, nextToken: String): TaskConnection!
}

type Mutation {
  addTask(input: TaskInput!): Task!
}
```

#### Create a policy that adds write access to Tasks table

The lambda function running the code for addTask needs the dynamodb::PutItem permission on the Tasks table, to be able to do that we are creating a policy with the permissions we need

```hcl
data "aws_iam_policy_document" "lambda_put_item_policy" {
  statement {
    actions = ["dynamodb:PutItem"]
    resources = [
      aws_dynamodb_table.tasks_table.arn
    ]
  }
}
```

#### Create an IAM role for the lambda function

The IAM role for the lambda function needs some permissions for execution, the assume role policy allows lambda to assume this role, the IAM role also have a managed policy AWSLambdaBasicExecutionRole that provides the lambda function access to create logs. To finish the configuration of the role we add an inline policy, we load the policy document and provide a name for it

```hcl
resource "aws_iam_role" "lambda_execution_role" {
  name = "lambdaExecutionRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      },
    ]
  })
  managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]

  inline_policy {
    name = "lambda_put_item_inline"
    policy = data.aws_iam_policy_document.lambda_put_item_policy.json
  }
}
```

#### Create the addTask lambda function

We are providing a name and a filename, the filename is used to load a packaged version of our function as a zip file.

We are using the IAM role described previously, our function will be able create a record on the Tasks table, we define the handler and the runtime of our function.

To finish our lambda function we add an environment variable with the name of the Tasks table

```hcl
resource "aws_lambda_function" "add_task_lambda_function" {
  function_name = "addTaskLambdaFunction"
  filename = "add_task_lambda_function.zip"
  role = aws_iam_role.lambda_execution_role.arn
  handler = "index.addTask"
  runtime = "nodejs20.x"
  timeout = 30
  environment {
    variables = {
      TASKS_TABLE = aws_dynamodb_table.tasks_table.name
    }
  }
}
```

The source code of the lambda function for addTask uses DynamoDB to store a new item, the id of the item is generated using the ulid library, ULID stands for **U** niversally unique **L** exicographically sortable **ID** entifiers, the values are sortable even when generated within a millisecond, this is an example

```javascript
ulid() // 01HR8FDWSNDC63YF1HBHCB1QBF
```

The field id is an ulid generated value, and it is sortable.

The value of field owner will have the username field of the cognito identity parameter, and our last parameter is the text that comes from the arguments.

The typescript implementation needs the type **InputArguments** with the structure needed to be used by **AppSyncResolverEvent** , by configuring this the application is able to call **event.arguments.input.text** to obtain the text input of an invocation

```typescript
import { AppSyncResolverEvent , AppSyncIdentityCognito} from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { ulid } from 'ulid'

const client = new DynamoDBClient({});
const documentClient = DynamoDBDocumentClient.from(client);

type InputArguments = {
  input: {
    text: String
  }
}

export const addTask = async (event: AppSyncResolverEvent<InputArguments> ) => {
  
  const id = ulid()
  const createdAt = new Date().toJSON()
  const identity = event.identity as AppSyncIdentityCognito

  const data = {
    id,
    owner: identity.username,
    text: event.arguments.input.text,
    createdAt
  }
  const command = new PutCommand({
    TableName: process.env.TASKS_TABLE,
    Item: data
  });

  await documentClient.send(command)

  return data
}
```

#### Configuring the IAM role for the AppSync data sources

The data source needs to have Query permissions on the global secondary index called byOwner

```hcl
data "aws_iam_policy_document" "appsync_query_index_inline_policy" {
  statement {
    actions = ["dynamodb:Query"]
    resources = [
      "${aws_dynamodb_table.tasks_table.arn}/index/byOwner"
    ]
  }
}
```

For the sake of keeping this deployment simple, we are going to use the same IAM role to call our add task lambda function, in order to do it we need to add InvokeFunction permission

```hcl
data "aws_iam_policy_document" "appsync_invoke_lambda_inline_policy" {
  statement {
    actions = ["lambda:InvokeFunction"]
    resources = [
      aws_lambda_function.add_task_lambda_function.arn
    ]
  }
}
```

Now we have two policies, one to query the index byOwner and another to invoke the lambda function for addTask, we are going to use these policies in our role for appsync data sources

#### Creating the IAM role for the appsync data sources

The role needs to be assumed by the appsync service and as we explained previously, we need our role to be able to query an index, and invoke a lambda function, we add two inline policies, use the query index and invoke lambda policies we created before

```hcl
resource "aws_iam_role" "appsync_datasource_role" {
  name = "appsync_datasource_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "appsync.amazonaws.com"
        }
      },
    ]
  })
  inline_policy {
    name = "appsync_query_index_inline"
    policy = data.aws_iam_policy_document.appsync_query_index_inline_policy.json
  }
  inline_policy {
    name = "appsync_invoke_lambda_inline"
    policy = data.aws_iam_policy_document.appsync_invoke_lambda_inline_policy.json
  }
}
```

#### Creating the tasks table data source

We create the tasks table datasource using our graphql api, in this scenario it is of type AMAZON\_DYNAMODB, so we are going to use a table as data source, we provide the dynamodb\_config configuration with the name of the table from our previously created Tasks table and the region

```hcl
resource "aws_appsync_datasource" "tasks_table_datasource" {
  api_id = aws_appsync_graphql_api.graphql_api.id
  name = "TasksTableDataSource"
  type = "AMAZON_DYNAMODB"
  service_role_arn = aws_iam_role.appsync_datasource_role.arn
  dynamodb_config {
    table_name = aws_dynamodb_table.tasks_table.name
    region = var.region
  }
}
```

#### Creating the getTasks resolver using JavaScript

After creating our tasks data source, we are now ready to create our JavaScript resolver, we do this by creating an appsync resolver, using our graphql api, with type of Query and field equals to getTasks.

The runtime of the resolver is APPSYNC\_JS, we provide a path to the code and a data source

```hcl
resource "aws_appsync_resolver" "get_tasks_resolver" {
  api_id = aws_appsync_graphql_api.graphql_api.id
  type = "Query"
  field = "getTasks"
  runtime {
    name = "APPSYNC_JS"
    runtime_version = "1.0.0"
  }
  code = file("resolvers/getTasks.js")
  data_source = aws_appsync_datasource.tasks_table_datasource.name
}
```

The resolver getTask.js exports two functions, the request function returns an operation Query on the index byOwner, ordered in ascending order by the value of the id field, the value for the owner variable is obtained from the current authenticated user making the request, the value is located in the context variable of the function

```hcl
import { util } from '@aws-appsync/utils';

export function request(context) {
  return {
      operation: "Query",
      query: {
          expression: "#owner = :userId",
          expressionNames: {
            "#owner": "owner"
          },
          expressionValues: {
              ":userId": util.dynamodb.toDynamoDB(context.identity.username)
          }
      },
      index: "byOwner",
      nextToken: context.arguments.nextToken,
      limit: context.arguments.limit,
      scanIndexForward: true, // true order ASC, false order DESC
      consistentRead: false,
      select: "ALL_ATTRIBUTES"
  }
}

export function response(context) {
  return {
    items: context.result.items,
    nextToken: context.result.nextToken
  }
}
```

The response function returns a structure of items and pagination.

#### Creating the data source for the addTask lambda function

We are close to the end of our application, we need to add the add task data source, which in this scenario is of type AWS\_LAMBDA.

The service role for the data source is the appsync\_datasource\_role, which allows AppSync to invoke the lambda function add task

```hcl
resource "aws_appsync_datasource" "add_task_datasource" {
  api_id = aws_appsync_graphql_api.graphql_api.id
  name = "AddTaskDataSource"
  type = "AWS_LAMBDA"
  service_role_arn = aws_iam_role.appsync_datasource_role.arn
  lambda_config {
    function_arn = aws_lambda_function.add_task_lambda_function.arn
  }
}
```

The last part of the puzzle is to add the resolver for our mutation addTask

```hcl
resource "aws_appsync_resolver" "add_task_resolver" {
  api_id = aws_appsync_graphql_api.graphql_api.id
  type = "Mutation"
  field = "addTask"
  data_source = aws_appsync_datasource.add_task_datasource.name
}
```

#### Deploy the project

We need the name of the previously created bucket, to use it as parameter for the terraform init command

```
terraform init -backend-config="bucket=<YOUR_BACKEND_S3_BUCKET_NAME>"
```

Then we need to run to see the details of the resources to te created

```
terraform plan
```

To make changes and create the resources we run

```
terraform apply
```

### How to test the project

Open your AWS console, the first thing we need to is to create an user in our user pool, to do that we are going to the search bar and search for cognito

![](https://cdn-images-1.medium.com/max/700/0*rm8cByq-0wZ5LXan.jpeg)

Click on the User Pool we created using CDK

![](https://cdn-images-1.medium.com/max/700/0*McrtsgNLX8hMZqG5.jpeg)

Once in the details page of the user pool, click on create user, fill the user email and mark as verified, last fill a password for the user, you will need this credentials later on.

![](https://cdn-images-1.medium.com/max/700/0*T2NqS7xGWWB0X9Ai.jpeg)

Now open the search bar and search for appsync

![](https://cdn-images-1.medium.com/max/700/0*SVSKboH8Qht0g_J4.jpeg)

On the list, select the todos-api and once on the details page for the api, on the left bar, go to the menu Queries and click in Login with User Pools

![](https://cdn-images-1.medium.com/max/700/0*kEbNvFl5qquobP75.jpeg)

Select the client ID that is available and login with the user created in the previous step.

![](https://cdn-images-1.medium.com/max/700/0*-knD80P-6lz7LNB5.jpeg)

We want to create a task, in the explorer select query and change for mutation then click on the + plus button, choose the addTask and click on the input text, in the example text is Hello World then click on Run MyMutation

![](https://cdn-images-1.medium.com/max/700/0*-9PboTJzPIhKaLYW.jpeg)

When you run the mutation you will have a result like this one

![](https://cdn-images-1.medium.com/max/700/0*ZCAgEOxIHQBSQrx8.jpeg)

Now go to the explorer and change the mutation to query and click on + plus, click on getTasks and click on all fields, then run MyQuery

![](https://cdn-images-1.medium.com/max/700/0*SrLfxMRVq3uMMAE2.jpeg)