From Terraform in Action by Scott Winkler
This article discusses how Terraform can make serverless infrastructure easy and painless to use.
This scenario is something I like to call “the two-penny website”, because this is how much I estimate it costs to run on a monthly basis. If you can scrounge some coins from between the seat cushions, you’ll be good for at least a year or two of web hosting. Not to mention (I’m probably overestimating a bit); for most low traffic applications, the true cost is likely to be a fraction of one penny. Because serverless is based on the premise of pay-as-you-go, it’s difficult to estimate ahead of time exactly how much it will cost. Perhaps, if you have a popular website, you might be charged five cents rather than two cents. Nevertheless, I’m sure you agree that serverless has the ability to be comically affordable, and by the end of this article, you won’t have any excuse not to have some sort of web presence. You can even justify making silly, or single purpose websites, because it’s practically free anyways.
The website we’re going to deploy is a ballroom dancing forum, called “Ballroom Dancers Anonymous”. Unauthenticated users are able to leave comments which are stored in a database and are viewable by other users on the site. The design is fairly simple, but the beauty is that this is generalized to work with a wide variety of different web applications. A sneak peak of the final product is shown in figure 1.
Figure 1 Ballroom Dancers Anonymous Website
We’ll be using Azure to deploy the serverless website. A basic deployment strategy is shown in figure 2.
Figure 2. Deploying to Microsoft Azure
Architecture and Planning
Although this website costs only pennies to run, by no means is it a toy. Because it’s deployed on Azure Functions, it’s able to rapidly scale up to handle tremendous spikes in traffic and do this with low latency. It also uses HTTPS, a NoSQL database, and serves both static content (HTML/CSS/JS) as well as a fully-fledged RESTful API. An architecture diagram is shown in figure 3.
Figure 3. Azure Function App listens for HTTP requests coming from the internet. When a request is made, it starts up a just-in-time webserver from source code located in a storage container. All stateful data is stored in a NoSQL database using a feature called Azure Table Storage.
Because the code we’ll write’s relatively short and cohesive, I’ve decided to put it all in a single main.tf file, instead of using nested modules.
Tip As a rule of thumb, I suggest having no more than two hundred lines of code per file. Any more, and it becomes more difficult for a reader to build a mental map of how the code works.
The big question is, how should the code be organized to be both easy to read and easy to understand? Organizing code based on the number of dependencies is a tried and true approach. Typically, this means organizing code such that resources having fewer number of dependencies are located toward the top, but resources having greater number of dependencies are located toward the bottom. This leaves a lot of room for ambiguity, like when resources have the same number of dependencies, but don’t feel like they belong together.
Instead of strictly organizing your code by the number of dependencies each resource has, I recommend grouping related resources together and sorting them by group (i.e. sorting first by group, then by resource). This can be visualized in figure 4.
Figure 4. Configuration files should be sorted first by group, then by resource
In the same way that it’s quicker to search for a word in a dictionary than in a crossword puzzle, it’s faster to find what you’re looking for when your code’s organized in a sensible manner (such as the pattern shown above). Although Terraform won’t necessarily operate on the code synchronously—due to the complex way that Terraform walks the dependency graph—the goal is that someone reading your code from top-to-bottom should be able to completely understand how it works without having to think too hard.
For this project, I’ve divided it into four main groups of resources, each one serving a particular purpose. These groups are:
- Resource Group – the resource group and other base level resources are created first, as they are required by many downstream resources but have no resource dependencies of their own
- Storage container – the storage container is used to store the build artifact (or source code) for Azure Functions, as well as the records of the NoSQL database
- Storage blob – the storage blob is a binary file containing the build artifact that Azure Functions App needs to be able to run. This is a two-step process: we first need to download the artifact before we can upload it.
- Azure Functions App – the Azure Functions app needs to be created and configured. Anything related to achieving this goal’s considered part of this step.
This can be pictured in figure 5.
Note Because we’ll be working through these groups one at a time, I’ll use the word “step” interchangeably with the word “group”
Figure 5. Four groups (or steps) are in the project, each serving a distinct purpose
The last part of planning we need to do is consider the overall inputs and outputs of the root module. From a black box perspective, there are three inputs and one output. Two of the inputs are used for configuring the provider/base resources (client_secret and region) and the third is for affording a consistent naming scheme (namespace). The sole output value we’ll have is website_url, which is a live link to the final deployed website. This can all be visualized in figure 6.
Figure 6. Overall input variables and output values of the root module
If you’re anything like me, you might be thinking to yourself “it’s great you’re showing me how to do all this, but there’s no way I could possibly have come up with this on my own.” When writing this book, I didn’t want it to be another cookbook of copy-paste snippets. Terraform can be used in many different ways and there’s no possible way I could cover every potential use case. Instead, I wanted to give you a set of tools and thought processes to use to solve your own problems. Here’s a list of steps that I take when tackling a new problem with Terraform:
- Define the problem and goals
- Research potential solutions, while keeping an open mind
- Select key technologies and tools to use
- Build a prototype to determine if further investment’s warranted
- Develop final product
For this particular scenario, I knew my goal was that I wanted to deploy a serverless website on Azure, but I haven’t used Azure much before (I’m more of an AWS guy). I had to do a lot of research to figure out what services were available and how they could best be used. Then I selected Azure Functions and Azure Storage because those both seemed to fit my requirements. Afterwards, I built a quick and dirty prototype using the UI editor, before finally sitting down to translate this all into clean and working Terraform configuration code. It looks neat and tidy only if you ignore all the dead-ends I went through before settling on the final design.
Tip problem solving is an art, and the only way to get better is with practice.