Blog

Azure infrastructure as code


By Mike Bain

07 September 2017

Find out how Microsoft has answered the call for consistent, version-controlled infrastructure deployments through code.

At Storm ID, we have been developing and deploying our solutions in Azure since 2014. It was a relatively slow start, all things considered, but things have really come on a lot since then – both in the Azure offering, as well as how Storm utilise it to improve our own workflow, enabling us to deliver faster for our clients and improve the quality of the work we deliver.

What is infrastructure as code?

Infrastructure as code (often shortened to IAC) is a process used to manage and provision computing infrastructure. It employs a declarative approach while utilising configuration by definition files rather than configuration tools.

Benefits

This process has tangible benefits.

  • IAC (with Azure Resource Manager API – ARM) provides declarative infrastructure configuration with the code being a complete and auditable record of the desired infrastructure.
  • IAC (with ARM) is idempotent so can be reused to update infrastructure configuration with minimal risk.
  • IAC (with ARM) can be used as the template in the provision of future infrastructure.

In its infancy

In hindsight, that initially slow start to our own adoption of IAC was reflected by Azure’s own relative shortcomings in its earlier management API. What then followed, however, is something I can only offer huge credit to Microsoft for. Through their ever growing and constructive online community, Microsoft identified these shortcomings and very quickly went to work rectifying them.

In fact, it was only a relatively short period after our initial adoption of the Microsoft-based Cloud platform, that Microsoft delivered the first public release of their (then new) Azure Resource Manager API – referred to as ‘ARM’ by most.

Template-based deployments of infrastructure and resources in readable text form, built around the new API model, at that time, promised to transform the process of building, deploying and managing large-scale complex applications into relatively simple and consistent actions.

While there have undoubtedly been some bumps along the road since the beginning, the product is now mature and reliable enough to truly deliver on that initial promise.

Three years on

ARM has become the de facto standard API through which Azure components and resources are delivered and managed today. They are truly built for scale on a new platform that encourages huge streams of innovation across the digitally connected world.

The ARM template model offers Storm (and others) the ability to stitch together resources such as Virtual Machines, Storage Accounts, NICs, Load-balancers, Virtual Networks, and other PaaS-based services like App Service and SQL Databases, into a single coherent application model.

Furthermore, with the ability to apply role-based access permissions at the API level, it enables users to deploy resources with the ease and speed of the CLI, but with the level of security modern-day enterprise demands.

At Storm, my typical workflow begins with a brief from a member of our Account Management team. Having first engaged with a Lead Developer and/or a Solutions Architect, the brief will include a list of required resource types, how those resources fit together, and an expected or desired volume of traffic that the solution must be able to handle while remaining performant. More often than not, I will engage directly with the developer, just to make sure we are all singing from the same hymn sheet.

My responsibility is then to gather (what I grudgingly refer to as) non-functional requirements – after all, without such requirements being correctly catered to, the solution would not function as it should.

Factors such as security requirements, privacy and GDPR requirements, operational requirements – those such as backups, monitoring and alerting, telemetry and configuration management – are all considered, and planned out at this stage.

Once I have all the required information at my disposal, I will build out a text-based infrastructure template of how both the QA and the Production environment will look. These templates are then deployed using a custom PowerShell script, specifically tailored to meet our own ‘non-functional’ requirements (including storage, scheduled backups, DNS place holders, vanity domains for our QA applications, SSL certificates, and web bindings).

This is when the real magic happens – it sounds a bit geeky, I know, but I still really get a kick out of seeing my code running from top to bottom and spitting out all of the resources and configuration we set out to define back at the beginning.

After carrying out a final sanity check of the resources produced and corresponding configuration objects, I am confident in handing the infrastructure over to the developer to carry out their work.

ARM template

I have included some snippets within an example ARM template for a test environment below. Defined in JSON, with much of the accompanying code edited out to make visualisation easier on the eye, the snippets represent the declarative configuration of some typical resources, including Azure Storage, SQL database, and an Azure App Service.

Blob storage:

{
      "name": "[variables('blobStorageName')]",
      "type": "Microsoft.Storage/storageAccounts",
      "location": "[resourceGroup().location]",
      "apiVersion": "2015-06-15",
      "dependsOn": [ ],
      "tags": {
        "displayName": "qablob"
      },
      "properties": {
        "accountType": "[parameters('blobGeoRedundancyType')]"
      }
}

SQL Database:

{
      "name": "[variables('sqlserverName')]",
      "type": "Microsoft.Sql/servers",
      "location": "[resourceGroup().location]",
      "tags": {
        "displayName": "SqlServer"
      },
      "apiVersion": "2014-04-01-preview",
      "properties": {
        "administratorLogin": "[variables('administratorLogin')]",
        "administratorLoginPassword": "[parameters('sqlAdminLoginPassword')]"
      },
      "resources": [
        {
          "name": "[variables('testDatabaseName')]",
          "type": "databases",
          "location": "[resourceGroup().location]",
          "tags": {
            "displayName": "testDatabase"
          },
          "apiVersion": "2014-04-01-preview",
          "dependsOn": [
            "[concat('Microsoft.Sql/servers/', variables('sqlserverName'))]"
          ],
          "properties": {
            "edition": "[parameters('sqlTier')]",
            "collation": "[variables('collation')]",
            "maxSizeBytes": "[variables('maxSizeBytes')]",
            "requestedServiceObjectiveName": "[parameters('databaseSpec')]"
          }
        }
      ]
}

App service:

{
      "apiVersion": "2015-08-01",
      "name": "[variables('testSiteName')]",
      "type": "Microsoft.Web/sites",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Web/serverFarms/', variables('webAppServicePlanName'))]"
      ],
      "tags": {
        "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', variables('webAppServicePlanName'))]": "empty",
        "displayName": "Test_Website"
      },
      "properties": {
        "name": "[variables('testSiteName')]",
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('webAppServicePlanName'))]"
      },
      "resources": [
        {
          "apiVersion": "2015-08-01",
          "type": "config",
          "name": "connectionstrings",
          "dependsOn": [
            "[concat('Microsoft.Web/Sites/', variables('testSiteName'))]"
          ],
          "properties": {
            "Default": {
              "value": "[concat('Data Source=tcp:', reference(concat('Microsoft.Sql/servers/', variables('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', variables('testDatabaseName'), ';User Id=', variables('testDBUserName'), '@', variables('sqlserverName'), ';Password=', parameters('sqlAdminLoginPassword'), ';')]",
              "type": "SQLServer"
            }
          }
        },
        {
          "name": "appsettings",
          "type": "config",
          "apiVersion": "2015-08-01",
          "dependsOn": [
            "[concat('Microsoft.Web/sites/', variables('testSiteName'))]"
          ],
          "tags": {
            "displayName": "AppSettings"
          },
          "properties": {           
            "StorageAccount.BlobRootUrl": "",
            "StorageAccount.ConnectionString": ""            
          }
        }
      ]
}

Infrastructure as code and DevOps

Development can take anywhere from weeks to months, depending on the size and complexity of the solution, but due to our own infrastructure planning and deployment processes – backed by code – I can be confident that the developers are armed with everything they need to do their job. Furthermore, I know that it can handle whatever is thrown at it, through a series of usability, security, functional and stress tests, using a consistent and familiar interface, and accompanying toolset.

If solution infrastructure requirements change, the idempotent nature of ARM templates makes any addition to template code both easy and non-destructive. Only changes to the template code are deployed, with existing resources omitted, ensuring that resources and their configuration remain in their original form until explicitly defined otherwise within the template.

Whether you are a developer, systems administrator, or engineer living in this DevOps world, in all of my nineteen years in IT, I don’t ever recall the creation and management of infrastructure being this much fun. In fact, it hasn’t even come close.