How to Implement an Azure Landing Zone Using the Microsoft Cloud Adoption Framework (CAF) — Part 2

Hai Nguyen
The Factory
Published in
17 min readApr 29, 2021

A deep dive into Level 0, getting started with the Landing Zone development and deploying Launchpad to your Azure Infrastructure.

Part 1 — Azure Landing Zone Architecture
Part 2 — Azure Landing Zone Level 0 — Launchpad
Part 3 — Azure Landing Zone DevOps agent and CI/CD pipeline
Part 4 — Azure Landing Zone Level 1
Part 5 — Azure Landing Zone Level 2

Introduction

In our previous blog, we walked you through each component of the Azure Landing Zone and touched a bit on the different layers of the Landing Zone that we will be covering in this blog series. Now let’s dive deeper into the first layer of the Landing Zone: Level 0.

Level 0 of the Landing Zone contains 2 components: the Launchpad to bootstrap the Landing Zone, and the agents for the pipeline. In this blog we will only cover the Launchpad; the agent deployment will be covered in the next blog, where we will cover the pipelines as well.

1. Getting your feet wet with the Landing Zone

First, I will give you a brief introduction to caf-terraform-landingzones.

caf-terraform-landingzones is a blueprint for the Azure Landing Zone. It combines the Rover, caf_azurermand caf-enterprise-scale modules together into a template for the different levels of the Landing Zone which you can use as a guideline to build your own Landing Zone.

So - now that we’ve started with the level 0 Launchpad, have a look at this launchpad template.

In the readme, you can see different scenarios laid out, which depend on the scope of your Landing Zone. We will start with the basic one, scenario 100.

In the caf-terraform-landingzones repository, there is an excellent guide to help you get started with the scenario, which you can find here.

To avoid “re-inventing the wheel” I will not repeat the same instructions in this blog but direct you there to follow that guide instead. It is crucial you set up your working PC correctly, so you are able to deploy launchpad scenario 100, as stated within the guide before we continue.

Follow until this step

After you successfully deployed the launchpad using the guide, destroy it using the above command with -a destroy. We will expand upon it to deploy our launchpad.

2. Define the scope of your Landing Zone

Before we get started with the Landing Zone, it is important we define the scope of what the Landing Zone will cover, and based on that, you will know what permissions you, and later on the agents, need to deploy the Landing Zone.

Currently, the Landing Zone supports all the modules listed here:

It contains resource templates for Azure Subscription and Azure AD including Applications, Groups, Roles, and Users.

For our customer, we will only manage the resources within Azure Subscription and not in Azure AD, so you and the agent level 0 will only need to be the OWNER of the subscription, for the first-time deployment and subsequently moving the deployment to the pipeline. The agent level 1 will need a Directory Reader role, but we will get into that later.

3. Setting up the Landing Zone git repository

We will split our Landing Zone into 2 repositories, so to get started, create these in Azure DevOps:

Azure DevOps Landing Zone Repositories

These two repositories, and the rover to run them, will be based on caf-terraform-landingzones. So - before we continue, pick a release version from the repo, and check it out locally on your pc. For our customer, we chose the version 2012.1.0 to implement our Landing Zone.

So, what do those two repositories do?

terraform-landingzones is the logic layer of the Landing Zone. It contains reusable Terraform modules for each Landing Zone level, or any Landing Zone resources you wish to deploy repeatedly (think of the VNET spoke module, for example).

For level 0 launchpad, you can use the caf_launchpad folder directly as your launchpad Terraform module code. The launchpad configuration to deploy the Azure Landing Zone resources will be done on caf-configuration repository instead.

terraform-landingzones repository

caf-configuration will contain all the configuration needed for deploying the Landing Zone, it will have the config variable needed to deploy the Terraform template based on the terraform-landingzones repository.

caf-configuration repository

4. Setting up the code within the repositories

If you followed the guide, you should now have a general idea of how to work with Rover, so let’s set it up to run for our solution.

Follow these steps:

  • Create a new folder that you will use to work on the Landing Zone, let’s call it Azure DevOps.
  • Check out terraform-landingzones and caf-configuration in that folder.
  • Head to the caf-terraform-landingzones repository that you checked out locally, make sure it is at version 2012.1.0, and copy caf-terraform-landingzones\.devcontainer folder to Azure DevOps folder.
  • Copy caf-terraform-landingzones\landingzones\caf_launchpad to terraform-landingzones\landingzones\caf_launchpad.

Once you are done with all that, if you open the Azure DevOps folder you should see a result similar to this:

Local Development folder

Now the setting up part is done, we can start with the configuration of the Landing Zone level 0.

For our customer, we only want to use the Landing Zone to manage subscription resources, so we will only need to have the Owner role at the subscription scope to deploy our first level of the Landing Zone. The following resources need to be created:

  • Resource groups for each Landing Zone level, and for the Virtual Network, the agents will be hosted on.
  • Managed Identities for each agent that will run each level of the Landing Zone.
  • Virtual network with each subnet for every agent deployed.
  • Storage Accounts to store the remote state of each level of the Landing Zone.
  • Keyvaults used by the agents/rover to exchange information needed for the Landing Zone.
  • Key Vault secrets used by the rover to manage the Landing Zone.
  • IAM role mapping to assign the necessary permissions for the agents and yourself to deploy the Landing Zone.

Now let’s assume you have Visual Studio Code (VSC) and the correct extension is installed, you can open the Azure DevOps folder with VSC, and open it again with the option Remote-Containers: Reopen in Container.

We will add our configuration for the launchpad in caf-configuration\landingzone\caf_launchpad. First, you can create multiple tfvars files for the launchpad as shown below:

launchpad configuration files

Let’s fill those up with the configuration needed to create the resources listed above.

configuration.tfvars

This is where you can define the core configuration needed for the launchpad, starting with the Landing Zone config:

landingzone = {
backend_type = “azurerm”
level = “level0”
key = “launchpad”
}

Define the regions you want to deploy your Landing Zone solution in. There are two regions you can define: one for the deployment of the Landing Zone itself, and one for disaster recovery. It’s always ideal to make your decision on which region to use for disaster recovery base on Azure Regional Pairs, as it ensures that Azure services within an Azure region offer the best possible performance and security. In this example, we chose westeurope and northeurope, with westeurope set as the default region where we will deploy our Landing Zone:

regions = {
region1 = “westeurope”
region2 = “northeurope
}
default_region = “region1”

Define the tags that will be applied across the Landing Zone resources:

tags = {
owner = “CAF”
deploymentType = “Terraform”
costCenter = “0”
BusinessUnit = “SHARED”
DR = “NON-DR-ENABLED”
}
inherit_tags = true

Create launchpad_key_names for all the levels of the Landing Zone:

launchpad_key_names = {
tfstates = [
"level0",
"level1",
"level2",
"level3",
"level4"
]
}

Create resource groups to store different launchpad resources in:

resource_groups = {
level0 = {
name = “launchpad-level0”
tags = {
level = “level0”
}
}
level1 = {
name = "launchpad-level1"
tags = {
level = "level1"
}
}
level2 = {
name = "launchpad-level2"
tags = {
level = "level2"
}
}
level3 = {
name = "launchpad-level3"
tags = {
level = "level3"
}
}
level4 = {
name = "launchpad-level4"
tags = {
level = "level4"
}
}
security = {
name = “launchpad-security”
}
networking = {
name = “launchpad-networking”
}
}

Now let’s move on to the next file

iam_managed_identitiess.tfvars

For each level, add an Azure Managed Identity(MSI) that the Azure DevOps agent can use:

managed_identities = {
level0 = {
name = “landingzone-level0-msi”
resource_group_key = “security”
}
level1 = {
name = "landingzone-level1-msi"
resource_group_key = "security"
}
level2 = {
name = "landingzone-level2-msi"
resource_group_key = "security"
}
level3 = {
name = "landingzone-level3-msi"
resource_group_key = "security"
}
level4 = {
name = "landingzone-level4-msi"
resource_group_key = "security"
}
}

iam_role_mapping.tfvars

This is where you can add RBAC for yourself and agent MSI to deploy resources on your behalf.

So, which permission do you need to give to each service principal?

For the subscription that you will deploy the launchpad in (logged_in_subscription):

  • “Owner” — only level 0 and level 1 managed identity need to be the OWNER of the subscription, level 0 is for deploying resources to this subscription, and level 1 is for you to be able to join this subscription to Management Groups that will be deployed to the level 1 pipeline later on. You also need to be assignedOwner role to be able to deploy the Landing Zone.
  • “Reader and Data Access” and “Key Vault Reader” — those permissions are needed if you want to deploy Landing Zone resources to other subscriptions but want to have it connected to the Landing Zone of this subscription, and keep all the .tfstate files to this subscription instead. You give those roles to all managed identities of upper levels of the Landing Zones, aka level2, level3, level4, etc. Those roles will be used by the Rover agents to assess the state of your Landing Zone.

For the storage_accounts that you are creating to store all the .tfstate files:

  • “Storage Blob Data Contributor” — For each storage account that you create (level0, level1, level2, etc), you give the role to yourself via object_ids for firsts time deployment, which can be removed once you handover the deployment to the Azure DevOps pipeline. You also need to give the role to the managed identity of that level, so level0 managed identity has the “Storage Blob Data Contributor” role for level0 storage account for example.
  • “Storage Blob Data Reader” — This is used so that the agent of the upper level of the Landing Zone can read the .tfstate file of the lower level of the Landing Zone. So if level1 needs to get some output from level0 to create its own resources, you give level1 managed identity “Storage Blob Data Reader” permission to the storage account level0. It isn’t always the case and some Landing Zone can operate completely separate from each other. In this case, we give the manage identities “Storage Blob Data Reader” role for its lower-level storage account.

With all that information in mind, we populate the iam_role_mapping.tfvars with the below configuration:

role_mapping = {
built_in_role_mapping = {
subscriptions = {
logged_in_subscription = {
“Owner” = {
managed_identities = {
keys = ["level0", "level1"]
}
"Reader and Data Access" = {
managed_identities = {
keys = ["level2", "level3", "level4"]
}
}
"Key Vault Reader" = {
managed_identities = {
keys = ["level2", "level3", "level4"]
}
}
}
}
}
storage_accounts = {
level0 = {
“Storage Blob Data Contributor” = {
object_ids = {
#put your own user object_id here
keys = [“b7dcca8c-9e7c-450b-a75f-2a7d02d27339”]
}
managed_identities = {
keys = [“level0”]
}
}
"Storage Blob Data Reader" = {
managed_identities = {
keys = ["level1"]
}
}
}
level1 = {
"Storage Blob Data Contributor" = {
object_ids = {
#put your own user object_id here
keys = [“b7dcca8c-9e7c-450b-a75f-2a7d02d27339”]
}
managed_identities = {
keys = ["level1"]
}
}
"Storage Blob Data Reader" = {
managed_identities = {
keys = ["level2"]
}
}
}
level2 = {
"Storage Blob Data Contributor" = {
object_ids = {
#put your own user object_id here
keys = [“b7dcca8c-9e7c-450b-a75f-2a7d02d27339”]
}
managed_identities = {
keys = ["level2"]
}
}
}
level3 = {
"Storage Blob Data Contributor" = {
object_ids = {
#put your own user object_id here
keys = [“b7dcca8c-9e7c-450b-a75f-2a7d02d27339”]
}
managed_identities = {
keys = ["level3"]
}
}
}
level4 = {
"Storage Blob Data Contributor" = {
object_ids = {
#put your own user object_id here
keys = [“b7dcca8c-9e7c-450b-a75f-2a7d02d27339”]
}
managed_identities = {
keys = ["level4"]
}
}
}
}
}

keyvault.tfvars

This is where you can deploy the Key Vaults that will be used to store the information needed by Rover to determine the state of your Landing Zone. So if you are deploying 5 levels of the Landing Zone, you add 5 key vaults here: level0, level1, level2, level3, level4.

Because key vaults do not only use RBAC, but using their own access policies to control who should have access to the secrets stored within it, we will need to add additional creation policies here so the Rover can have access to them.

Let’s get started on level0 key vault, here you need to give yourself and level0 MSI full permission to secrets, so you can do the initial deployment, and the MSI can take over via the pipeline. Since Rover also use the key vault to identifies whereas the LandingZone already exist, you also need to give [“Get”, “List”] permission to all the MSI of the upper level as well, which in this case will be msi_level1, msi_level2, msi_level3, msi_level4

For all other key vault levels, you just need to secret permission to yourself, level0 MSI, and the MSI of the level corresponds to the key vault. For example, for level2 key vault, you need to give secret permission to yourself, level0 MSI and level2 MSI.

With that in mind, we populate the keyvault.tfvars as stated below:

keyvaults = {
level0 = {
name = “level0”
resource_group_key = “level0”
sku_name = “standard”
soft_delete_enabled = true
tags = {
tfstate = “level0”
environment = “production”
}
creation_policies = {
yourself = {
#put your own user object_id here
object_id = "b7dcca8c-9e7c-450b-a75f-2a7d02d27339"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level0 = {
lz_key = "launchpad"
managed_identity_key = "level0"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level1 = {
lz_key = "launchpad"
managed_identity_key = "level1"
secret_permissions = ["Get", "List"]
}
msi_level2 = {
lz_key = "launchpad"
managed_identity_key = "level2"
secret_permissions = ["Get", "List"]
}
msi_level3 = {
lz_key = "launchpad"
managed_identity_key = "level3"
secret_permissions = ["Get", "List"]
}
msi_level4 = {
lz_key = "launchpad"
managed_identity_key = "level4"
secret_permissions = ["Get", "List"]
}
}
}
level1 = {
name = "level1"
resource_group_key = "level1"
sku_name = "standard"
soft_delete_enabled = true
tags = {
tfstate = "level1"
environment = "production"
}
creation_policies = {
yourself = {
#put your own user object_id here
object_id = "b7dcca8c-9e7c-450b-a75f-2a7d02d27339"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level0 = {
lz_key = "launchpad"
managed_identity_key = "level0"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level1 = {
lz_key = "launchpad"
managed_identity_key = "level1"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
}
}
level2 = {
name = "level2"
resource_group_key = "level2"
sku_name = "standard"
soft_delete_enabled = true
tags = {
tfstate = "level2"
environment = "production"
}
creation_policies = {
yourself = {
#put your own user object_id here
object_id = "b7dcca8c-9e7c-450b-a75f-2a7d02d27339"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level0 = {
lz_key = "launchpad"
managed_identity_key = "level0"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level2 = {
lz_key = "launchpad"
managed_identity_key = "level2"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
}
}
level3 = {
name = "level3"
resource_group_key = "level3"
sku_name = "standard"
soft_delete_enabled = true
tags = {
tfstate = "level3"
environment = "production"
}
creation_policies = {
yourself = {
#put your own user object_id here
object_id = "b7dcca8c-9e7c-450b-a75f-2a7d02d27339"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level0 = {
lz_key = "launchpad"
managed_identity_key = "level0"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level3 = {
lz_key = "launchpad"
managed_identity_key = "level3"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
}
}
level4 = {
name = "level4"
resource_group_key = "level4"
sku_name = "standard"
soft_delete_enabled = true
tags = {
tfstate = "level4"
environment = "production"
}
creation_policies = {
yourself = {
#put your own user object_id here
object_id = "b7dcca8c-9e7c-450b-a75f-2a7d02d27339"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level0 = {
lz_key = "launchpad"
managed_identity_key = "level0"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
msi_level4 = {
lz_key = "launchpad"
managed_identity_key = "level4"
secret_permissions = ["Set", "Get", "List", "Delete", "Purge", "Recover"]
}
}
}
}

dynamic_secret.tfvars

With the Key Vault created using the configuration, we can start adding secrets to it, all the value in here is just something Rover used to determine the state of the Landing Zone, paste it in as-is below:

dynamic_keyvault_secrets = {
level0 = {
msi = {
output_key = “managed_identities”
resource_key = “level0”
attribute_key = “id”
secret_name = “msi-resource-id”
}
subscription_id = {
output_key = “client_config”
attribute_key = “subscription_id”
secret_name = “subscription-id”
}
tenant_id = {
output_key = “client_config”
attribute_key = “tenant_id”
secret_name = “tenant-id”
}
}
level1 = {
msi = {
output_key = "managed_identities"
resource_key = "level1"
attribute_key = "id"
secret_name = "msi-resource-id"
}
lower_stg = {
output_key = "storage_accounts"
resource_key = "level0"
attribute_key = "name"
secret_name = "lower-storage-account-name"
}
lower_rg = {
output_key = "resource_groups"
resource_key = "level0"
attribute_key = "name"
secret_name = "lower-resource-group-name"
}
subscription_id = {
output_key = "client_config"
attribute_key = "subscription_id"
secret_name = "subscription-id"
}
tenant_id = {
output_key = "client_config"
attribute_key = "tenant_id"
secret_name = "tenant-id"
}
}
level2 = {
msi = {
output_key = "managed_identities"
resource_key = "level2"
attribute_key = "id"
secret_name = "msi-resource-id"
}
lower_stg = {
output_key = "storage_accounts"
resource_key = "level1"
attribute_key = "name"
secret_name = "lower-storage-account-name"
}
lower_rg = {
output_key = "resource_groups"
resource_key = "level1"
attribute_key = "name"
secret_name = "lower-resource-group-name"
}
subscription_id = {
output_key = "client_config"
attribute_key = "subscription_id"
secret_name = "subscription-id"
}
tenant_id = {
output_key = "client_config"
attribute_key = "tenant_id"
secret_name = "tenant-id"
}
}
level3 = {
msi = {
output_key = "managed_identities"
resource_key = "level3"
attribute_key = "id"
secret_name = "msi-resource-id"
}
lower_stg = {
output_key = "storage_accounts"
resource_key = "level2"
attribute_key = "name"
secret_name = "lower-storage-account-name"
}
lower_rg = {
output_key = "resource_groups"
resource_key = "level2"
attribute_key = "name"
secret_name = "lower-resource-group-name"
}
subscription_id = {
output_key = "client_config"
attribute_key = "subscription_id"
secret_name = "subscription-id"
}
tenant_id = {
output_key = "client_config"
attribute_key = "tenant_id"
secret_name = "tenant-id"
}
}
level4 = {
msi = {
output_key = "managed_identities"
resource_key = "level4"
attribute_key = "id"
secret_name = "msi-resource-id"
}
lower_stg = {
output_key = "storage_accounts"
resource_key = "level3"
attribute_key = "name"
secret_name = "lower-storage-account-name"
}
lower_rg = {
output_key = "resource_groups"
resource_key = "level3"
attribute_key = "name"
secret_name = "lower-resource-group-name"
}
subscription_id = {
output_key = "client_config"
attribute_key = "subscription_id"
secret_name = "subscription-id"
}
tenant_id = {
output_key = "client_config"
attribute_key = "tenant_id"
secret_name = "tenant-id"
}
}
}

storage_accounts.tfvars

Add the storage account that Rover will use to store the Terraform Landing Zone remote state file. Nothing special here, for each level of the Landing Zone you want to deploy, add a storage account, so for our Landing Zone, we add 5 storage accounts for 5 levels:

storage_accounts = {
level0 = {
name = “level0”
resource_group_key = “level0”
account_kind = “BlobStorage”
account_tier = “Standard”
account_replication_type = “RAGRS”
tags = {
## Those tags must never be changed after being set as they are used by the rover to locate the launchpad and the tfstates.
# Only adjust the environment value at creation time
tfstate = “level0”
environment = “production”
launchpad = “launchpad”
}
containers = {
tfstate = {
name = “tfstate”
}
}
}
level1 = {
name = "level1"
resource_group_key = "level1"
account_kind = "BlobStorage"
account_tier = "Standard"
account_replication_type = "RAGRS"
tags = {
# Those tags must never be changed while set as they are used by the rover to locate the launchpad and the tfstates.
tfstate = "level1"
environment = "production"
launchpad = "launchpad"
}
containers = {
tfstate = {
name = "tfstate"
}
}
}
level2 = {
name = "level2"
resource_group_key = "level2"
account_kind = "BlobStorage"
account_tier = "Standard"
account_replication_type = "RAGRS"
tags = {
# Those tags must never be changed while set as they are used by the rover to locate the launchpad and the tfstates.
tfstate = "level2"
environment = "production"
launchpad = "launchpad"
}
containers = {
tfstate = {
name = "tfstate"
}
}
}
level3 = {
name = "level3"
resource_group_key = "level3"
account_kind = "BlobStorage"
account_tier = "Standard"
account_replication_type = "RAGRS"
tags = {
# Those tags must never be changed while set as they are used by the rover to locate the launchpad and the tfstates.
tfstate = "level3"
environment = "production"
launchpad = "launchpad"
}
containers = {
tfstate = {
name = "tfstate"
}
}
}
level4 = {
name = "level4"
resource_group_key = "level4"
account_kind = "BlobStorage"
account_tier = "Standard"
account_replication_type = "RAGRS"
tags = {
# Those tags must never be changed while set as they are used by the rover to locate the launchpad and the tfstates.
tfstate = "level4"
environment = "production"
launchpad = "launchpad"
}
containers = {
tfstate = {
name = "tfstate"
}
}
}
}

It’s getting a bit long, I know, but we are almost there! Now we just need to create the network resources that can be used to deploy the agents on, we do this in:

networking.tfvars

Here you can deploy the VNET, and all the subnets that you can use to deploy the agents from each level. Optionally you can add Bastion Host or Jump Host subnets so that you can log in to the agent and troubleshoot it if it does not work correctly, but we will be using Virtual Machine Scale Set so that it can scale based on demand, and if the agent acts up, you can terminate it and it will automatically be spun up again.

To get started, pick a CIDR range based on what is available from the customer to use for our VNET CIDR block. It should have a maximum prefix of /24. After that divide it into 6 smaller CIDR blocks to be used for each subnet. There shouldn’t be any overlap between them. Once that’s done you can populate networking.tfvars as shown below:

vnets = {
devops_region1 = {
resource_group_key = “networking”
region = “region1”
vnet = {
name = “devops”
address_space = [“172.16.2.0/24”]
}
specialsubnets = {}
subnets = {
release_agent_level0 = {
name = "level0"
cidr = ["10.92.2.32/28"]
service_endpoints = ["Microsoft.KeyVault"]
}
release_agent_level1 = {
name = "level1"
cidr = ["10.92.2.48/28"]
service_endpoints = ["Microsoft.KeyVault"]
}
release_agent_level2 = {
name = "level2"
cidr = ["10.92.2.64/28"]
service_endpoints = ["Microsoft.KeyVault"]
}
release_agent_level3 = {
name = "level3"
cidr = ["10.92.2.80/28"]
service_endpoints = ["Microsoft.KeyVault"]
}
release_agent_level4 = {
name = "level4"
cidr = ["10.92.2.96/28"]
service_endpoints = ["Microsoft.KeyVault"]
}
private_endpoints = {
name = "private_endpoints"
cidr = ["10.92.2.112/28"]
enforce_private_link_endpoint_network_policies = true
}
}
}
}

5. Deploy the launchpad

Welp, that is a long list of configuration to do, but now it’s done, you are ready to deploy the launchpad and kickstart your first Landing Zone - run the following commands:

rover login

az account set — subscription "<subscription-id>"

rover -lz /tf/caf/terraform-landingzones/landingzones/caf_launchpad -var-folder /tf/caf/caf-configuration/landingzone/caf_launchpad/ \
-a plan \
-launchpad \
-env production \
-tfstate “caf_launchpad-caf_launchpad.tfstate”

What’s up with the variable caf_launchpad-caf_launchpad.tfstate you might ask? It is needed for us to integrate the Landing Zone into the pipeline, which we will explain in a later blog.

For now, check the resource from the Terraform plan to see if there is an error. If there are none you can proceed with this command, the Rover will deploy the launchpad on your behalf:

rover -lz /tf/caf/terraform-landingzones/landingzones/caf_launchpad -var-folder /tf/caf/caf-configuration/landingzone/caf_launchpad/ \
-a apply \
-launchpad \
-env production \
-tfstate “caf_launchpad-caf_launchpad.tfstate”

Conclusion

Congratulations! You have successfully deployed your first Landing Zone - take a break, you deserve it. In our next blog, we will go over deploying the agent for level 0 and setting up the CI/CD pipeline to do the deployment for you.

--

--