Terraform offers lots of flexibility in coding Infrastructure as Code (IaC) deployments. Using Feature Flags and Environment Toggles can help in increasing the flexibility of the Terraform code to better handle multi-Environment deployments. They can also be used to conditionally deploy certain resources and configurations. These are techniques and design patterns that are greatly helpful to better control what resources are deployed and making multi-Environment maintenance easier to handle.
Let’s get started!
Table of Contents
Related: If you’re new to Terraform, or are looking for a bit more information, then we recommend you check out our “Get Started with Terraform on Azure” article. This will give you a great jump start to both using Terraform for infrastructure automation, as well as some tips on using it with Microsoft Azure.
Why use Feature Flags and Environment Toggles?
Feature Flags and Environment Toggles are very common techniques in software development that enable you to implement switches in software so that the code can be released into production but keep certain featured disabled until they are ready to be used. This can help hide features until public availability, or even keep them turned off until they are fully tested and debugged. These techniques allow you to release code to production more quickly and often while maintaining a single source code branch (such as main
or master
).
Then when the features are ready, you simply turn them on as the code is already out there. This can be done by modifying a configuration file or a build pipeline without requiring the source code to be completely rebuilt. In traditional CI/CD (Continuous Integration / Continuous Deployment) workflows, you can perform a promotion strategy of releasing / deploying the code up through the various environments; such as DEV -> TEST -> STAGE -> PROD. By reusing the same binaries and flipping the feature flags and environment toggles on the releases, the deployment process will incur less risk of file corruption or accidentally deploying untested / unproven code.
The same feature flag and environment toggles can be used in an Infrastructure as Code (IaC) deployment for managing Infrastructure using a tool like Terraform to borrow some of the software development best practices to help build more reliable and flexible infrastructure deployments.
Implement Terraform Feature Flags
Terraform uses the HashiCorp Configuration Language (HCL) as a programming language to declaratively define the configuration of an Infrastructure as Code (IaC) deployment. As with any other program, it has the ability to define variables, loops, and conditions; among other features built into the language. These features enable the ability to build out IaC that can be built to share code with and be flexible when deploying to multiple environments. This is done by implementing feature flags in the code.
Feature flags enable you to flip certain configurations within the Terraform project, or even pass in variables to the project on deployment to customize the IaC being deployed to meet environment or even multi-tenant requirements. Depending on the infrastructure needed, you can use feature flags with the main Terraform project or within Terraform modules used by one or more Terraform projects.
Feature flags in Terraform enable you to conditionally deploy certain resources within the project by configuring individual flags that can be configured differently by environment or even to turn of deploying resources for something that isn’t ready to be deployed yet. This is a great way to build a single Terraform project with a single code base to be used to provisioning multiple environments that may need to be different in some fashion.
Resource Feature Flags
Resource blocks in Terraform are used to define the infrastructure deployment for specific resources based on the specific Terraform resource provider being used. One easy way to define Feature flags to be used is to add them as local
variables in the Terraform project. This enables a sort of static feature flag to be configured and then checked into source control with the rest of the Terraform project.
To define the feature flag, the typical approach is to define a single, local
boolean variable for the feature the needs to be wrapped in a feature flag. Then you can set the feature flag variable to either true
or false
to turn the feature on or off in the project. This can be used to easily write the Terraform for infrastructure that’s needed, but keep the feature flag set to false
to remain “turned off” until the work is finished or it needs to be deployed when the Terraform project is applied via the terraform apply
command.
The local
variable for a feature flag can be defined as follows:
locals {
provision_b59func = true
}
Next, you can use the new feature flag as a condition to determine whether to deploy certain resources defined within the Terraform project. However, Terraform does not contain a sort of provision = local.provision_b59func
syntax. Instead, you need to use the count
parameter on the Terraform resource. Then with the count
parameter, you can use a condition to tell Terraform conditionally provision either 1
or 0
of the declared resource based on the feature flag variable.
Here’s an example of adding the count
parameter on a Terraform resource to conditionally declare to provision the resource based on the feature flag.
resource "azurerm_function_app" "b59func" {
# Conditionally based on feature flag
count = local.provision_b59func == true ? 1 : 0
# If 'true', provision 1 of this resource
# If 'false', don't provision this
[...]
}
With the count
parameter, the condition can be configured to check if the feature flag is true
and set the count
to 1
for provisioning the resource. Then, also, when the feature flag is set to false
the Terraform will be declared to skip provisioning the resource by setting the count
to 0
(zero).
Module Feature Flags
When building out a Terraform IaC project, you can break out your Terraform code into modules. This essentially allows you to group multiple Terraform resources into a reusable block of HCL code. You can also think of a Terraform module as a function or method of Terraform code; to borrow terms from software development.
To implement Terraform modules, you will place the Terraform code (.tf
) files within a sub-folder or separate folder. Then to reuse the module within you Terraform code, you declare a Terraform module
block and use the source
argument to tell Terraform where to find the .tf
code files that declare the Terraform resources to be deployed for the module.
Here’s an example folder structure for a really simple Terraform project with a module define within the modules/azure-function
sub-folder:
Example Terraform Project w/ Module
├── deploy.tf
├── modules
├── azure-function
├── deploy.tf
In this example, the Terraform project has a single deploy.tf
file for simplicity. Most Terraform projects will be comprised of multiple .tf
files. Then the modules
subfolder is used to contain any additional modules defined within the project. This example shows the modules/azure-function
subfolder to define a module for deploying one or more resources as a reusable block of Terraform code.
The modules/azure-function
module folder contains the Terraform code for this module. This code will contain all the Terraform code to declare one or more resources this module will be used to deploy and manage. In order to support a feature flag to be passed into the module, the Terraform code will also need to declare a variable to be passed in.
The feature flag value will be passed into the module by declaring an input variable
for the module. This will allow the consuming Terraform project to pass in the desired feature flag value to conditionally deploy resources declared within the module.
Here’s a short example for some Terraform code that could be contained within the example module/azure-function
module. Notice the code includes a similar condition on the resources based on the feature flag using the count
argument, in addition to the variable
declaration for the to_provision
feature flag of type bool
to accept the feature flag.
# Terraform Module: /modules/azure-function/deploy.tf
variable "to_provision" {
type = bool
default = true
}
resource "azurerm_function_app" "b59func" {
# Conditionally based on feature flag
count = var.to_provision == true ? 1 : 0
# If 'true', provision 1 of this resource
# If 'false', don't provision this
[...]
}
resource "azurerm_storage_account" "b59storage" {
# Conditionally based on feature flag
count = var.to_provision == true ? 1 : 0
# If 'true', provision 1 of this resource
# If 'false', don't provision this
[...]
}
Next, to consume and utilize the module within the main Terraform project, a module
block is declared using the source
argument to tell Terraform what folder contains the source code for the module being used.
locals {
provision_b59func = true
}
module "b59func" {
source = "modules/azure-function"
to_provision = local.provision_b59func
}
The module
block does not use the count
argument directly. This may be supported in future versions of Terraform, but at this time count
is not supported on modules. As a result, you’ll need to declare an input variable on the module that will be used to pass in the feature flag value with the deployment conditions implemented within the modules code.
Organize Feature Flags within locals
When implementing multiple feature flags within the same Terraform project, it can help to organize them within a group. You can optionally create a local
variable that is an object map with individual properties within the object for each feature flag.
Here’s an example of declaring a local.feature_flags
variable to contain a couple of feature flags:
locals {
feature_flags = {
provision_b59func = true
provision_b59iothub = false
}
}
Grouping feature flag variables under a local
object map can help increase clean code practices within the Terraform project by consolidating them all in a single place. It can also help to keep the names of the feature flags from conflicting with the names of other variables declared within the project.
The same method is still used to reference the local
object map-based feature flag variables within Terraform code. The only slight difference is that you need to reference them as properties of the “parent” local
object.
Here’s an example of referencing the local
object map-based feature flags:
resource "azurerm_function_app" "b59func" {
# Conditionally based on feature flag
count = local.feature_flags.provision_b59func == true ? 1 : 0
# If 'true', provision 1 of this resource
# If 'false', don't provision this
[...]
}
Implement Terraform Environment Toggles
An Environment Toggle is really just another type of Feature Flag that is used to setup deployment conditions based on the environment being deployed. This allows for the exact same Terraform code to be used to deploy and manage multiple different environments through environment specific switches that are passed in at deployment time.
When writing Terraform code for multiple environments, the simple solution would be to create a new Terraform project for each Environment. This will work just fine at first, however, as the project grows it will become increasingly difficult to manage all the environment projects. This is especially true as the list of deployed environments grows from the initial “Development” and “Test”, to including “Stage”, “Production” and even more environments beyond that.
Having separate Terraform projects for each deployed Environment will require N+1 code bases to be managed as you deploy out all the various environments your project / organization needs. By implementing Environment Toggles, you will create a single code base to manage and simply add conditions within it based on the Environment being deployed to enable certain infrastructure deployment and configurations to be made when deployed.
Pass in Varables to plan
and apply
commands
The main difference with Environment Toggles is they are passed in to to the Terraform project at the time it’s deployed. More specifically, the Environment Toggle value is passed to the terraform plan
and terraform apply
commands.
To pass in an Environment Toggle to the terraform plan
command you will use the -var
argument to pass in the value with the toggle name and value separated by an equal sign in the format of [toggle-name]=[value]
.
Below is an example of passing in a variable to the terrafrom plan
command with the name of environment
. This is an easy way to passing a single Environment Toggle / variable to the deployment for the environment being deployed. However, keep in mind that you can also pass in multiple variable to the command using this method as well.
terraform plan \
-var 'environment=dev' \
-out deploy.tfplan
Additionally, you can use the exact same -var
argument on the terraform apply
command to pass in Environment Toggle variables as well:
terraform apply \
-var 'environment=dev'
Passing in the Environment Toggle variable to the plan
and apply
commands also require the Terraform project to be configured to use the variable. This can be done by adding a variable
block to the Terraform project defining the variable name and value that the Terraform project is to expect to be passed in.
variable "environment" {
type = "string"
description = "The Environment being deployed to"
}
The example above is showing a variable passed in named “environment”. This can be used to pass in a single Environment Toggle variable to tell the Terraform project which environment is being deployed. Keep in mind that you can always define and pass in multiple input variables to pass into the Terraform project in this way. You could keep the environment specific variable and additionally pass in other feature flag variables to implement the custom set of feature flags and environment toggles necessary for your own Terraform solution.
terraform apply \
-var 'environment=dev' \
-var 'provision_b59func=true'
Conditionally Provision Based on Environment Toggle
The Terraform input variables pass in to the apply
and plan
commands can be referenced in the Terraform code in a similar fashion to the local
variables. It can be done using the var
keyword to base conditions on; like the following:
resource "azurerm_function_app" "b59func" {
# Conditionally based on feature flag
count = var.environment == true ? 1 : 0
# If 'true', provision 1 of this resource
# If 'false', don't provision this
[...]
}
This is a great way to write the Terraform code in the project so that it deploys different resources and configures them according to the particular environment being deployed.
Array Values and Conditional Lookup based on Feature Flags
The use of Feature Flags (and Environment Toggles) is not just limited to simple value types. These can also be used to create custom configuration of Terraform resources based on input variables and local
variables. This is where a little bit of programmability comes into play within the Terraform project source code.
One particular area where this can be used is with array
value types. Some of the properties on Terraform services will accept an array of values. The normal path is hard coding these value arrays, but what if you need to conditionally configure the array custom for the specific environment being deployed?
Creating arrays conditionally based on Environment Toggles or Feature flags can be done easily by creating a local
variable with its value set to an Object with property/value pairs with the property name being the Environment Toggle / Feature Flag value that will be used to select which property value to select conditionally. Also, the value of the property will be set to the value needed; such as an array.
Let’s take a look at configuring an Array variable conditionally based on an Environment Toggle!
Define local
variable with default value for configuration
To start, we’ll want to create a local
variable that contains the default value that will be needed for the Terraform resource property being configured. Here’s a local
variable with it’s value set to an Array:
locals {
# Define Default IP Allow List to apply to all environments
default_ip_allow_list = {
"10.50.0.1",
"10.50.0.2"
}
}
Create local
Variable Object Map of Values to Select
Next, we’ll need to create a local
variable with its value set to an Object with property/value pairs that match each of the possible Environment Toggle (or Feature Flag) values that are expected to be used when running the terraform plan
and terraform apply
commands.
Here’s an example of declaring a local
variable with properties for each fo the dev
, test
, and prod
values that will be passed in for the Environment Toggle variable:
locals {
# Define map of IP Allow List arrays for
# different Environments you'll be deploying to
ip_allow_list_environment_map = {
dev = local.default_ip_allow_list,
test = local.default_ip_allow_list,
prod = local.default_ip_allow_list
}
}
Notice that each of the property values are set to the same local.default_ip_allow_list
variable. This can be used as the base to provide the initial configuration for each Environment set to the default array value needed.
Then you can customize the value for the various environments as necessary for each environment’s requirements. This can be done by directly setting the environment value directly to the value needed; such as Array in this example case. With an Array value, there is another option of using the concat
method to take the default value as the base and combine it with another Array of values.
Here’s an example of extending the local
variable Object map with an additional Array of values for one of the Environments:
locals {
# Define map of IP Allow List arrays for
# different Environments you'll be deploying to
ip_allow_list_environment_map = {
dev = concat(local.default_ip_allow_list, [
"10.51.0.1"
])
test = local.default_ip_allow_list,
prod = local.default_ip_allow_list
}
}
This usage of the concat
method to combine the default Array values with another set of values will enable you to create a type of “inheritance” of Array values for a particular environment. The default Array will contain the values that are needed for all environments being deployed, and the concat
method will add on the additional values needed for the environment being setup.
Conditionally Lookup Value to Use
Finally, you need to conditionally lookup the particular Object map property value based on the Environment Toggle (or Feature Flag). This can be done using the lookup
method that will pick the property value from the Object map variable with the name of the property that matches the Environment Toggle (or Feature Flag) value.
Now that the Object map and default variables have been define, the code needs to be set up to conditionally select the value to use based on the Environment Toggle or Feature Flag being used. This can be done using the lookup
method in Terraform to pass in the Object map value and the Environment Toggle value to use to select the required value from the Object map. Also, the last parameter of the method passed in is the default value to use in case the Environment Toggle value isn’t configured within the Object map; basically, the catch all in case it’s not defined.
locals {
# Conditionally determine which IP Allow List
# to use for the specified Environment
ip_allow_list = lookup(
# object map of values to choose
local.ip_allow_list_environment_map,
# Environment Toggle
var.environment,
# Default values for edge cases
local.default_ip_allow_list
)
}
The above example is using the lookup
method to conditionally choose the Object map value based on the Environment Flag passed. It also sets the result of that selection to the value of another local
variable that can be used throughout the Terraform project as necessary. Setting the result of this lookup to another local
variable allows this variable to be used multiple time within the project. If you only need to use the value once, then you could just set the result of the lookup
method call directly to the Terraform resource property that uses it as an alternative.
resource "azurerm_key_vault" "b59vault" {
[...]
network_acls {
[...]
# Set property based on local variable
ip_rules = local.ip_allow_list
}
[...]
}
Full Array Values and Conditional Lookup Example
Here’s a full Terraform code example of setting up an Array value and conditional lookup based on an Environment Toggle or Feature Flag. Hopefully, this example puts all the pieces together to make setting this all up clearer.
locals {
# Define Default IP Allow List to apply to all environments
default_ip_allow_list = [
"10.50.0.1",
"10.50.0.2"
]
# Define map of IP Allow List arrays for
# different Environments you'll be deploying to
ip_allow_list_environment_map = {
# Combine Default IP Allow List with additional
# IP Address to add to the IP Allow List
# for 'dev' environment
dev = concat(local.default_ip_allow_list, [
"10.51.0.1"
])
test = local.default_ip_allow_list
prod = local.default_ip_allow_list
}
# Conditionally determine which IP Allow List
# to use for the specified Environment
ip_allow_list = lookup(
local.ip_allow_list_environment_map,
var.environment,
local.default_ip_allow_list
)
}
Wrap Up
Feature Flags and Environment Toggles are a great way to conditionally change the behavior of code at runtime. These are common practices performed in software development and are a great carry over to Infrastructure as Code (IaC) solutions like Terraform. These enable you to conditionally deploy and manage resources based on input variables and local variables within the code and will help simplify your Terraform projects. Using these design patterns will enable you to reduce the amount of Terraform code necessary to build and maintain your multiple deployed environments.
Keep in mind that Terraform Modules are a great way to break out code to be more reusable and can help with simplifying Terraform projects as well. Also, by combining Feature Flags and Environment Toggles with Modules can offer a lot of flexibility for coding multi-Environment deployments using Terraform.
Count, depends_on and for_each are supported in modules in Terraform v0.13
Yes this is accurate. The newer version releases of Terraform are adding more features. Although, it’s great to still know how to perform Feature Flag and Environment Toggle capabilities across multiple Terraform releases; including older versions as not everyone using Terraform is always on the latest version. Thanks for pointing this out!
Excellent write up thank you!
Also we need to separate a state files for each environment…
Nice article – one conceptual difference about applying feature flags to TF versus applications is the lack of a constant runtime to check for flag changes. An application in a CD model is deployed to production with every code push, and flags act as a release mechanism, changing the behavior of the running app. But TF flags would only impact the deployment itself, so while the “deploy on every push” model would technically work, changing a flag necessitates a new plan/apply cycle. I’m curious how you have dealt with this in your efforts to use flags in TF code? What do your working processes look like?
You run the deployment as needed. When a feature flags needs to change, then you change it and rerun the Terraform plan/apply to make the change to the environment.
Let’s say you are using a single main branch for all your environments as mentioned in this document, and you are performing a major upgrade to a publicly available module.
The newer module (version2) has deprecated some of the inputs that were previously available in the older module (version1).
Using a single branch for all your environments… seeing that the input parameter for version1 module no longer exist in version2 module, would cause terraform plan to fail when in the env trying to use the version2 module
I don’t believe you can write a condition that would allow you to toggle off a parameter being passed to a module. What would the solution here be? Ideally, not having to modify the publicly available module.