Terraform
Deploy the Microsoft Cloud Adoption Framework enterprise-scale module
Microsoft's Cloud Adoption Framework (CAF) for Azure is a collection of documentation, tools, and best practices that guide organizations through cloud adoption. The framework sets up landing zones, which are pre-configured environments where you can host your workloads. These landing zones implement best practices in governance, networking, and identity management that allow you to securely scale your workloads. There are two strategies to implement the Cloud Adoption Framework in your organization:
- Start with enterprise scale to deploy fully integrated landing zones with governance, security, networking, and more.
- Start small and expand to deploy landing zones that have the basic landing zones considerations, these include compute, storage, networking and database decisions. You can supplement these landing zones with governance and management layers in the future.
Each strategy includes a corresponding Terraform module to help you deploy resources according to the strategy guidelines. Every organization is unique, so you must consider different trade-offs of each approach when you deploy landing zones.
In this tutorial, you will use the caf-enterprise-scale Terraform module to:
- deploy the core and demo landing zones. The core landing zones manage and organize user workloads to provide the services and governance that the CAF recommends. The demo landing zones represent sample landing zones that your infrastructure team would deploy to give downstream teams access to infrastructure for their workloads.
- deploy a custom landing zone. This simulates a workflow you can use to deploy landing additional, CAF-compliant infrastructure for teams within your organization.
- add logging and security with the management submodule.
- create subnets, DNS zones, and policies with the connectivity module.
In the process, you will review the module configuration to help you tailor the module to your organization's priorities and enable sustainable scale.
Prerequisites
This tutorial assumes that you are familiar with the standard Terraform workflow. If you are new to Terraform, complete the Get Started tutorials first.
For this tutorial, you will need:
- the Terraform 1.0.4+ CLI installed locally.
- an Azure account with one or more Subscriptions.
- a configured Azure CLI.
Clone the example repository
In your terminal, clone the example repository.
$ git clone https://github.com/hashicorp-education/learn-terraform-microsoft-caf-enterprise-scale Navigate to the cloned repository.
$ cd learn-terraform-microsoft-caf-enterprise-scale Explore the configuration
This repository contains the configuration to deploy the core and demo landing zones according to the Cloud Adoption Framework guidance for the enterprise scale strategy. The module will deploy the demo landing zones in the Landing Zones management group that are part of the core landing zones.
Here, you will find the following files:
the
terraform.tffile defines the provider versions this configuration uses. The module requires anazurermprovider version2.77.0or greater.the
.terraform.lock.hclfile ensures that Terraform uses the same provider versions for each run.the
providers.tffile defines the three Azure providers this configuration uses. This configuration supports multi-subscription workloads, so that different teams can manage each submodule. If you do not provide a subscription ID (as a Terraform variable) for management or connectivity, the management and connectivity Azure providers default to the current subscription.the
main.tffile configures thecaf-enterprise-scalemodule. This configuration deploys the core and demo landing zones in the current subscription and thedefault_location. Thedefault_locationdefaults touseast, as defined byterraform.tfvars.example.the
client.tffile contains data sources that retrieve your current subscription ID.the
variables.tffile declares the configuration's input variables. Each variable has a description and a default value.the
terraform.tfvars.examplefile defines values for the variables declared in thevariables.tffile.the
locals.tffile converts the input variables to local values. Assigning input variables to local values enables you to modify the values before you use them throughout the configuration.Take note of the
subscription_id_managementandsubscription_id_connectivitylocal values.locals.tf
locals { ## ... subscription_id_management = coalesce(var.subscription_id_management, data.azurerm_client_config.current.subscription_id) subscription_id_connectivity = coalesce(var.subscription_id_connectivity, local.subscription_id_management) }This configuration supports multi-subscription workloads, but uses the
coalesce()Terraform function to default to your account's default subscription ID if you do not set thesubscription_id_managementandsubscription_id_connectivityinput variables.
Deploy enterprise-scale resources
The caf-enterprise-scale Terraform module provides an opinionated way to deploy and manage the core platform capabilities defined in the enterprise-scale landing zone architecture documentation. The module creates the core resources, which include management groups, policies, and roles, that make it easier to organize and manage your other landing zone. In addition, the module includes three submodules; each one creates management group(s) with policies scoped the resources they manage:
- The management submodule adds a central Log Analytics workspace and Automation Account and enables Azure Security Center.
- The connectivity submodule creates guidelines for networks by defining common policies. In addition, this submodule creates an Azure Firewall and configures subnets and DNS zones.
- The identity submodule creates policies to manage compliance and security.
The core landing zones create management groups for Decommissioned, Landing zones, Platform, and Sandboxes. The CAF recommends these landing zones to manage and organize your workloads. You can also configure the module to provision Online, Corp, and SAP demo landing zones. These landing zones represent ones that your organization may set up to migrate your workloads to Azure.
Deploy the core and demo enterprise scale landing zones.
First, rename the terraform.tfvars.example to terraform.tfvars.
mv terraform.tfvars.example terraform.tfvars Then, in terraform.tfvars, replace the security contact email address with your email address.
terraform.tfvars
security_contact_email_address = "security.contact@replace_me" Next, initialize the Terraform configuration.
$ terraform init Initializing modules... Downloading Azure/caf-enterprise-scale/azurerm 1.0.0 for enterprise_scale... - enterprise_scale in .terraform/modules/enterprise_scale - enterprise_scale.connectivity_resources in .terraform/modules/enterprise_scale/modules/connectivity - enterprise_scale.identity_resources in .terraform/modules/enterprise_scale/modules/identity - enterprise_scale.management_group_archetypes in .terraform/modules/enterprise_scale/modules/archetypes - enterprise_scale.management_resources in .terraform/modules/enterprise_scale/modules/management Initializing the backend... Initializing provider plugins... - Reusing previous version of hashicorp/azurerm from the dependency lock file - Reusing previous version of hashicorp/time from the dependency lock file - Installing hashicorp/azurerm v2.79.1... - Installed hashicorp/azurerm v2.79.1 (signed by HashiCorp) - Installing hashicorp/time v0.7.2... - Installed hashicorp/time v0.7.2 (signed by HashiCorp) Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. Finally, apply the configuration. It may take a while for Terraform to generate the execution plan — the caf-enterprise-scale module provisions 187 resources. Once prompted, respond yes to confirm.
$ terraform apply ## ... Plan: 187 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes ## ... Apply complete! Resources: 187 added, 0 changed, 0 destroyed. Review the configuration by reading the next section while you wait.
Review caf-enterprise-scale module
The current main.tf configuration deploys the core and demo landing zones by setting the following module arguments to true. The local.deploy_*_landing_zones are set to true in the terraform.tfvars file.
main.tf
module "enterprise_scale" { source = "Azure/caf-enterprise-scale/azurerm" version = "~> 1.0.0" ## ... # Control deployment of the core landing zone hierarchy. deploy_core_landing_zones = true deploy_corp_landing_zones = local.deploy_corp_landing_zones deploy_online_landing_zones = local.deploy_online_landing_zones deploy_sap_landing_zones = local.deploy_sap_landing_zones } In this section, you will review the caf-enterprise-scale module configuration to understand how the module arguments modify the architecture that the module provisions.
When Terraform initializes the configuration, it downloads the providers and modules used into the .terraform directory. For this configuration, Terraform stores a copy of the caf-enterprise-scale module in the .terraform/modules directory.
$ ls -la .terraform/modules/ total 8 drwxr-xr-x 4 dos staff 128B Oct 8 12:25 ./ drwxr-xr-x 4 dos staff 128B Oct 7 20:24 ../ drwxr-xr-x 35 dos staff 1.1K Oct 7 20:24 enterprise_scale/ -rw-r--r-- 1 dos staff 1.3K Oct 7 23:31 modules.json Terraform stores the module under the module name as the name defined in the main.tf file (enterprise_scale) and not the module source (caf-enterprise-scale).
main.tf
module "enterprise_scale" { source = "Azure/caf-enterprise-scale/azurerm" ## ... } This module uses local values and submodules to manipulate the input variables into data structures. The submodule configuration is in the modules directory, and the rest of the configuration primarily consists of two types of files:
- The
locals.*.tffiles parse and manipulate the variables. - The respective
resources.*tffiles use the local values to configure resources.
For example, the locals.management_groups.tf file creates the local values that resources.management_groups.tf consumes.
Review core and demo landing zones
The caf-enterprise-scale module uses a similar pattern for each of the landing zones you can provision. Review the configuration for the core landing zones; the deploy_core_landing_zones argument in the configuration toggles the core landing zones deployment.
Run the following command to find references of deploy_core_landing_zones in the .terraform/modules/enterprise_scale directory. It only searches files that end in .tf.
$ grep --include=\*.tf -rnw '.terraform/modules/enterprise_scale/' -e 'deploy_core_landing_zones' | grep -v "tests" .terraform/modules/enterprise_scale//locals.tf:15: ... .terraform/modules/enterprise_scale//variables.tf:39:variable ... .terraform/modules/enterprise_scale//locals.management_groups.tf:265: ... .terraform/modules/enterprise_scale//locals.management_groups.tf:266: ... .terraform/modules/enterprise_scale//locals.management_groups.tf:267: ... .terraform/modules/enterprise_scale//locals.management_groups.tf:270: ... This argument appears in three files: variables.tf, locals.tf, and locals.management_groups.tf.
The
.terraform/modules/enterprise_scale/locals.tffile sets thedeploy_core_landing_zoneslocal value to the value of the input variable. Assigning input variables to local values lets you modify the variables before you use them throughout the configuration.The
.terraform/modules/enterprise_scale/variables.tffile declares thedeploy_core_landing_zonesinput variable. It sets the default value totrue..terraform/module/enterprise_scale/variables.tf
variable "deploy_core_landing_zones" { type = bool description = "If set to true, module will deploy the core Enterprise-scale Management Group hierarchy, including \"out of the box\" policies and roles." default = true }The
.terraform/modules/enterprise_scale/locals.management_groups.tffile uses thelocal.deploy_core_landing_zonesto determine whether to assign a list of recommended landing zones.Find
es_core_landing_zones_to_include..terraform/modules/enterprise_scale/locals.management_groups.tf
es_core_landing_zones_to_include = local.deploy_core_landing_zones ? local.es_core_landing_zones : nullIf
deploy_core_landing_zonesis set to true, it setses_core_landing_zones_to_includelocal value to thees_core_landing_zoneslocal value. Otherwise, it sets the value tonull. This makes the module more dynamic, only provisioning the core landing zones if enabled.Find the
es_core_landing_zoneslocal value. This local value interpolates input information including theroot_nameandroot_idto create a data structure that defines customized core management groups for all of the CAF-recommended landing zones..terraform/modules/enterprise_scale/locals.management_groups.tf
es_core_landing_zones = { (local.root_id) = { display_name = local.root_name parent_management_group_id = local.root_parent_id subscription_ids = local.es_subscription_ids_map[local.root_id] archetype_config = local.es_archetype_config_map[local.root_id] } "${local.root_id}-decommissioned" = { display_name = "Decommissioned" parent_management_group_id = local.root_id subscription_ids = local.es_subscription_ids_map["${local.root_id}-decommissioned"] archetype_config = local.es_archetype_config_map["${local.root_id}-decommissioned"] } ## … }Find the
es_landing_zones_mergelocal value..terraform/modules/enterprise_scale/locals.management_groups.tf
es_landing_zones_merge = merge( local.es_core_landing_zones_to_include, local.es_corp_landing_zones_to_include, local.es_online_landing_zones_to_include, local.es_sap_landing_zones_to_include, local.es_demo_landing_zones_to_include, local.custom_landing_zones, )Notice that this local value uses
mergeto combine thees_core_landing_zones_to_includelocal value with the other landing zones, including the demo landing zones andcustom_landing_zone.Find the
es_landing_zones_maplocal value..terraform/modules/enterprise_scale/locals.management_groups.tf
es_landing_zones_map = { for key, value in local.es_landing_zones_merge : "${local.provider_path.management_groups}${key}" => { id = key display_name = value.display_name ## ... } }This local value iterates through
es_landing_zones_mergeand automatically generates the management groups, populating them with default values.Find the
azurerm_management_group_level_*local values..terraform/modules/enterprise_scale/locals.management_groups.tf
locals { azurerm_management_group_level_1 = { for key, value in local.es_landing_zones_map : key => value if value.parent_management_group_id == local.root_parent_id } ## ... }These local values take the landing zones listed in
es_landing_zones_mapand organize them according to their hierarchy.
Open resources.management_groups.tf. This file defines and deploys the management groups. The parent_management_group_id argument creates dependencies between different groups.
.terraform/modules/enterprise_scale/resources.management_groups.tf
resource "azurerm_management_group" "level_2" { for_each = local.azurerm_management_group_level_2 name = each.value.id display_name = each.value.display_name parent_management_group_id = "${local.provider_path.management_groups}${each.value.parent_management_group_id}" subscription_ids = each.value.subscription_ids depends_on = [azurerm_management_group.level_1] } Since the demo landing zones are merged into es_landing_zones_merge, the module deploys all of the enabled zones, including the demo landing zones, when you apply your configuration.
.terraform/modules/enterprise_scale/locals.management_groups.tf
es_landing_zones_merge = merge( local.es_core_landing_zones_to_include, local.es_corp_landing_zones_to_include, local.es_online_landing_zones_to_include, local.es_sap_landing_zones_to_include, local.es_demo_landing_zones_to_include, local.custom_landing_zones, ) The caf-enterprise-module also creates archetypes, which define the Azure Policy and Access control (IAM) settings needed to secure and configure the landing zones. This includes creating guidelines for role-based access control (RBAC) settings, security settings, and common workload configurations. The module uses es_landing_zones_map and the archetypes submodule to create the landing zone archetypes.
.terraform/modules/enterprise_scale/main.tf
module "management_group_archetypes" { for_each = local.es_landing_zones_map source = "./modules/archetypes" ## ... } The ./modules/archetypes submodule creates and assigns the policies and roles for each management group. The locals.*_definitions.tf files contain the definition; the locals.*_assignments.tf files contain the assignment. These files reference the roles and policies defined in their respective directories in ./modules/archetypes/lib.
Verify core and demo landing zones
Once Terraform finishes applying your resources, open the Azure Portal's Management group page. Here, you will find the management groups provisioned by the caf-enterprise-scale module.
Click the Expand/Collapse all button to view all the management groups in the Learn Terraform ES management group.

Under Landing Zones, notice there are three management groups (Corp, Online, and SAP), each one mapping to the demo landing zones you defined in the enterprise-scale module.
Deploy custom landing zones
Now that you have deployed the core and demo landing zones, deploy a custom landing zone with the caf-enterprise-scale module. You will follow this workflow to deploy landing zones within your organization.
Define a new management group with default policies and access control (IAM) settings by adding the following code snippet to the enterprise_scale module in main.tf.
main.tf
module "enterprise_scale" { ## ... # Define an additional "LearnTerraform" Management Group. custom_landing_zones = { "${local.root_id}-learn-tf" = { display_name = "LearnTerraform" parent_management_group_id = "${local.root_id}-landing-zones" subscription_ids = [] archetype_config = { archetype_id = "default_empty" parameters = {} access_control = {} } } } } Next, apply the configuration. Once prompted, respond yes to deploy the custom landing zone. The module automatically nests the custom landing zone in the root parent management group.
$ terraform apply ## ... Plan: 2 to add, 0 to change, 1 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes ## ... Apply complete! Resources: 2 added, 0 changed, 1 destroyed. The module defines custom landing zones using the same patterns as the core and demo landing zones.
Verify custom landing zone
Once the deployment completes, open the Azure Portal's Management group page.
Under Landing Zones, there is a new landing zone named "LearnTerraform".

Select the LearnTerraform landing zone to review its policies and access control (IAM) settings, which follow Microsoft's best practices. The caf-enterprise-scale module codifies these recommendations, helping you easily provision secure and scalable cloud environments.
Select Access control (IAM) in the left navigation, then Roles. You will find a CustomRole named [TF-CAFES] Network-Subnet-Contributor, a standard role defined by the module.

Deploy management resources
In this section, you will deploy a new management group that will enable logging and security resources, covering all your landing zones.
Add the following code snippet to the enterprise_scale module block in main.tf.
main.tf
module "enterprise_scale" { ## ... # Configuration settings for management resources. # These are used to ensure Azure Policy is correctly configured with the same # settings as the resources deployed by module.enterprise_scale_management. # Please refer to file: settings.management.tf deploy_management_resources = true configure_management_resources = local.configure_management_resources subscription_id_management = data.azurerm_client_config.management.subscription_id } Next, create a new file named settings.management.tf with the following configuration to enable Log Analytics and Security Center in the new landing zone.
settings.management.tf
locals { configure_management_resources = { settings = { log_analytics = { enabled = true config = { retention_in_days = 30 enable_monitoring_for_arc = true enable_monitoring_for_vm = true enable_monitoring_for_vmss = true enable_solution_for_agent_health_assessment = true enable_solution_for_anti_malware = true enable_solution_for_azure_activity = true enable_solution_for_change_tracking = true enable_solution_for_service_map = true enable_solution_for_sql_assessment = true enable_solution_for_updates = true enable_solution_for_vm_insights = true enable_sentinel = true } } security_center = { enabled = true config = { email_security_contact = local.security_contact_email_address enable_defender_for_acr = true enable_defender_for_app_services = true enable_defender_for_arm = true enable_defender_for_dns = true enable_defender_for_key_vault = true enable_defender_for_kubernetes = true enable_defender_for_servers = true enable_defender_for_sql_servers = true enable_defender_for_sql_server_vms = true enable_defender_for_storage = true } } } location = null tags = null advanced = null } } This configuration defines a local value that the caf-enterprise-scale modules uses to configure the management resource settings including enabling log analytics and Azure Security Center.
Next, apply the configuration. Once prompted, respond yes to deploy the custom landing zone.
$ terraform apply ## ... Plan: 45 to add, 1 to change, 31 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes Apply complete! Resources: 45 added, 1 changed, 31 destroyed. Review management submodule
The management submodule deploys the management group and subscription organization by setting the following module arguments:
The deploy_management_resources is set to true to enable the submodule. The configure_management_resources argument customizes the management resources. The settings in settings.management.tf enable log analytics and Azure Security Center. The subscription_id_management sets the management subscription ID. This is useful if you want to deploy management resources in another subscription. Otherwise, this defaults to your current subscription.
.terraform/modules/enterprise_scale/main.tf
module "enterprise_scale" { source = "Azure/caf-enterprise-scale/azurerm" version = "~> 1.0.0" ## ... deploy_management_resources = true configure_management_resources = local.configure_management_resources subscription_id_management = data.azurerm_client_config.management.subscription_id } Like the core resources, review how the caf-enterprise-scale module deploys and customizes the management resources.
First, open resources.management.tf.
.terraform/modules/enterprise_scale/resources.management.tf
resource "azurerm_resource_group" "management" { for_each = local.azurerm_resource_group_management provider = azurerm.management # Mandatory resource attributes name = each.value.template.name location = each.value.template.location tags = each.value.template.tags } Notice that this module deploys resource groups defined by the azurerm_resource_group_management resource.
Open locals.management.tf.
.terraform/modules/enterprise_scale/locals.management.tf
locals { es_management_resource_groups = module.management_resources.configuration.azurerm_resource_group } locals { azurerm_resource_group_management = { for resource in local.es_management_resource_groups : resource.resource_id => resource if resource.managed_by_module } } The azurerm_resource_group_management local value depends on es_management_resource_groups, which is defined by the module.management_resources submodule.
In .terraform/modules/enterprise_scale/management/outputs.tf, configuration maps to local.module_output. Open .terraform/modules/enterprise_scale/management/locals.tf and find module_output to review how the management submodule defines the resource group configuration and adds logging and Security Center to it.
Verify management resources
After Terraform applies your changes, open the Azure Portal's Management group page.
Under Platform, find and click the subscription in the "Management" management group.

The deploy_management_resources argument enables log analytics and Azure Security Center for this subscription. Verify this subscription has Security Center by clicking Security from the left navigation menu.

Deploy connectivity resources
Add the following configuration to the enterprise_scale module block in main.tf
main.tf
module "enterprise_scale" { ## ... # Configuration settings for connectivity resources. # Uses default settings. deploy_connectivity_resources = true subscription_id_connectivity = data.azurerm_client_config.connectivity.subscription_id } This defines a new management group with default policies and access control (IAM) settings. In addition, it creates a centralized hub so your organization can connect with on-premise resources, secures the network with Azure Firewall, and centrally manages the DNS zones.
Next, apply the configuration. Once prompted, respond yes to deploy the custom landing zone.
$ terraform apply ## ... Plan: 111 to add, 0 to change, 26 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes Apply complete! Resources: 111 added, 0 changed, 26 destroyed. Review connectivity submodule
The module deploys the connectivity resources by setting the following module arguments.
The deploy_connectivity_resources is true. This enables the connectivity submodule. The subscription_id_connectivity sets the management subscription ID. This is useful if you want to deploy management resources in another subscription. Otherwise, this defaults to your current subscription.
.terraform/modules/enterprise_scale/main.tf
module "enterprise_scale" { source = "Azure/caf-enterprise-scale/azurerm" version = "~> 1.0.0" ## ... deploy_connectivity_resources = true subscription_id_connectivity = data.azurerm_client_config.connectivity.subscription_id } The module deploys the connectivity resources very similarly to the management resources.
First, open resources.connectivity.tf to find all the connectivity resources this module deploys, including resource groups, virtual network, and subnets.
.terraform/modules/enterprise_scale/resources.connectivity.tf
resource "azurerm_resource_group" "connectivity" { for_each = local.azurerm_resource_group_connectivity provider = azurerm.connectivity # Mandatory resource attributes name = each.value.template.name location = each.value.template.location tags = each.value.template.tags } The resources in this file reference local values for their definitions. Open locals.connectivity.tf to view these local values.
.terraform/modules/enterprise_scale/locals.connectivity.tf
locals { es_connectivity_resource_groups = module.connectivity_resources.configuration.azurerm_resource_group } locals { azurerm_resource_group_connectivity = { for resource in local.es_connectivity_resource_groups : resource.resource_id => resource if resource.managed_by_module } } Similarly to the management local values, the connectivity local values are defined in the connectivity submodule. Review the connectivity submodule's outputs.tf and locals.tf files to understand how it parses and creates the downstream local values.
Verify connectivity resources
Once the deployment completes, open the Azure Portal's Subscription page page and select your subscription. Then, click Resource groups on the left navigation menu.
Filter the resource groups on tf-cafes.

You should find a total of four resource groups. The connectivity submodule created the tf-cafes-connectivity-eastus and tf-cafe-dns resource groups for all the resources it provisioned.
The tf-cafes-connectivity-eastus resource group contains a single virtual network named tf-cafes-hub-eastus. The caf-enterprise-scale module pre-configured the virtual network with subnets for GatewaySubnet and AzureFirewallSubnet. The DDos Protection Standard is disabled to save costs for this tutorial. Enable this in production environments. The tf-cafes-dns resource group creates all the DNS resources. Even though the resource group is in useast (as defined by the `default_location input value), the DNS resources are all global resources. By default, the module creates a private DNS Zone for all services and connects each private DNS zone to the virtual network.
Clean up resources
Before moving on, destroy the infrastructure you created in this tutorial.
$ terraform destroy ## ... Plan: 0 to add, 0 to change, 287 to destroy. Do you really want to destroy all resources? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes ## ... Destroy complete! Resources: 287 destroyed. Be sure to respond to the confirmation prompt with yes.
Next steps
Over the course of this tutorial, you used the caf-enterprise-scale module to deploy the core, management, and connectivity resources as defined by the Cloud Adoption Framework. In the process, you reviewed the module configuration to understand how the module operates behind-the-scenes. Now, you are equipped with the skills to better tailor the module to your organization's priorities and set your organization on a path to sustainable scale.
For more information on topics covered in this tutorial, check out the following resources.