Chapter 12: Exploring Best Practices for Future Maintenance

This is the last chapter of this book, and I am very happy you are reading it. This means you now know how to create great templates and help your team deploy your infrastructure via code with great confidence.

However, there are a few more topics we need to go through to make sure you always keep yourself and your team ahead of the curve since, in the cloud world, everything moves faster than any other industry. We will cover topics including best practices in IaC, idempotency in deployments, modularity, and some best practices when it comes to Bicep. We will finish this chapter by pointing out the current limitations in Bicep that you need to take into consideration.

In this chapter, we are going to cover the following main topics:

  • Applying version control and code flows for your IaC
  • Idempotency and its importance
  • Modularity and microservices
  • Bicep best practices
  • Managing service version updates
  • Azure Bicep limitations

Technical requirements

This chapter does not have any technical requirements. You can find the samples code for this chapter at https://github.com/PacktPublishing/Infrastructure-as-Code-with-Azure-Bicep/tree/main/Chapter12.

Applying version control and code flows for your IaC

When it comes to IaC, there are many patterns you can leverage. We will review the two most important ones and review the pros and cons of each.

Single pipeline approach

The first approach is to put all the steps to deploy the infrastructure and code in the same pipeline. This has some benefits, such as making sure the environments and code are compatible by testing the IaC before deployment, which gives you great confidence in the result.

However, this has some drawbacks, including the fact that every time there is a code change or a change in the environment, all the steps will run, which makes every run longer than it should be. This also means that pull request validations will take longer, so you might feel a little slowness in your developers getting tasks done.

Separate pipelines for code and infrastructure

A much better approach is to have separate pipelines for code and infrastructure. This means that code changes will only trigger a pipeline and changes to cloud resources will only trigger its pipeline.

Another great aspect of this approach is the speed of testing and deployment since everything is separate. You will need to make some extra effort to implement this approach by using some extra configuration for your pipelines, such as a path filter and trigger exclusions, but I can assure you that the result is worth it.

There are some drawbacks to this approach too, which include more work upfront and deployment complexity. If you need to access the infrastructure deployment outputs in your code pipeline, you need to add some extra tasks and use one of the approaches we talked about in the last two chapters.

It also makes it difficult to make sure cloud resource changes do not break the application or vice versa, which requires integration testing to be written. But again, the result will make your team agile, confident, and in full control of the environment.

Source control everything

Last, we would not be talking about pipelines if we did not have source control in place. Having your templates in source control makes it easier to keep track of the changes that have been applied and roll back to a previous good state in case of failure. Apart from that, the reusability that is created will let other teams within your organization benefit from collaboration with your team and vice versa. Now that we have talked about IaC, it is time to review idempotency in your deployments.

Idempotency and its importance

Idempotency means no matter how many times you run your pipeline or what the current state of your cloud resources is, you always get the same result at the end of your deployments.

Idempotency

For example, if your template is deploying a storage account in the first run, and then an additional storage account if you run it again, it is not idempotent. It is very important to keep your deployment idempotent to prevent any change in behavior, which makes troubleshooting a nightmare.

Immutability

Immutability is also something that you need to consider. Not only it is important to make sure your templates provide the same result at each run, but the configuration of your resources must be maintained if no change has been recorded.

These issues usually happen when you are not as confident about your cloud deployment as you are about your code and as such, your applications might suffer from slow memory, run out of disk space, and more. To prevent this, you need to make sure your resources are immutable, which means instead of modifying your existing resources, you must deploy a set of new resources.

Of course, this is not applicable in every scenario, especially since it usually requires provisioning additional resources to route the traffic accordingly if a resource is replaced with a new one.

Modularity and microservices

Modularizing your templates is essential, especially if your cloud environment is complex. This will help your code be more maintainable, your templates more readable, and resource ownership easier.

Imagine an organization with different teams for networking and applications. If each team takes ownership of their corresponding resources, the risk of any misconfiguration or incorrect resource structure will be minimized. Keep in mind that this will not undermine the concept of cross-functional teams, since the point here is keeping resources in modules that are related and can be deployed together. Furthermore, it may only be the templates that are generated by the corresponding team and shared for other teams for reuse for their deployments.

This will also make your templates easier to read since you now have references to modules instead of repeating your code over and over again. Not only that, but it also makes your templates a long and hard-to-maintain document, which scares every developer in your team when it comes to making any changes to it.

Apart from that, this approach makes it easier for you to implement a microservice architecture since you can break down your apps into smaller services and reuse the module to deploy them.

Now, it's time to review some best practices in Bicep that will help you create better templates and modules.

Bicep best practices

Like any other IaC tool, Bicep requires some discipline to make sure that if you look at your templates in a few years, when the original developers might have left the team, the new members can still read and modify them without any issues.

Parameters

Starting with parameters, the very first recommendation is to choose a good naming convention, document it, and stick to it. Make sure your resource names are clear, it's easy to pinpoint their role, and they are easy to find. Keep all the parameter definitions at the top of the template to make it easier to review, add, or remove them in the future. You can also refer to https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming to find some of the most used conventions and best practices published by Microsoft.

Limit the values for your parameters using the @allowed decorator to reduce the risk of a user passing an invalid or impractical value to your template. Provide a minimum and a maximum for those parameters where there are limitations enforced by Azure to prevent any errors at runtime and catch them upfront.

Variables

Similarly, use a good naming convention for variables, define them where applicable and where they are easy to find, and use functions for more complex formulas rather than inline expressions. Also, use camel case for their naming to make reading them easy and coming up with one easier.

Use the uniqueString() function to generate unique values for your variables instead of trying to come up with a random value yourself, which increases the risk of facing an issue later.

Resource definitions

When you're defining your resources, choose a good naming convention for symbolic names too. Try avoiding complex expressions and use variables or functions for those situations. If you are using resource properties later, try not to populate them yourself. Instead, use their output properties, which give you the exact values.

When possible, avoid the reference and referenceId functions and use symbolic names instead. Use the existing keyword if your resource has not been created in this template, and then use the symbolic name to access its properties in the child or dependent resources.

Avoid nesting too many resources, bring them to the top level, and set their parents using symbolic names. This makes your template much easier to read and maintain.

Outputs

Avoid outputting sensitive information such as keys and secrets. Only output what might be used later in your pipeline or other dependent ones. Choose a naming convention for your outputs too and stick to it. If you need to access the properties of previously created resources, use the existing keyword, and define them in your template. They will not be created in your ARM template but they will make your job and reading your template much easier.

Configuration set

Rather than defining lots of individual parameters, create predefined sets of variables and select the one you need during your deployments. For example, imagine that we need different SKUs for production versus non-production resources. You can create a variable such as the following:

var environmentConfigurationMap = {

  Production: {

    appServicePlan: {

      sku: {

        name: ‘P2V3'

        capacity: 3

      }

    }

    appServiceApp: {

      alwaysOn: false

    }

  }

  NonProduction: {

    appServicePlan: {

      sku: {

        name: ‘S2'

        capacity: 1

      }

    }

    appServiceApp: {

      alwaysOn: false

    }

  }

}

Then, pass a single parameter that defines which environment is being deployed. Based on the value of this parameter, choose the corresponding part of the variable:

@allowed([

  ‘Production'

  ‘NonProduction'

])

param environmentType string = ‘NonProduction'

resource appServicePlan ‘Microsoft.Web/serverfarms@2020-06-01' = {

  name: appServicePlanName

  location: location

  sku: environmentConfigurationMap[environmentType].appServicePlan.sku

}

With this approach, you will not need to depend on the user to know which SKU to use for which environment, and everything is kept simple and effective in your source control. Just try to group the properties by resource to simplify the definitions. This enables you to define both individual property values and object values.

You can mix this approach with resource conditions to create even more powerful templates.

Shared variable file

Instead of repeating variables in each template when they need to be shared, try loading them from a shared JSON file within your template. This approach simplifies your template even more and allows you to keep a single source of truth for your variables.

To use this approach, simply create a JSON file like the following:

{

    “storageAccountPrefix”: “stg”,

    “appServicePrefix”: “myapp”

}

Then, in your template, use the json function in combination with the loadTextContent function to load the file. Then, simply access the variables defined in the file:

var sharedNamePrefixes = json(loadTextContent(‘./shared-prefixes.json'))

var appServiceAppName = ‘${sharedNamePrefixes.appServicePrefix}- ${uniqueString(resourceGroup().id)}'

var storageAccountName = ‘${sharedNamePrefixes.storageAccountPrefix}myapp${uniqueString(resourceGroup().id)}'

Keep in mind that when you use this approach, the JSON file will be included within the ARM template, and with the maximum file size being 4 MB, it is important to use it wisely and when necessary.

Managing service version updates

Microsoft Azure is always changing for the better. Services get updated and new versions get released, sometimes daily. To keep yourself ahead of these changes, you can use the Azure Updates tool to subscribe to and receive any update that is made to Azure services as soon as possible. You can find the tool at https://azure.microsoft.com/en-us/updates/.

This will allow you to have control and make informed decisions on which version of the service to use in your Bicep templates.

Microsoft publishes best practices that get updated as the product evolves. You can read more on this at https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices.

Azure Bicep's known limitations

Like any other tool, Bicep has some limitations, and considering how new it is, it does make sense for some of these issues to have not been addressed yet. So, let's see what these limitations are and where to find more information about them.

Single-line objects and arrays

The first known limitation is that there is no support for single-line objects and arrays. You might have used an array to create multiple resources using copy in your ARM templates, in which case you would have passed an array parameter (that is, [‘a', ‘b', ‘c']). You cannot have the same in Bicep, but this can be done using a new line:

allowed: [

  ‘Basic'

  ‘Standard'

  ‘Premium'

]  

Work is in progress to add support for this, and you can find updates in their GitHub repository at https://github.com/Azure/bicep/issues/498.

Newline sensitivity

Bicep lang is newline-sensitive, which means that if you accidentally insert a new line in your file, the interpretation will be different. Let's see what this means by looking at an example. Say you want to write an if condition to create a variable value. You might write it like so:

var value = myParam == ‘value1' ? settingsTable.value1 :

            myParam == ‘value2' ? settingsTable.value2 :

            {}

If you use this code snippet in your Bicep file, you will get a compilation error since this means something completely different in Bicep. In Bicep, you need to write this in a single line:

var value = myParam == ‘value1' ? settingsTable.value1 : myParam == ‘value2' ? settingsTable.value2 : {}

Note

Do not pay attention to how the correct form is wrapped – this is just because of a lack of horizontal space on this page.

This was supposed to be on the v0.2 milestone but it got moved up to v0.5 later. You can find more information on their GitHub issue at https://github.com/Azure/bicep/issues/146.

No support for API profiles

API profiles specify the Azure resource provider and its API version, which should be used on the ARM REST endpoints. Using this feature, you can create different clients in different languages, which will use different versions of APIs. This might be for backward compatibility reasons or simply being behind the latest version. Did this sound complex? Let me clarify by providing an example. Normally, you would need the resource type and its API version to tell ARM what to create:

‘microsoft.compute/virtualMachines@2020-06-01'

This is important for Azure Resource Manager, but why confuse users when you can simplify it if they do not care what version is used to create their resource?

‘microsoft.compute/virtualMachines' => virtualMachines or vm

Azure Bicep does not support this yet and the team is working on it. However, they have mentioned that it might be implemented differently in Bicep since it is more complex than it looks. You can find more information on their GitHub issue at https://github.com/Azure/bicep/issues/622.

CLI limitations

For now, there are two temporary limitations with the CLI and they only occur during compilation. The first is that templates that use copy cannot be decompiled. The other is the templates that use nested templates can only be decompiled if they are using the inner expression evaluation scope. If you are not familiar with this concept, do not worry about it – I will give you an example to make it clear what this limitation is about.

When using nested templates, you can explicitly specify whether the expressions are evaluated at the parent or child level using the expressionEvaluationOptions flag:

{

  “type”: “Microsoft.Resources/deployments”,

  “apiVersion”: “2020-10-01”,

  “name”: “nestedTemplate1”,

  “properties”: {

    “expressionEvaluationOptions”: {

      “scope”: “inner”

    },

  ...

And that is all you need to add to your templates to transpile them successfully. However, keep in mind that the scope has changed for your variables, so you might need to make the necessary adjustments. Now, it's time to recap what we reviewed in this chapter and wrap up this book.

To find out more about these limitations and keep up to date, go to https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview#known-limitations.

Summary

In this chapter, we discussed many topics, from IaC best practices to what idempotency and immutability are when it comes to IaC. This was followed by an overview of modularity and how it helps you implement microservices patterns, and then some of the best practices when it comes to Bicep that cover ideas and industry standards as regards the use of parameters, variables, resources, outputs, and some useful patterns such as configuration settings and variables files.

Finally, we reviewed some of the known limitations of Azure Bicep since the tool is new and it is natural for some of these to have not been addressed yet.

I am so glad that you have come so far reading this book. Hopefully, it has helped you not only get started with Bicep but become an expert in creating reusable, modular templates, which will help your team and others in your organization. If anything changes in this space, I will make sure to update this book, so keep an eye out for some upcoming announcements.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.210.133