Lambda Sources and Thundra Layers ~ A match made in Serverless
Anyone who has ever worked with GraphQL can ratify that its strength lies in its flexibility. Taking this into account, I went a step further with the first partof my GraphQL series as I proposed that AppSync is the perfect development tool to use when building with GraphQL. In the second part, I demonstrated how easy it is to get GraphQL working with AWS AppSync. With this third and final part of the series, I intend to go one step further and add Thundra to the stack, showing how much easier it gets in developing a GraphQL data-driven application with AppSync. That is because there is always room for improvement, and with the services out there in the vast landscape of software development, it is imperative that we choose the right tools to excel the development of our own software. With this last piece, I also aim to signify the power of GraphQL with AppSync by using Lambda functions as data sources and resolvers, allowing me to achieve practically any business logic.
Issues with Lambda Data Sources
It is a well-known fact that AWS Lambda functions mitigate the issues of running business logic on the cloud, either by reducing the complexity of running functions on the cloud or by the pay-per-request billing that AWS has conveniently set up. However, when it comes to GraphQL, the benefits that the humble yet powerful Lambda service manifests is in the fact that you can perform any query or mutation on your dataset, the only constraint being your aptitude to code. This, in turn, amplifies the already myriads of benefits fostered by AppSync. In fact, one can even go as far as saying that one of the greatest advantages of AppSync is its ability to allow using Lambda functions as data sources. This is because the developer is no longer restricted to the paradigms of other data sources such as using DynamoDB directly or Amazon Aurora clusters. Utilizing Lambda thus allows you to break the bonds of AWS services and query data from whatever is out there in the world of software services. You would find yourself using Lambda data sources to connect to services such as LiteDB if you are a .NET guru and want to tap into its lightweight and powerful database services, or Hazelcast to take advantage of its powerful distributed caching abilities. AppSync is already one of the most formidable and beneficial tools to develop data-driven applications using GraphQL. The use of Lambda functions simply makes it better.
However, there is a catch. Developing Lambda functions as data sources puts the developer in the wild west when it comes to monitoring and debugging. No longer are we within the comfort zone of predefined templates and code generation that AppSync provides for other forms of data sources. Moreover, the conventional, and quickest way of testing whether your GraphQL operation works, is by actually executing the operation with the help of AppSync’s Query functionality. This obviously is after you set up your Lambda function as a data source, a task that I had described in the second part of this series. Testing with the Query functionality is extremely easy, however, when it comes to Lambda functions, there could be innumerable errors resulting in your Lambda function to crash. As we all know, getting the code to run perfectly the first try is an act of the Gods, one that us mere mortals cannot achieve. Therefore, developers traditionally turn to log messages and basic console print statements for quick debugging and testing of their code. However, these logs and print statements cannot be seen in the Query functionality of AppSync, hence making it a bitter experience to gain from the benefits and power of Lambda functions as data sources. The developer can still turn to the function’s CloudWatch logs to gauge the performance of his Lambda data sources, but this, in turn, leads to more problems and inefficiencies in development. Using CloudWatch logs is like using markdown language to make a 5th-grade presentation on Abraham Lincoln, it is just not worth going through all the pain. Just use Powerpoint!
To make matters even worse, another issue is the presence of code smells. There are no fixed rules to achieve good code and implementation practices. This stems from the point being reiterated that when developing Lambda data sources, developers are in the complete open concerning how they would like to perform their operations. That creates possibilities of inefficient operations and bad implementations. For example, when querying for a specific set of data from your DynamoDB resources, one can always choose between the ‘Scan’ operation and ‘Query’ operation. To perform a ‘Scan’ means going through every item in your DynamoDB table and filtering the items according to your ‘Filter’ expressions to procure the exact data required. This thus adds an extra step, and it is recommended to use the ‘Query’ operation in such cases. These such decisions may not impact the validity of your application, but it may impact the ability to scale. Many of us may not be aware of the potential disadvantages of our production decisions, and that is mainly because we are not getting any substantial feedback on the performance of these operations. At most, we can see the time taken by each invoke of our Lambda data source on the AWS Lambda console, but that too is subject to change as per the conditions of the AWS environment. Additionally, it becomes infeasible to check individual duration measures of each operation involved, especially as some data-driven applications can lead to numerous operations being implemented. Consequently, even though using Lambda functions allow the developer to attain flexibility, what the developer loses is guidance. I am not saying that building Lambda data sources for GraphQL operations is a difficult task. On the contrary, it is extremely easy, but without the right metrics and data to measure against, even spirited developers may be disappointed with the outcome of choosing Lambda data source. The fact of the matter it that CloudWatch and the AWS Lambda console alone are not the best tools to gain absolute transparency into your Lambda data sources.
Thundra for Easier Development
Considering the issues that can discomfort developers of GraphQL applications in the AWS environment, Thundra provides relief in form of serverless monitoring. This is because it brings observability to your Lambda functions by providing trace data, metrics and logs. Thus, you are now able to use conventional print statements and logging methods for quick debugging and testing of your application, and also pinpoint the exact causes of errors and breakdowns with the Thundra console. You can also see the bottlenecks in your Lambda function, which operations are resource intensive and gauge the overall performance of your Lambda data source on your API. As a result, not only can we now debug and test our Lambda data sources, but we can also evaluate the data source written according to the duration of querying and manipulation operations measured by Thundra.
Thundra makes building Lambda data sources for your data-driven applications via AppSync easier, and this is what this piece aims to demonstrate. Hence, in order not to hijack the topic to tutorials on Lambda Layers and AWS custom runtimes, it will be presumed that at least the basic idea of what these novel AWS features entail is known. The reason why we mention these services is that with Thundra Layers and Custom Runtime support, using Thundra to monitor your Lambda functions takes literally less than a minute. Nevertheless, there is no need to stop reading this piece and dive into hour-long tutorial videos on these AWS services. We shall see during the course of this demonstration that using Layers and Custom Runtime support is extremely easy.
Adding and Using Lambda Data Sources
Before we can dive into Thundra monitoring, we must first be clear how we will be using Lambda functions to act as data sources and resolvers for our GraphQL based data-driven application. As I had previously mentioned in my previous pieces, the Data Sources functionality of AppSync makes integrating Lambda functions as data sources extremely easy. Moreover, by selecting the Lambda data source when constructing your resolver allows AppSync to invoke the Lambda function whenever the respective operation is executed. It even sends payload data to your Lambda function. The feature of AppSync automatically invoking the Lambda function upon the execution of the operation, with payload data, is what we shall use to map the operation to the correct method in the Lambda function.
After adding your Lambda function, you can select it as the data source of choice when setting up your resolver mapping an operation to the business logic. AppSync already knows that the data source you added is a Lambda-type data source and thus the request and response mapping will be pre-written accordingly.
The approach of mapping the operation in the AppSync schema to the methods in the Lambda function can differ as per the needs of the developer. For example, we can have individual Lambda functions for each operation or have a single Lambda function for all the schema operations. Alternatively, we can even set up several Lambda functions for sets of schema operations, depending on the structure of the application. For the animal reserve demo application (yes, still sticking to the Zambian national reserve demo), I decided to add all my business logic in one Lambda function and call respective methods according to the invocation by different operations executed.
By including all my business logic in a single Lambda function, the issue now is how to call the correct function method according to the operation that invoked the Lambda function. For this, I made use of the payload that is passed to the Lambda function upon invocation as the event. By altering the payload, I added the key ‘field’ which indicates which operation is being executed. Similarly, in the handler file of my Lambda function, I have a switch statement which takes the value of the ‘field’ key and thus calls upon the correct methods to handle the operation. You may find the Lambda function here.
Configuring and Using Thundra
When developing Lambda functions as data sources for data-driven applications, there are several issues that one is bound to face. It is crucial that if Thundra is aimed at helping you resolve these issues, and hence its own configuration and use must be as simple as possible. Let us not fall victim to the irony of incorporating difficult-to-use tools with the aim of making development easier.
Therefore, to configure Thundra, I made use of Thundra Layers and Custom Runtime support. To do so, I opened the Lambda function I built in the AWS Lambda console and used the layers option to add Thundra’s latest Layer for the Node.js runtime. I then changed the ‘Runtime’ to ‘Use custom runtime in function or code’. These two steps alone allowed me to set up the entire serverless monitoring tool. The only thing that was left was to add my Thundra API key as an environment variable in the console, and I was up and running.
After the configuration, simply executing a query in the Query panel of AppSync led to my first AppSync data being registered in the Thundra console. That meant I could now see all my monitoring data, including traces, logs, and metrics. I could also get important insights from the Performance Analysis feature of the Thundra console regarding the performance of my GraphQL operations.
I added additional Thundra configurations via environment variables to perform automatic instrumentation of my Lambda function. Thundra’s instrumentation allows you to see detailed trace data in the form of spans, including the individual functions that are being measured and the external services that are being interacted with. Therefore, considering the Lambda function I created, my handler file calls upon another file called ‘services.js’ which holds all the methods pertaining to the business logic. The ‘services.js’ file is basically responsible for communication with DynamoDB, performing the respective table operations needed. Therefore, to monitor which methods from the ‘service.js’ file are being called upon, I decided to instrument my Thundra trace data to also monitor the ‘services.js’ file using the `thundra_agent_lambda_trace_instrument_traceableConfig` environment variable specific for Node.js instrumentation.
After the elementary configurations and instrumentation of the Lambda data source, the monitoring data received was extremely valuable. I found myself relying on the monitoring data several times in building this demo function, without deliberate errors for demonstrative purposes. For example, the first schema operation that I started to code was to add an animal item in the Animal DynamoDB table. When I finished writing what I thought was correct code, due to the simplicity of the operation, I was amazed to find that the ‘addAnimal’ operation, when executed in the AppSync Query terminal, returns a null value!
From the AppSync Queries terminal itself, not much can be deduced. However, I could detect the exact problem when I visited the Thundra console. According to the response message in the trace data of the function, I realized that out of haste, I did not initialize my AWS DynamoDB object correctly. Without Thundra, I would not have realized this minor yet fatal mistake so quickly.
Other Thundra features that I used to help me out while building this demo project was the Logs and Performance Analysis functionalities. Using Thundra Logs, I could quickly see all the ‘console.log’ print statements, and ensure that the correct payload was being passed to the Lambda function. This is something that I would never have been able to see on the AppSync Query terminal unless I tediously pass it into the return message. However, that itself would destroy the structure of the returned data, nullifying the purpose of GraphQL. Turning to CloudWatch logs would provide some respite, but I would have to find the payload message in a plethora of CloudWatch messages that are usually generated by the Lambda function of this manner. Hence Thundra’s Log view provided quick and detailed access to the information that I needed.
Similarly, from the Performance Analysis view, I could get an insight into which operations are the most straining on my application. For example, from the Resource Usage graph I can see that the Delete operation is more resource intensive than the Write operation. I could conclude this as I executed two invocations within 15 minutes, one ‘deleteAnimal’, and the other ‘addAnimal’, and compared them using the Resource Usage graph.
Overall, read operation of the Reserve T cause the most resource intensive, which makes sense as most operations executed would first check for the animal reserve.
Moreover, the issue of resource intensive and time consuming read operations on the Reserve DynamoDB table is further exacerbated due to cold starts as this can be seen in using the Heat Map in the Performance Analysis. By selecting the time consuming invocations in the Heat Map, it is clear that they represent only those invocations that result in cold starts and that are read operations on the Reserve Table. This could never have been seen if we were only using AppSync’s Query terminal or the AWS Lambda console along with cloud watch. Thanks to the data illustrated in the Performance Analysis, I can not think of restructuring the operation business logic, specifically targeting the bottlenecks as presented.
In conclusion, it can be seen that even though Lambda functions as a data source in AppSync is greatly beneficial, it also has its downsides. These issues can dissuade developers from choosing Lambda data sources, or AWS AppSync in general. However, Thundra attenuates the effects of the pitfalls introduced. This is because Thundra provides all the monitoring data needed in an accessible and comprehensible manner. Due to the transparency that THundra brings, it becomes extremely easy to build GraphQL based applications using AWS AppSync along with Lambda Functions as data sources and resolvers. At the end of the day, you still benefit from all power and flexibility of GraphQL in AppSync, without having to deal with any complexity potentially introduced when dealing with Lambda data sources. It can thus be seen that adding Thundra in the stack really does affirm the age-old saying… “There is always room for improvement”.