At Jetabroad, we have to call Amazon Web Service API to manage our EC2 instances. The problem is
how to minimize API call and handle the schema provided as efficiently as possible.
We have internal infrastructure tooling used to maintain our environments: production, uat, test and
so forth. The tool has to call the AWS API to retrieve EC2 instances information as well as the ALB
related to the instances.
We use Octopus to deploy new versions to the instances. This means we have AWS and Octopus
as the third party APIs that we must call. We also call IIS to get metrics for each instance too.
The issue is how the tool was implemented, initially as follows:
- Call the Octopus API to get a list of machines.
- For each machine, Call AWS EC2 API to get EC2 instance information (id, name, running state, etc.)
- For each instance, Call AWS ALB API by the instanceId from step 2 to determine if the instance is in ALB target group.
- Call IIS to get metrics for each instance
- Project the result into view model
Even though those APIs are called “concurrently”, their imperative nature seriously impairs
the tool speed. This tool was created when we only had a few number of instances. Now is the time
to tackle this problem properly.
First, we found that step 2 relies on information from step 1 while step 3 relies on step 2.
These dependencies prevent us from using asynchronous calls because we have to wait for each step.
Thus, refactoring the dependencies and enabling asynchronous calls should solve the speed issue.
Conveniently, we can use “Environment Name” to get all the information from steps 1,2,3.
Second, we found that Octopus had instance name, ALB had instance ID, and EC2 had both, enabling
us to construct an outer join for the data
Third, we project the joined results into our view model and display to our developers.
To recap:
- Get machines from Octopus API
- Get instances from AWS EC2
- Get ALB states from AWS
- Get metrics from IIS
- Join 1,2,3,4 into view model
Although it looks like 5 steps, it is actually divided into two time-slots: API calls and the join operation, because
we can perform 1,2,3,4 concurrently. Now, our tool can handle a large number of instances with an
extremely short load time compared to the previous implementation.
It might not be so obvious, but we can compare both implementations from an imperative and functional
programming perspective. The first implementation is all about iteration and building data
on top of data, the second is about combining sets of data into another data which is informally called “set-based thinking”[ref].
It is easier to build a huge Lego from many small-connected-Legos than building a huge Lego piece by piece.