Front-End Web & Mobile

Invoke AWS services directly from AWS AppSync

* Written by Josh Kahn, Senior AWS Solutions Architect based in Chicago, IL.

September 14, 2021: Amazon Elasticsearch Service has been renamed to Amazon OpenSearch Service. See details.

AWS AppSync is a managed GraphQL service that enables developers to easily build data-driven mobile and web applications. Using a serverless backend, you can build a GraphQL API by connecting AWS AppSync to various data sources—including Amazon DynamoDB, AWS Lambda, and Amazon OpenSearch Service (successor to Amazon Elasticsearch Service). AWS AppSync added support for HTTP data sources in May 2018, which makes it easy to add legacy APIs to your GraphQL endpoints.

AWS AppSync has now been extended to support calling AWS services via HTTP data sources. In order for AWS to identify and authorize HTTP requests, they must be signed with the Signature Version 4 process. Otherwise, those requests are rejected. AWS AppSync can now calculate the signature on your behalf, based on the IAM role that’s provided as part of the HTTP data source configuration.

This means that you can call a broad array of AWS services without the need to write an intermediary Lambda function. For example, you could start execution of an AWS Step Functions state machine, retrieve a secret from AWS Secrets Manager, or list available GraphQL APIs from AWS AppSync itself from an AWS AppSync resolver.

Implementing a long-running query on AWS AppSync

To demonstrate the utility of this new feature, let’s build a GraphQL API with a long-running query. AWS AppSync limits GraphQL query execution to a maximum of 30 seconds. However, we might have a query (for example, a search) that sometimes takes up to one minute. We’re using AWS Step Functions to coordinate the long-running query across multiple steps, but you could opt to implement in other ways as well. We use AWS AppSync subscriptions to asynchronously update the client when the query is finished.

Create an HTTP data source

To start execution of the search state machine from AWS AppSync, we start by defining a new HTTP data source. We need to provide two additional components to invoke an AWS service:

  • An IAM role that can be assumed by AWS AppSync with permission to call states:StartExecution for our Step Functions state machine
  • The signing configuration

AWS AppSync currently provides support to add these components by using the AWS CLI, SDKs, and AWS CloudFormation.

First, we create a new IAM role with the following policy (be sure to note the role Amazon Resource Name (ARN)):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "states:StartExecution"
            ],
            "Resource": [
                "arn:aws:states:<REGION>:<ACCOUNT_ID>:stateMachine:aws-appsync-long-query"
            ],
            "Effect": "Allow"
        }
    ]
}

And trust relationship:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "appsync.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Next, we create the HTTP data source for AWS AppSync. To do this, create the following configuration file named http.json.

{
    "endpoint": "https://states.<REGION>.amazonaws.com/",
    "authorizationConfig": {
        "authorizationType": "AWS_IAM",
        "awsIamConfig": {
            "signingRegion": "<REGION>",
            "signingServiceName": "states" 
        }
    }
}

Then, use the AWS CLI to create the data source:

$ aws appsync create-data-source --api-id <API_ID> \
                                 --name StepFunctionHttpDataSource \
                                 --type HTTP \
                                 --http-config file:///http.json \
                                 --service-role-arn <ROLE_ARN>

Alternatively, you can create the same data source by using AWS CloudFormation:

StepFunctionsHttpDataSource:
    Type: AWS::AppSync::DataSource
    Properties:
      ApiId: !GetAtt SearchApi.ApiId
      Name: StepFunctionsHttpDataSource
      Description: Step Functions HTTP
      Type: HTTP
      # IAM role defined elsewhere in AWS CloudFormation template
      ServiceRoleArn: !GetAtt AppSyncServiceRole.Arn
      HttpConfig:
        Endpoint: !Sub https://states.${AWS::Region}.amazonaws.com/
        AuthorizationConfig:
          AuthorizationType: AWS_IAM
          AwsIamConfig:
            SigningRegion: !Ref AWS::Region
            SigningServiceName: states

With our HTTP data source defined and configured to sign requests to Step Functions, we can build our GraphQL API.

Building a GraphQL API

To enable our long running query, we submit the search request and immediately return the status of the search (PENDING), as well as a unique identifier for the query. Our client then needs to subscribe to updates for that particular query. The GraphQL schema is as follows:

type Result {
  id: ID!
  status: ResultStatus!
  listings: [String]
}

enum ResultStatus {
  PENDING
  COMPLETE
  ERROR
}

input ResultInput {
  id: ID!
  status: ResultStatus!
  listings: [String]!
}

type Query {
  # called by client to initiate long running search
  search(text: String!): Result
}

type Mutation {
  # called by backend when search is complete
  publishResult(result: ResultInput): Result
}

type Subscription {
  onSearchResult(id: ID!): [Result]
    @aws_subscribe(mutations: [ "publishResult" ])
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

Next, define the resolver for the search query in the AWS Management Console, AWS CLI, SDK, or AWS CloudFormation. Select the HTTP data source that was created previously and configure the request mapping template as follows:

$util.qr($ctx.stash.put("executionId", $util.autoId()))

{
  "version": "2018-05-29",
  "method": "POST",
  "resourcePath": "/",
  "params": {
    "headers": {
      "content-type": "application/x-amz-json-1.0",
      "x-amz-target":"AWSStepFunctions.StartExecution"
    },
    "body": {
      "stateMachineArn": "arn:aws:states:<REGION>:<ACCOUNT_ID>:stateMachine:aws-appsync-long-query",
      "input": "{ \"name\": \"$ctx.stash.executionId\" }"
    }
  }
}

There are a few items to note in this mapping:

  • We use the AWS AppSync $util.autoId() function to generate a unique identifier for the search query. This value is stored in the context stash and is later returned to the client. This value is also passed as an input parameter to our state machine.
  • The x-amz-target header specifies the AWS service and associated action to invoke.
  • JSON input data, such as the input to our state machine, must be double escaped.

The response mapping for this resolver immediately returns the query identifier and a PENDING status:

{
  "id": "${ctx.stash.executionId}",
  "status": "PENDING"
}

On executing the search query, AWS AppSync now starts the execution of the desired Step Functions state machine, passing the execution identifier as a parameter. The last step of the state machine is a task step that invokes a Lambda function. This function triggers an AppSync mutation. This causes the results to be published to the client through a subscription.

Implementing the long-running query and returning a result

With our AWS AppSync query now starting execution of the Step Functions state machine, we can implement the query and return a response to the client. For the purpose of demonstration, our state machine is quite simple. It waits for one minute, and then invokes the “Return Results” task, as shown in the following diagram.

You can easily update this simple state machine definition to support more complex or multistep queries, as long as the definition returns results through an AWS AppSync mutation along the way.

Generally, a GraphQL client executes a mutation of data that causes a change in a data source, which then triggers a notification of the change to subscribers. In this case, we define a Lambda function that calls the mutation to trigger a notification of the results.

Instead of including a GraphQL library, we’re using a simple HTTP POST to the AWS AppSync endpoint, passing an appropriate payload for a mutation. While our sample uses an AWS AppSync API_KEY authorization type, we recommend AWS_IAM in a production scenario, and would require signing the request as well. Using the nodejs8.10 runtime, our simple Return Result function is implemented as follows:

const PublishResultMutation = `mutation PublishResult(
    $id: ID!,
    $status: ResultStatus!,
    $listings: [String]!
  ) {
    publishResult(result: {id: $id, status: $status, listings: $listings}) {
      id
      status
      listings
    }
  }`;

const executeMutation = async(id) => {
  const mutation = {
    query: PublishResultMutation,
    operationName: 'PublishResult',
    variables: {
      id: id,
      status: 'COMPLETE',
      listings: [ "foo", "bar" ]
    },
  };

  try {
    let response = await axios({
      method: 'POST',
      url: process.env.APPSYNC_ENDPOINT,
      data: JSON.stringify(mutation),
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': process.env.APPSYNC_API_KEY,
      }
    });
    console.log(response.data);
  } catch (error) {
    console.error(`[ERROR] ${error.response.status} - ${error.response.data}`);
    throw error;
  }
};

exports.handler = async(event) => {
  // unique identifier of query is passed as event.name
  await executeMutation(event.name)
  return { message: `finished` }
}

AWS AppSync sends a notification to the client that’s subscribed for this particular query result. The client can then update appropriately.

Client implementation

Using AWS Amplify, we can implement a simple client to demonstrate the process of submitting the query and then subscribing for results using the returned identifier.

import Amplify, { API, graphqlOperation } from "aws-amplify";

// 1. Submit search query
const { data: { search } } = await API.graphql(
    graphqlOperation(searchQuery, { text: 'test' })
);
console.log(`Query ID: ${search.id}`);

// 2. Subscribe to search result
const subscription = API.graphql(
        graphqlOperation(onSearchResultSubscription, { queryId: search.id })
    ).subscribe({
        next: (result) => {
            // Stop receiving data updates from the subscription
            subscription.unsubscribe();
            console.log(result);
        }
    });

You can find a working example of this project on GitHub.

Invoking AWS services directly from AWS AppSync can simplify a number of use cases, such as implementation of a long-running query, or enabling the use of AWS AppSync as a GraphQL facade for other services. For more details, see the AWS AppSync Developer Guide. We’re eager to see how you take advantage of this new capability. Please reach out to us on the AWS AppSync Forum with any feedback.