visit
- There is one Serverless framework project (as in, one
serverless.yml
) in the repo, and one CI/CD pipeline.- This
serverless.yml
contains every backend infrastructure – AppSync APIs, DynamoDB tables, S3 buckets, Lambda functions, etc.- The project uses the to configure the AppSync APIs.- There are two AppSync APIs – one for the mobile app and one for a browser-based CMS system.- There are two Cognito User Pools – one for the mobile app and one for the CMS.- There are a total of 26 DynamoDB tables. There is one table per entity, with a few exceptions where I applied Single Table Design for practical reasons.This might be very different from how your projects are set up. Every decision I made here is to maximize feature velocity for a small team. The client is a bootstrapped startup and cannot afford a protracted development cycle.If you want to learn more about this project and how I approach it from an architectural point-of-view, please watch .On a high-level, the backend infrastructure consists of these components:Even though AppSync integrates with DynamoDB directly in most cases, there are still quite a few Lambda functions in the project. As you can see from the System Map in .
The advantage of this approach is that it doesn’t require any code change to my
serverless.yml
. Existing references through !Ref
and !GetAtt
still work even when the resources have been moved around.The
split-stacks
plugin converts these references into CloudFormation Parameters
in the nested stack where the references originate from (let’s call this NestedStackA
):If the referenced resources are defined in the
root
stack, then they are passed into the nested stack as parameters.If the referenced resources are defined in another nested stack (let’s call this
NestedStackB
), then the referenced values are included in the Outputs
of NestedStackB
. The root
stack would use GetAtt
to pass these outputs as parameters to NestedStackA
.In the generated CloudFormation template, this is what it looks like. Here the
root
stack passes the output from one nested stack as a parameter to another.The plugin has a number of built-in migration strategies –
Per Lambda
, Per Type
and Per Lambda Group
.However, I needed more control of the migration process to avoid circular dependencies. Fortunately, the plugin gives me of the migration process by adding a
stacks-map.js
module at the root of the project.This is especially true when you’re working with AppSync as there are quite a few different types of resources involved. For example,
AWS::AppSync::DataSource
references Lambda functions or DynamoDB tables, and also references the AWS::AppSync::GraphQLApi
for ApiId
.As a first attempt, I sliced up the resources based on the AppSync API they belong to. The DynamoDB tables are kept in the
root
stack since they are shared by the two AppSync APIs. Other than that, all the other resources are moved into one of two nested stacks (one for each AppSync API).Additionally, the
split-stacks
plugin automatically puts the AWS::Lambda::Version
resources into its own nested stack.This was the simplest and safest approach I could think of. There was no chance for circular dependencies since the two nested stacks are independent of each other. Although they both reference the same DynamoDB tables in the
root
stack, there are no cross-references between themselves.But as you can see from the screenshot above, there are a lot more resources in the
AppSyncNestedStack
than the AppSyncCmsNestedStack
. Pretty soon, the nested stack for the AppSync API for the mobile app would grow too big.The
AppSyncNestedStack
contains a lot of resources related to its Lambda functions. So the natural thing to do next was to move the Lambda function resources out into their own nested stack.This was because the project now has over 60 Lambda functions. So the
VersionsNestedStack
had over 60 AWS::Lambda::Version
resources, each requiring a reference to the corresponding AWS::Lambda::Function
. Therefore, the stack had over 60 parameters, one for every function.And so I also had to split the
VersionsNestedStack
in two.As the AppSync API for the mobile app approach 150 resolvers, the
AppSyncNestedStack
hit the 200 resources limit again.No duplicate resource names
For instance, if a Lambda function is moved from one nested stack to another, then the deployment will likely fail because “A function with the same name already exists”. This race condition happens because the function’s new stack is deployed before its old stack is updated. The same problem happens with CloudWatch Log Groups as well as IAM roles.To work around this problem, I add a random suffix to the names the Serverless framework generates for them.No duplicate resolvers
Similarly, you will run into trouble if an AppSync resolver is moved from one nested stack to another. Because you can’t have more than one resolver with the same
TypeName
and FieldName
.Unfortunately, I haven’t found any way to work around this problem without requiring downtime – to delete existing resolvers, then deploy the new nested stacks. Instead, my strategy is to pin the resolvers to the same nested stack by:1. use a fixed number of nested stacks2. hash the logical ID of the data source so they are always deployed to the same nested stack3. when I need to increase the number of nested stacks in the future, hardcode the nested stack for existing data sourcesIf you can think of a better way to do this, then please let me know!So that’s it on how I scaled an AppSync project to over 200 resolvers, hope you have found this useful!If you want to learn GraphQL and AppSync with a hands-on tutorial then please check out my where you will build a Twitter clone using a combination of AppSync, Lambda, DynamoDB, Cognito and Vue.js. You will learn about different GraphQL modelling techniques, how to debug resolvers, CI/CD, monitoring and alerting and much more.Previously published at