Practical Appsync + Cognito for Single-Page Applications
Introduction
If you want to get new-agey and deploy a GraphQL API in a serverless architecture, what’s the best way? You can make that work with API Gateway but it’s a little cludgey and normally results in implementing a super Lambda function that contains all your GraphQL resolver logic.
Lucky us! AWS has a GraphQL specific API proxy called AppSync. Last month, I did an article overviewing AppSync and concluded I would try it out on an upcoming project. Well dear reader, I have done it and now I am bringing you my results!
Starting with AWS Amplify
AWS Amplify is a cool project with a lot of potential. It’s made up of several different projects:
- Amplify Framework: client libs for JS and Native
- Amplify CLI: command line tool for bootstrapping your project and managing backend code and infrastructure
- Amplify Console: CI/CD system that deploys amplify compatible apps
(I may have the naming scheme wrong; but it seems correct to me)
The Amplify framework is a great set of libraries. It offers you clients for AppSync, Cognito, and more. The AppSync client it provides is also compatible with the popular Apollo project. Amplify Framework and Amplify CLI are often used hand-in-hand. The Amplify console is purely optional, but a nice addition for automated deploys.
Out-of-the-box, the CLI tool gives you a lot:
- Scaffolds AppSync API w/ Cognito Auth
- Infers AppSync resolvers and data source backends from annotated GraphQL schema
- Code generation for GraphQL queries, mutations, and subscriptions
- Manages Lambda Functions
- Creates S3+Cloudfront static hosting for your SPA
One thing to note, everytime I setup both Amplify Console and CLI hosting resources, they deployed to different S3 buckets. Not sure if that’s always the case or I just missed something.
Overall, Amplify really is an awesome project and provided an excellent dev/test environment for me to quickly iterate and experiment with AppSync and Cognito.
(there’s a but coming)
BUT…
Amplify CLI has a few edge cases that are either really annoying OR are missing and really important for production applications.
- No DynamoDB secondary indexes; HUGELY IMPORTANT!
- Tricky and poorly documented to connect Lambda functions to AppSync
- Must write Lambda functions in Node.js or you’ll have a bad day
- Many config files are in
.gitignore
and the process to download them is brittle and a little cludgey - Uses Cloudformation for everything:
- Annoyingly slow for very simple changes
- No dry run (i.e. change set) that reports what it will change on
push
- Inconsistent usage of templates between resource types: e.g. sometimes only support JSON or only support YAML
- Hellish debugging: poor error reporting and uses nested stacks injected with implicit variables
- Failure rollbacks are annoying and occasionally scary (I dropped Dynamo tables unexpectedly multiple times; still not sure why)
A lot of these things will be addressed as Amplify continues to mature, I have no doubt.
I also don’t fault Amplify team for using Cloudformation because (a) they work at Amazon, (b) it stores all state in AWS be default which is easy for tool providers, and (c) rollbacks can be nice. But my personal goal is to use Cloudformation as little as possible. Small doses, it’s fine. Big doses, annoying AF.
Replacing Amplify for Production
For using Cognito and AppSync in the client, I still use the Amplify JS libraries which are quite awesome.
Then, using the Cloudformation tempates generated by Amplify CLI as a guide, I re-implemented them in terraform! For Lambda functions, I used serverless! Lucky for me, it only took me an afternoon to accomplish the entire switch!
Here is a link to my example repository: https://github.com/tgroshon/amplify2terraform
My terraform scripts are in terraform/
and the resources are organized thusly:
main.tf
: Variables, Outputs, and Data resourcesapi.tf
: DynamoDB; AppSync API, Data Sources, and Resolvers; IAM permsauth.tf
: Cognito Identity Pool, User Pool, User Groups, Clients, and IAM roleswebsite.tf
: S3 website bucket, CloudFront distribution, Route 53 Record
Serverless functions are in services/
. The code is just a dummy example. The serverless.yml
however shows a few useful examples:
- Giving access to DynamoDB tables
- Outputting your function ARNs so you can plug them into terraform as variables
When using the terraform scripts in a non-toy app, I create multiple terraform workspaces
and separate <stage>.tfvars
files for use with each. Not beautiful, but works.
Upsides of this approach:
- Using Terraform!
- Changes are comparatively lightning fast (ok, my setup creates a Cloudfront distro which takes forever, but otherwise … fast!)
- Predictable and well-understood
plan
andapply
workflow - HCL is 10x better than cloudformation templates
- Workspaces do the
amplify env
workflow more transparently - Complete control of your infrastructure
- Using Serverless!
- Awesomely battle-tested project
- Supports tons of languages and Lambda Layers
- Still lets you do Cloudformation if you need to (meh)
This approach is not without it’s downsides however. The main ones are (a) losing the CLI code generation features and (b) losing automagic CI/CD with the Amplify Console.
Those downsides acknowledged, I am still really enjoying this approach.
Conclusion
AppSync and Cognito are really awesome for frontend development projects. For quickly experimenting with them in a project, I recommend using AWS Amplify. But when the day comes to ship a product, you don’t have to give up control and predictability. You can take your same client code and, without much work, roll it over to terraform + serverless!