How to Invoke AppSync from a Lambda function

How to Invoke AppSync from a Lambda function

The only time I drink Starbucks is when I travel through the airport. Last time, I noticed they now allow you to skip the line buy ordering from a QR Code. After making your online order, the baristas still get your order on a screen.

In another use case, I want to create an AI-generated bedtime story for my kids. When it comes to creating the image, audio, and story, these things take time. Instead of polling, I'd like to be notified when it's done.

Both of those scenarios are examples where you'd want to call AppSync from a Lambda function and what the focus of this post is about.

๐Ÿ“น This post now has a full video guide!

๐Ÿ—’๏ธ I provided a repo above incase you want to get straight to the good bits!

If you're familiar with Lambda functions but new to AWS AppSync, no worries, I have the video for you!

NodeJS Lambda Functions in the AWS CDK

Fortunately, when it comes to the AWS CDK and Lambda functions, we have the ability to create our resources in TypeScript. From the readme, this is how we create the Lambda function:

export const createInvokeAppSyncFunc = (
    scope: Construct,
    props: InvokeAppSyncFuncProps
) => {
    const invokeAppSyncFunc = new NodejsFunction(
        scope,
        `${props.appName}-invokeAppSyncFunc`,
        {
            functionName: `${props.appName}-invokeAppSyncFunc`,
            runtime: Runtime.NODEJS_18_X,
            handler: 'handler',
            entry: path.join(__dirname, `./main.ts`),
        }
    )

    return invokeAppSyncFunc
}

Note that aside from some props being passed in, the core is essentially giving it a name, a runtime, and pointing it to the file location.

How to allow Lambda to Sign with SigV4

This file sucks. I wish it were easier (now it is!). The good news is that you never have to modify it. There are some NPM packages out there that allow you to install it, but it's simple enough to just paste in a project.

Let's break it down:

import { SignatureV4 } from '@aws-sdk/signature-v4'
import { Sha256 } from '@aws-crypto/sha256-js'
import { defaultProvider } from '@aws-sdk/credential-provider-node'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { default as fetch, Request } from 'node-fetch'

Those imports are needed because the @aws-sdk v3 takes a modular approach. So every bit and piece comes from a standalone package.

Next, we have the following:

// deconstruct the url and create a URL object
const endpoint = new URL(params.config.url)

// create something that knows how to let Lambda sign AppSync requests
const signer = new SignatureV4({
    credentials: defaultProvider(),
    region: params.config.region,
    service: 'appsync',
    sha256: Sha256,
})

Not too bad ๐Ÿ˜„ This parses the AppSync GraphQL endpoint in an object containing it's various pieces (protocol, pathname, etc).

In addition, we create a signer by passing in details relating to what we're trying to sign. Sigv4 is similar to creating a JWT or hashing a password in my opinion. Not from a cryptography standpoint, but from a "Hey, I'm going to pass you stuff, and you do all the hard work for me" standpoint.

From there, we keep it going by taking that signing mechanism and passing it the request we are trying to sign:

// Setup the request that we are wanting to sign  with our URL and signer
const requestToBeSigned = new HttpRequest({
    hostname: endpoint.host,
    port: 443,
    path: endpoint.pathname,
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        host: endpoint.host,
    },
    body: JSON.stringify(params.operation),
})

// Actually sign the request
const signedRequest = await signer.sign(requestToBeSigned)

// Create an authenticated request for fetch
const request = new Request(endpoint, signedRequest)

With all that in place, we now have a request that is signed, sealed and in the try/catch block, delivered!

Usage

In the main.ts file, we actually use the AppSyncRequestIAM helper method by invoking it with our AppSync operation:

 const res = await AppSyncRequestIAM({
  config: {
    region: process.env.REGION as string,
    url: process.env.APPSYNC_API_URL as string,
  },
  operation: {
    operationName: 'BroadcastMessage',
    query: broadcastMessage,
    variables: {
      msg: event.msg,
    } as BroadcastMessageMutationVariables,
  },
})

Note that this only pertains to signing the request. It will never get this far to begin with if the following isn't enabled:

  1. The AppSync API doesn't have IAM authorization enabled

  2. The Schema doesn't have the @aws_iam directive listed on the operation

  3. The Lambda was never given persmissions to invoke AppSync (api.grantMutation(invokeAppSyncFunc)


Conclusion

Calling AppSync from a Lambda function--or any out-of-band service, requires IAM permissions. This can be tricky, especially the first time. But I hope this post showed you just how simple things can be when you understand the moving pieces.

What are some use cases you'd like to see covered? Let me know in the comments or on social media.

Until then, Happy Coding ๐Ÿฆฆ

Did you find this article valuable?

Support Michael Liendo by becoming a sponsor. Any amount is appreciated!