Infrastructure as Configured

Infrastructure is difficult to manage but your code shouldn't be.  Having worked on infrastructure for most of the last 25 years one thing I know for certain is that complexity should be removed wherever possible.  The examples I use below are specific to Azure and ARM templates but are applicable to any cloud provider and Infrastructure as Code language. That said, I would like to share some thoughts I have about how to remove complexity without sacrificing functionality.

Whenever possible, you should strive to create a single flat file for each resource that you manage.  If you have an Azure SQL database which is running your production subscription, then you should have a single file which contains all of its configuration.  This simple approach offers several benefits.  First by maintaining a 1:1 relationship between code and resource you ensure that any changes made to the configuration apply to exactly one resource.  The configuration changes are declared in an easy-to-read language and the schema for each resource is publicly available.  The schema contains every property and value accepted for each resource which makes it is easy to verify.  As a reviewer, you know to expect 1 file changed for each resource being updated.  Last, if the scope of a change is a single resource, then you can execute the change quickly as it requires a single deployment to a single environment.  It isn’t a code promotion, it is applying new configuration.  One single flat file per resource makes authoring, reviewing, and deploying changes easier.

Resource Group
Resource Group
Subscription
Subscription
Resource Group
Resource Group
Keep it SImple
Keep it SImple
Traditional
Traditional
Resource Group
Resource Group
Subscription
Subscription
Resource Group
Resource Group
Text is not SVG - cannot display

When I say a single flat file per resource, I mean to the extent possible you want your code to consist of properties and values only.  The point of this approach is to make it absolutely clear exactly what the configuration is supposed to be without interpretation.  This really shouldn't be Infrastructure as Code at all. Instead, we should think of this in terms of Infrastructure as Configured.  There should be no input parameters, expressions, conditionals, repeat loops, or dynamic references anywhere in your configuration.  While these are well understood concepts, they also create complexity which I would argue is more than they are worth. 

Input Parameters help ensure a consistent configuration by allowing you to specify attributes that change between resources while keeping everything else the same.  This seems like a great idea, however, if you are using them then you are almost ensuring that you will have more than one resource per configuration.  When changing an existing configuration or approving a pull request that uses parameters you are forced to acquire contextual information about what that parameter is.  You may need to determine its type such as string, int, bool, or object.  You may need to understand if the parameter has a default value.  It will be necessary to review if there are any validation constraints for potential values.  You will also need to determine how these parameters are passed and whether they meet all the above conditions.  Obviously, the more input parameters that are defined, the bigger the task you will have to complete.  Parameters often become the basis for further complications in your code like expressions and conditionals. 

Prod
Prod
Test
Test
Dev
Dev
WebApp
Template
WebApp...
WebApp
Template
Dev
WebApp...
WebApp
Template
Test
WebApp...
WebApp
Template
Prod
WebApp...
ServerFarm
Template
Dev
ServerFarm...
ServerFarm
Template
Test
ServerFarm...
ServerFarm
Template
Prod
ServerFarm...
Traditional
Traditional
Keep It Simple
Keep It Simple
Text is not SVG - cannot display

Expressions can provide a lot of functionality, but they also can create some significant challenges.  In general, expressions are used to evaluate functions, build formatted strings, create timestamps, generate UUIDs, and other operations.  Expressions are used to derive values based on a set of inputs.  A typical use case for expressions would be implementing a naming convention for resources based on provided inputs for environment, product, and service.  Some resources require uniqueness within the name which can also be achieved using expressions.  The challenge this creates is that an expression must be evaluated before we can understand the result.  The evaluation of infrastructure code expressions happens in the runtime environment for the cloud provider which we do not have access to.  We can only create an expression and run a deployment to see if it succeeds and produces the desired result.  This leads to a lot of trial and error that creates overhead. 

#Don’t do this  
  "parameters": { 
      "webAppName": {
        "type": "string", 
        "defaultValue": "Acme", 
        "minLength": 2, 
        "metadata": { 
          "description": "Base name of the resource such as web app name and app service plan "
        } 
      }, 
      "environment": {
        "type": "string", 
        "defaultValue": "Dev", 
        "metadata": { 
          "description": "SDLC designation for the environment i.e. dev/test/prod." 
        }
      }
    }, 
   "resources": [ 
      {
        "type": "Microsoft.Web/serverfarms", 
        "apiVersion": "2022-03-01",
        "name": "[format('{0}-WebApp-{1}',parameters('webAppName'),parameters('environment'))]",
         . . .
      } 

#Do this instead    
   "resources": [
      {
        "type": "Microsoft.Web/serverfarms",
        "apiVersion": "2022-03-01", 
        "name": 'Acme-WebApp-Dev',
         . . . 
      }

Conditionals, on the other hand, provide a mechanism to do one thing if a certain criterion is satisfied or a different thing if not satisfied.  Conditionals let you optionally set values or even create resources based on a set of inputs.  Conditionals are often used to handle situations where resources are configured differently in production versus everything else.  You may have a requirement to use a D16v5 VM for production but use D4v5 for everything else.  While conditionals are not hard, the dilemma we run into is not knowing all the possible options when the conditional is first written.  Inevitably, there comes a time when one of those non-production environments needs production quality hardware, or you find out some environments can be made with even less hardware.  Often conditionals are used in conjunction with expressions, which makes debugging and troubleshooting even more difficult since we cannot step through the code as we would with a debugger. 

#Basic conditional using an expression to determine sizing 

#Dont do this 
   "variables": { 
      "productionSizeHardware": ["prod","staging"], 
      "appServicePlanName": "[format(Asp-{0}-{1}', parameters('webAppName'),parameters('environment'))]" 
    }, 
    "resources": [ 
      { 
        "type": "Microsoft.Web/serverfarms", 
        "apiVersion": "2022-03-01", 
        "name": "[variables('appServicePlanName')]", 
        "location": "[parameters('location')]", 
        "sku": { 
          "name": "[if(contains(variables('productionSizeHardware'),tolower(parameters('environment'))),'P2','S1')]" 

#Instead create a separate templates for each environment which explicitly sets the sku, location, name, etc.

For some of the same reasons as conditionals and expressions, you should eliminate repeat loops from your code.  Repeat loops are a method of performing a specific operation for each item in a list.  A repeat loop might be useful for assigning the same Role Based Access Control to a list of Azure AD groups.  It might also be useful for creating several App Service instances within the same App Service plan.  The loop saves lines of code by repeating the same code block for each item in our list.  However, some tools will have issues if the list ever needs to change.  In some extreme cases, this could cause all such resources to be deleted and reprovisioned.  Loops also become problematic to maintain should you need to customize one of the resources but not all of them.  This leads to the addition of conditionals with inside each loop.  A practice which forces us to evaluate the condition for every resource in the list and then validate only the right actions were taken. 

References are another thing you should avoid using in general although there is at least one case where you will want to use them.  References create complexity because they add an external resource to your config without managing it.  For example, to provision and Azure SQL Database you must have an Azure SQL instance.  Instead of referencing the Azure SQL instance, you should explicitly provide the resource id.  This avoids an external call which will improve the processing time of your deployment.  It also avoids a situation where an API version change returns different results.  The time to use references is when you need to provide sensitive values like keys, connection strings, and passwords to your infrastructure.  Obviously, those should never be exposed in plain text. 

The main takeaway here is that maintaining flat files which are easy to read is more important than efficient coding.  Sure, you will have many files which are mostly the same, but that really isn’t a problem.  Today’s editors make it easy to search across files and replace text which negates much of that problem.  In my next post, I will share some ideas about how to achieve consistency with your code by adding IaC tests. 

Jason Brisbin

Jason Brisbin is a Solution Architect at iTrellis. Jason is an experienced Platform Engineering Architect with whose background is managing servers, storage, virtualization, and networking for a wide variety of clients, ranging from startups to global enterprise. Jason is passionate about systems integration, automation and security across IT.

Previous
Previous

Automated Application Testing using Selenium

Next
Next

Using a Domain Model for Time in .NET