Photo by Ryan Quintal on Unsplash

Serverless GraphQL API Federation

Serverless Advocate
12 min readJan 30, 2023

--

Preface

✔️ Using GraphQL through AWS AppSync to federate many Private API Gateways.
✔️ Fully aligned to SAL Architecture i.e. experiences and domains (DDD).
✔️ Aligned to Team Topologies as the GraphQL API is a product owned by a Stream Aligned Team. The domains are also separate Stream Aligned Teams.

The code for the article can be found here:

Introduction

This article is going to cover how we can build a GraphQL API utilizing AWS AppSync as a proxy to many underlying private domain APIs which are built using Amazon API Gateway.

“Our aim is to have one GraphQL API federate multiple private domain service APIs..” — Lee Gilmore

What we will be building today

This is very much in line with SAL Architecture (Serverless Architecture Layers) where our domain services are private to the AWS network (e.g. Orders, Customers, Stock.. all private API Gateway services), and they are only accessible via well-defined private versioned APIs.

Note: The GraphQL API is a product owned by a 
Stream Aligned team from a Team Topologies perspective.

Our aim is to have one GraphQL API federate multiple private domain service APIs to return the data from the Orders and Customers private APIs in the following format:

The data above is coming from multiple private domain APIs.

A good high-level example of this is shown below in purple when we look at SAL architecture as a whole:

An example GraphQL API for multiple channels (consumers) in the Experience Layer; all utilizing domain logic from private domain services.

As you can see from the diagram above, we can have multiple channels in our experience layer consuming our GraphQL API i.e. mobile, websites, voice, chatbots, etc; which federates to our private domain APIs (Orders and Customers in this example) through PrivateLink.

If you would like more information on Serverless Architecture Layers (SAL Architecture) see the link below:

What are the benefits of GraphQL here in the Experience Layer?

The Customer Experience GraphQL API in the diagram above has the following benefits:

✔️ We can have multiple customer channels consuming the same GraphQL API, meaning quicker time to market. i.e. we can add new channels very quickly, as they can pick and choose the their mutations, queries and data.

✔️ It allows for a customer omnichannel experience across many channels, as we build the authentication and authorisation once on the API.

✔️ Distinct front-end clients for multiple platforms (web, iOS, etc.), each with different data requirements.

✔️ The backend APIs can serve data from multiple domain services, legacy systems, and even SaaS providers (not just AWS domain services).

✔️ The consumers can utilize the data that they require without over-fetching as you would traditionally with REST.

⚠️ Note: The examples are to explain the concepts only and are not production ready.

What are we building? 🔩

To take us on the learning journey, we are going to build out a new API for customers to buy IPA beers over the web from our ficticious company ‘Lee James Golden IPA’.

Our ficticious company ‘Lee James Golden IPA’.

We can see from the diagram below that we are allowing multiple ‘channels’ for our beer company to consume our experience layer GraphQL API which will federate our private domain APIs:

Note: A 'channel' is an experience in the experience 
layer which utilises a BFF (Backend for Frontend) API.
In Team Topologies this is a Stream Aligned team
which would own this API service as a product.

We can see that:

  1. We have a GraphQL API using AWS AppSync which allows mutliple consumers (mobile, web, chatbot) to utilise it.
  2. We use Lambda resolvers which have the specific IAM role for the execute-api, which allows them to use IAM Auth and SigV4 to sign the API requests for the underlying private domain API’s in the Domain Layer.
  3. The API requests from the Lambda resolvers need to go via VPC Interface Endpoints to allow them to flow to the private API Gateways for the two domains (orders and customers).
  4. The requests flow via PrivateLink to the two private API Gateways meaning they stay private to the AWS backbone network. The first of which is the Order service.
  5. The requests also flow through to the Customers API in the same manner.
  6. The private domain APIs have their own Lambda functions which perform the relevant business logic (in our basic example it is simply CRUD). As part of the business logic we both persist and query the items in/to DynamoDB; but as we are within a VPC we utilise VPC Gateway endpoints to allow the requests to flow to the DynamoDB service.
  7. We finally persist or query the items in DynamoDB.
Note: This is a very basic example of what this 
pattern could look like, and of course using SAL architectures
we would typically use clean code and hexagonal
architectures for the domain layer too (link below)

Which AWS services are we using? 💭

Before we go any further, let’s discuss at a high level what services we are using:

✔️ AWS AppSync

AWS AppSync creates serverless GraphQL and Pub/Sub APIs that simplify application development through a single endpoint to securely query, update, or publish data. For more information see the following link or video below.

https://aws.amazon.com/appsync/

✔️ AWS Private API Gateway

Using Amazon API Gateway, you can create private REST APIs that can only be accessed from your virtual private cloud in Amazon VPC by using an interface VPC endpoint. This is an endpoint network interface that you create in your VPC.

“In all cases, traffic to your private API uses secure connections and does not leave the Amazon network — it is isolated from the public internet.”

Using resource policies, you can allow or deny access to your API from selected VPCs and VPC endpoints, including across AWS accounts. Each endpoint can be used to access multiple private APIs.

In all cases, traffic to your private API uses secure connections and does not leave the Amazon network — it is isolated from the public internet.

✔️ AWS VPC Endpoints and PrivateLink

AWS PrivateLink allows the resources in your VPC to connect to services in other VPCs using private IP addresses, as if those services were hosted directly in your VPC.

Basic example of PrivateLink and VPC Endpoints.

In our example, we utilise PrivateLink for allowing:

✔️ The Experience Layer Lambda Functions to directly connect to the private API Gateways in the Orders and Customers VPCs, without the need for a Nat Gateway, or for the traffic to go through the public network. We keep it fully private and secure on the AWS network.

✔️ We also use it to allow the Lambda functions in the Orders and Customers private VPCs (Domain Layer) to connect to DynamoDB without the need for Nat Gateways as above.

You can use a NAT gateway so that instances in a private subnet 
can connect to services outside your VPC but external
services cannot initiate a connection with those instances. This
allows traffic to flow to the public internet.

A service consumer (our GraphQL service in the experience layer) creates a VPC endpoint to connect their VPC to an endpoint service (API Gateway). A service consumer must specify the service name of the endpoint service when creating a VPC endpoint. There are multiple types of VPC endpoints. You must create the type of VPC endpoint that’s required by the endpoint service.

✔️ Interface - Create an interface endpoint to send traffic to endpoint services. Traffic destined for the endpoint service is resolved using DNS.

✔️ Gateway - Create a gateway endpoint to send traffic to Amazon S3 or DynamoDB using private IP addresses. You route traffic from your VPC to the gateway endpoint using route tables.

The following diagram shows both Interface and Gateway endpoints:

https://docs.aws.amazon.com/whitepapers/latest/aws-privatelink/what-are-vpc-endpoints.html

Deploying the solution 🏗️

⚠️ Note: This will incur charges in your AWS account, and also remember to tear down the stacks afterwards.

To deploy the solution perform the following:

1. Add your own aws profile to three cdk.json files in the solution.

2. Deploy the customers domain service: cd golden-ipa-customers && npm run deploy

3. Deploy the orders domain service: cd golden-ipa-orders && npm run deploy

4. Deploy the experience layer GraphQL API:cd golden-ipa-experience && npm run deploy

⚠️ Note: to tear down the stacks please do an npm run remove in the opposite order to above.

Once successfully deployed, we can create customers and orders, and return them, all using the following GraphQL queries:

✔️ Create a new customer

mutation createCustomer {
createCustomer(input: {firstName: "Lee", surname: "Gilmore"}) {
id
firstName
surname
}
}
Note: This returns a new generated customerId for the following calls.

✔️ Create a new order for an existing customer

mutation createOrder {
createOrder(input: {customerId: "customer-id-here",
productId: "LEE-123", quantity: 1}) {
id
}
}

✔️ Get existing customer and associated orders

query getCustomer {
getCustomer(id: "customer-id-here") {
id
surname
firstName
orders {
id
productId
quantity
}
}
}

The final query displays the customer and their related orders, which are federated from multiple private domain services i.e. Customer and Orders:

Example of our GraphQL API with a response to getCustomer

Talking through the key code

Now that we have discussed the overall solution, the services used, and how to deploy them; lets now talk through some of the key code.

Lambda Resolvers

Lambda functions for AppSync resolvers highlighted in pink circles

Our AppSync API utilises Lambda function resolvers, with the functions sitting in their own experience layer VPC. This means that they can utilise AWS PrivateLink to federate to private API Gateways for both customer and orders.

The function code is shown below for create-customer.ts which is used by a Lambda resolver in AppSync:

We can see that the private API URL is passed through for the private customer API Gateway, and the calls are signed using SigV4 before sending the request.

Each Lambda function also has a dedicated role for the execute-api for the specific private URL it needs to communicate with, as shown below:

getOrdersHandler.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["execute-api:Invoke"],
resources: [props.ordersDomainExecutionArn],
})
);

VPC Interface Endpoint

VPC Interface Endpoints highlighted with a pink circle above

The calls from the Lambda function resolver above can be made via PrivateLink on the AWS backbone network using a VPC Interface endpoint, as shown below in the following AWS CDK code:

const vpcEndpoint = new ec2.InterfaceVpcEndpoint(
this,
"ExternalApiVpcEndpoint",
{
vpc: this.vpc,
service: {
name: `com.amazonaws.eu-west-1.execute-api`,
port: 443,
},
subnets: this.vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
}),
privateDnsEnabled: true,
securityGroups: [sg],
}
);

This means that the traffic flowing to the ‘execute-api’ (i.e. the private Orders or Customers APIs) will all be funnelled through the VPC Interface Endpoint and through PrivateLink (totally secure and not going via the public internet).

Private Domain APIs

Our two private domain APIs using Amazon API Gateway

The private domain API’s have resource policies for account based access typically in the following format:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::{{otherAWSAccountID}}:root",
"arn:aws:iam::{{otherAWSAccountID}}:user/{{otherAWSUserName}}",
"arn:aws:iam::{{otherAWSAccountID}}:role/{{otherAWSRoleName}}"
]
},
"Action": "execute-api:Invoke",
"Resource": [
"execute-api:/{{stageNameOrWildcard*}}/{{httpVerbOrWildcard*}}/{{resourcePathOrWildcard*}}"
]
}
]
}
Note: We can also tie down access via source VPC or VPC 
Endpoint ID as shown above.
To restrict access to specific VPCs and VPC endpoints,
you must include 'aws:SourceVpc' and 'aws:SourceVpce'
conditions in your API's resource policy. If your policy
does not include any of these conditions, your API will
be accessible by all VPCs.

An example of ensuring we only have traffic incoming from a specific VPC would be the following:

const apiPolicy = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["execute-api:Invoke"],
principals: [new iam.AnyPrincipal()],
resources: [
`arn:aws:execute-api:${props.region}:${props.accountId}:*/*/*/*/`,
],
}),
new iam.PolicyStatement({
effect: iam.Effect.DENY,
principals: [new iam.AnyPrincipal()],
actions: ["execute-api:Invoke"],
resources: [
`arn:aws:execute-api:${props.region}:${props.accountId}:your-rest-api-id/*/*/*/`,
],
conditions: {
StringNotEquals: {
"aws:SourceVpc": ["your-vpc-id"],
},
},
}),
],
});

In our example since we are deploying to one AWS account our resource policies look like this, meaning all VPCs in the account can access the private domain API’s:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::your-account-id:root"
},
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:eu-west-1:your-account-id:*/*/*/*/"
}
]
}

This means we are allowing any API consumers to be in our AWS account, and that it can call any of the private rest APIs via VPC Interface Enpoints.

Note: Typically outside of this basic example we would have our domain
services and experiences in different AWS accounts. In this instance
we would tie down the permissions in the resource policies so the traffic
can only flow from specific interface endpoints, and for specific API endpoints.
This is detailed in the repo below:

VPC Gateway Endpoints

Our VPC Gateway Endpoints are highlighted above with pink circles.

We use VPC Gateway Endpoints to allow our Lambda functions in our private domain APIs to communicate with the DynamoDB service, without the need for a NAT Gateway, as well as keeping the traffic on the AWS backbone network. The CDK code for this is shown below:

// create the vpc with one private subnet in two AZs
// note we have no nat gatways as all traffic will use private link
this.customersVpc = new ec2.Vpc(this, "CustomersVpc", {
cidr: "10.1.0.0/16",
natGateways: 0,
maxAzs: 2,
subnetConfiguration: [
{
cidrMask: 24,
name: "private-subnet-1",
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
});

// add the dynamodb vpc gateway endpoint
this.customersVpc.addGatewayEndpoint("DynamoDbEndpoint", {
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
});

Summary

I hope you found that useful as a basic example of federating multiple private API Gateways using AWS AppSync i.e. GraphQL. The key takeaways of this approach are:

✔️ The Domain APIs need to be fully versioned and treat as ‘products’, as well as easily discoverable, making life easy for the team which builds the experience GraphQL API.

✔️ All traffic is kept to the AWS backbone network through PrivateLink for faster and more secure communication between APIs.

✔️ Unfortunatly AWS AppSync HTTP resolvers can not yet federate directly to Private API Gateways (as shown below). This is for sure on my AWS wishlist as this would have removed the need for the Lambda functions in the experience layer:

Only public endpoints are supported by AWS AppSync. As a workaround, use Lambda resolvers as an entry point to Amazon VPC resources — https://aws.amazon.com/premiumsupport/knowledge-center/appsync-access-private-resources-in-vpc/

Wrapping up 👋

Please go and subscribe on my YouTube channel for similar content!

I would love to connect with you also on any of the following:

https://www.linkedin.com/in/lee-james-gilmore/
https://twitter.com/LeeJamesGilmore

If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋

Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)

About me

Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect and Global Serverless Architect based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.

I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture and technology.

*** The information provided are my own personal views and I accept no responsibility on the use of the information. ***

You may also be interested in the following:

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Written by Serverless Advocate

AWS Serverless Hero empowering others through expert knowledge | AI | Architect | Speaker | Engineering | Cloud Native | AWS x 7 Certified 🚀

Responses (1)

Write a response