This configuration provides a minimal set of resources to function as an execution environment for Terraform which is often a critical part of a DevOps pipeline. It is a useful starting point for DevOps / Infrastructure-As-Code (IaC) projects that require a secure and isolated environment for deploying and managing infrastructure using Terraform. The root module creates network, storage and security prerequisites. The virtual machine used as a Terraform execution environment is implemented in a child module which can be repurposed in other environments (such as a platform landing zone) where the resources defined in the root module already exist.
This section provides a brief overview of the features included in this configuration.
- A pre-configured virtual network, including:
- A subnet for use by the Linux virtual machine
- A NAT gateway for outbound internet connectivity
- A pre-configured storage account, including:
- A container for storing Terraform state files
- Public access is enabled by design since there may be the first environment provisioned in Azure. This can be disabled once a platform landing zone is in place.
- Shared access keys are disabled by default and Azure RBAC is used for authorization.
- A pre-configured key vault for managing secrets
- A pre-configured Linux virtual machine for use as a Terraform execution environment implemented as a module for inclusion in other configurations, including:
- Key vault secrets for secure SSH access to the Linux virtual machine
- Managed identity role assignments for Azure Blob Storage for use as a Terraform state backend
- Pre-configured environment variables for Terraform azurerm provider to use managed identities
- Pre-installed software for IaC / DevOps projects
- Optional public access for connectivity from the internet
NOTE: The Linux virtual machine is implemented as a module so it can be easily reused in other configurations. It is not intended to be used as a production jumpbox. It is intended to be used as a Terraform execution environment for DevOps / IaC projects.
This section describes the prerequisites required in order to provision this configuration.
- Microsoft Entra ID Tenant and Azure Subscription
- Service Principal
- Other Prerequisites
- Terraform Execution Environment
- Identify the Microsoft Entra ID tenant to be used for identity and access management, or create a new tenant using Quickstart: Set up a tenant.
- Identify a single Azure subscription or create a new Azure subscription. See Azure Offer Details and Associate or add an Azure subscription to your Microsoft Entra tenant for more information.
- Identify the owner of the Azure subscription to be used to provision this configuration. This user should have an Owner Azure RBAC role assignment on the subscription. See Steps to assign an Azure role for more information.
-
Verify the subscription owner has privileges to create a service principal on the Microsoft Entra tenant. See Permissions required for registering an app for more information.
-
Ask the subscription owner to Create an Azure service principal (SPN) to provision this configuration with Azure Cloud Shell. The service principal can be created with the following command:
az ad sp create-for-rbac -n DevOpsSPN --role Owner --scopes /subscriptions/YOUR-SUBSCRIPTION-ID-HERE
The output should look like this:
{ "appId": "YOUR-SERVICE-PRINCIPAL-APP-ID-HERE", "displayName": "DevOpsSPN", "password": "YOUR-SERVICE-PRINCIPAL-PASSWORD-HERE", "tenant": "YOUR-ENTRA-TENANT-ID-HERE" }
Save the service principal appId and password in a secure location such as a password vault.
- Some Azure subscriptions may have low quota limits for specific Azure resources which may cause failures. See Resolve errors for resource quotas for more information.
- Some Azure subscriptions may not have the required resource providers registered. Terraform is configured to automatically register the required resource providers, but this may take some time on first use.
- Some Azure subscriptions may not have the required features enabled which may cause failures. Monitor your plan application for errors related to specific subscription features and enable them as needed.
- Some organizations may institute Azure policy which may cause failures. This can be addressed by using custom settings which pass the policy checks, or by disabling the policies on the Azure subscription being used for the configurations.
A variety of Terraform execution environments can be used to provision this configuration interactively, including:
-
Windows-Only Client: A Windows-only client can be used to run Terraform. The following software should be installed:
- git
- PowerShell 7.x
- Terraform
- Visual Studio Code
-
Windows Subsystem for Linux (WSL) Client: WSL offers the best of Windows and Linux in the same client environment and was used to develop this project. The following software should be installed:
- Linux (WSL) software
- Linux Distro: Ubuntu 24.04 LTS (Noble Numbat)
- pip3 Python library package manager and the PyJWT Python library.
- git
- Azure CLI
- Terraform
- Windows software
- Visual Studio Code configured for Remote development in WSL
- Linux (WSL) software
-
Linux / MacOs Client: A Linux or MacOS client can be used to run Terraform. The following software should be installed:
- git
- Azure CLI
- Terraform
- Visual Studio Code
-
Azure Cloud Shell: Azure Cloud Shell can be used to run Terraform. Most of the software dependencies are pre-installed.
-
GitHub Codespaces: GitHub Codespaces has not been tested but should be okay. Most of the software dependencies should be pre-installed.
This section covers the steps to get started with this configuration using an interactive Terraform execution environment. The steps include logging into Azure, cloning the repository, initializing Terraform, configuring variables, validating and applying the configuration, and smoke testing.
- Step 0: Login
- Step 1: Clone the Repository
- Step 2: Initialize Terraform
- Step 3: Configure Variables
- Step 4: Validate and Apply Configuration
- Step 5: Complete Smoke Testing
- Step 6: Use the Virtual Machine
- Step 7: Clean Up
Before you can provision this configuration interactively, you need to log in to your Azure account. Use the following steps to log in
-
Open a terminal window and run one of the following commands to log in to Azure according to your preference:
# Log in using Azure CLI az login --use-device-code# Log in using PowerShell Connect-AzAccount -UseDeviceAuthentication
-
Check the terminal for a message like this:
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXXXXXX to authenticate. -
Open a browser and navigate to the URL provided in the terminal. Enter the code displayed in the terminal to authenticate your Azure account.
-
Sign in with an account that has an Azure RBAC
Ownerrole assignment on the subscription you will be using to provision this configuration. -
Depending upon how your Entra ID tenant is configured, you may be prompted to enter your password and/or approve a sign-in request on your mobile device.
-
Click Continue when asked if you are trying to sign in to Azure.
-
Check the terminal for a prompt like this:
Retrieving tenants and subscriptions for the selection... [Tenant and subscription selection] No Subscription name Subscription ID Tenant ----- ------------------------------ ------------------------------------ ---------------- [1] * SUBSCRIPTION1-NAME-HERE SUBSCRIPTION1-ID-HERE TENANT-NAME-HERE [2] SUBSCRIPTION2-NAME-HERE SUBSCRIPTION2-ID-HERE TENANT-NAME-HERE The default is marked with an *; the default tenant is 'TENANT-NAME-HERE' and subscription is 'SUBSCRIPTION1-NAME-HERE' (SUBSCRIPTION1-ID-HERE). Select a subscription and tenant (Type a number or Enter for no changes): -
Select the subscription you will be using to provision this configuration by entering the number next to it. If you only have one subscription, just press Enter.
Clone the Azure Sandbox repository to the Terraform execution environment using the following command:
git clone https://github.com/Azure-Samples/azuresandboxWARNING: By default this configuration assumes you will be using a local Terraform state file which includes sensitive information. This is by design since it is actually creating the storage account where Terraform state files are to be stored for other configurations.
After cloning the repository, navigate to the rg-devops-iac directory and initialize Terraform. This step downloads the necessary provider plugins and modules.
cd azuresandbox/extras/configurations/rg-devops-iac
terraform initThere many variables in this configuration. Defaults have been set for most of them so only a few need to be set in advance to provision this configuration. There are a few different ways to set variables in Terraform.
- Environment Variables: Set environment variables for sensitive information such as passwords and secrets used in the root module.
- terraform.tfvars file: Create a
terraform.tfvarsfile in the root directory of the project to set variables for the root module. - main.tf file: Override module default values in the
main.tffile by customizing the module blocks.
Follow these steps to configure the variables for this configuration:
-
First, create an environment variable for the service principal password. For security reasons this secret should not be stored in the
terraform.tfvarsfile.# Set environment variable in bash export TF_VAR_arm_client_secret=YOUR-SERVICE-PRINCIPAL-PASSWORD-HERE
# Set environment variable in PowerShell $env:TF_VAR_arm_client_secret = "YOUR-SERVICE-PRINCIPAL-PASSWORD-HERE"
-
Next, create a
terraform.tfvarsfile in the root directory of the project. This file should set the necessary variables for your deployment. Here is an example of what theterraform.tfvarsfile might look like:aad_tenant_id = "YOUR-ENTRA-TENANT-ID-HERE" arm_client_id = "YOUR-SERVICE-PRINCIPAL-APP-ID-HERE" location = "YOUR-AZURE-REGION-HERE" subscription_id = "YOUR-AZURE-SUBSCRIPTION-ID-HERE" user_object_id = "YOUR-USER-OBJECT-ID-HERE" tags = { project = "devops", costcenter = "mycostcenter", environment = "dev" }
Helper scripts are provided to generate the
terraform.tfvarsfile:# Bash bootstrap script, run from the root directory of the project ./scripts/bootstrap.sh# PowerShell bootstrap script, run from the root directory of the project ./scripts/bootstrap.ps1
The variable defaults set in this configuration can be overridden by customizing the terraform.tfvars file or modifying the module block in main.tf. For example, the default value for the vm_jumpbox_linux_name variable in the vm-jumpbox-linux module is jumplinux2. You can override this value by adding the following to the vnet-shared module block in main.tf:
module "vm_jumpbox_linux" {
source = "./modules/vm-jumpbox-linux"
enable_public_access = true
key_vault_id = azurerm_key_vault.this.id
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
storage_account_id = azurerm_storage_account.this.id
subnet_id = azurerm_subnet.this.id
tags = var.tags
depends_on = [time_sleep.wait_for_roles]
# Override defaults here
vm_jumpbox_linux_name = "devopsagent1"
}Remember that Terraform will compare the state of existing resources in Azure with what it expects to be there when creating a plan. If the state of existing resources in Azure does not match what Terraform expects, the plan will modify existing resources to match the configuration. This can happen when resources are modified manually or via Policy outside of Terraform. This is known as "drift".
Follow these steps to validate and apply the configuration:
-
First, validate that the configuration is syntactically correct:
terraform validate
-
Next, create a plan to see what resources will be created:
terraform plan
- IMPORTANT: If you are provisioning this configuration for the first time, you will see a lot of
+signs in the output. This indicates that new resources will be created if this plan is applied. If you are updating this configuration after it has been provisioned, you may see~signs indicating that existing resources will be updated. If you see-signs, this indicates that existing resources will be deleted. Be careful with these operations as they may cause data loss.
- IMPORTANT: If you are provisioning this configuration for the first time, you will see a lot of
-
Finally, apply the configuration to create the resources:
terraform apply
-
Monitor the progress of the apply operation in the console. If errors occur, that may not be reflected in the console immediately. Terraform will try to apply as much of the plan as possible first, then will show the errors when it is done. It can take up to 15 minutes to provision this configuration. If everything goes well you should see a message like this:
Apply complete! Resources: XX added, XX changed, XX destroyed.
After the configuration has been provisioned, complete the smoke testing procedures in the vm-jumpbox-linux module.
You now have a fully provisioned DevOps IaC environment! You can use it as a Terraform execution environment interactively or via DevOps pipelines.
Don't forget to delete your DevOps IaC environment when you're done. You don't want to have to explain to your boss why you left an unused resources laying around that costs your company money. The quickest way to clean up is to delete the DevOps IaC resource group. Do this with care because data loss will occur, including any Terraform state files in the Azure Blob Storage container.
This section provides documentation regarding the overall structure of the root module for this configuration. See the README.md files in each module directory for more information about that module.
- Root Module Structure
- Root Module Input Variables
- Root Module Resources
- Child Modules
- Virtual Network Design
- Dependencies
This configuration is organized into the following structure:
├── images/ #
│ └── rg-devops-iac-diagram.drawio.svg # Architecture diagram
├── modules/ #
│ └── vm-jumpbox-linux/ # Linux jumpbox virtual machine module
├── scripts/ #
│ ├── bootstrap.sh # Bash helper script for generating terraform.tfvars
│ └── bootstrap.ps1 # PowerShell helper script for generating terraform.tfvars
├── locals.tf # Local variables
├── main.tf # Resource configurations
├── network.tf # Network resource blocks
├── outputs.tf # Output variables
├── providers.tf # Provider configuration blocks
├── storage.tf # Storage resource blocks
├── terraform.tf # Terraform configuration block
└── variables.tf # Variable definitions
This section lists input variables used in the root module. Defaults can be overridden by specifying a different value in terraform.tfvars.
| Variable | Default | Description |
|---|---|---|
| aad_tenant_id | The Microsoft Entra tenant id. | |
| arm_client_id | The AppId of the service principal used for authenticating with Azure. Must have a 'Contributor' role assignment. | |
| arm_client_secret | The password for the service principal used for authenticating with Azure. Set interactively or using an environment variable 'TF_VAR_arm_client_secret'. | |
| location | centralus | The name of the Azure Region where resources will be provisioned. |
| storage_access_tier | Hot | The access tier for the new storage account. |
| storage_replication_type | LRS | The type of replication for the new storage account. |
| subnet_address_prefix | 10.0.0.0/24 | The address prefix for the miscellaneous subnet. The minimum size is /29. |
| subnet_name | snet-devops-01 | The name of the subnet to be created in the new virtual network. |
| subnet_privatelink_address_prefix | 10.0.1.0/24 | The address prefix for the PrivateLink subnet |
| subnet_privatelink_name | snet-privatelink-03 | The name of the subnet for PrivateLink endpoints |
| subscription_id | The Azure subscription id used to provision resources. | |
| storage_container_name | tfstate | The name of the storage container to be created in the new storage account. |
| tags | The tags in map format to be used when creating new resources. | |
| user_object_id | The object id of the user in Microsoft Entra ID. | |
| vnet_address_space | 10.0.0.0/16 | The address space in CIDR notation for the new virtual network. The minimum size is /24. |
| vnet_name | devops | The name of the new virtual |
The root module includes a resource group, key vault and storage account used by the child modules. It also implements Azure RBAC role assignments for both the service principal used by Terraform as well as the interactive user.
| Address | Name | Notes |
|---|---|---|
| azurerm_key_vault.this | kv-devops-dev-xxx | Used to store secrets for the Linux virtual machine. |
| azurerm_nat_gateway.this | ng-devops-dev | NAT gateway for outbound internet connectivity. |
| azurerm_nat_gateway_public_ip_association.this | Associates the NAT gateway with the public IP address. | |
| azurerm_network_security_group.devops | nsg-devops-dev-snet-devops-01 | NSG used to open inbound connectivity for port 22 (SSH) to the Linux virtual machine. |
| azurerm_private_dns_zone.key_vault | privatelink.vaultcore.azure.net | Private DNS zone for Azure Key Vault |
| azurerm_private_dns_zone.storage_blob | privatelink.blob.core.windows.net | Private DNS zone for Azure blob storage |
| azurerm_private_dns_zone_virtual_network_link.key_vault | Links private dns zone to azurerm_virtual_network.this | |
| azurerm_private_dns_zone_virtual_network_link.storage_blob | Links private dns zone to azurerm_virtual_network.this | |
| azurerm_private_endpoint.key_vault | pe-devops-dev-key-vault | Private endpoint for Azure key vault |
| azurerm_private_endpoint.storage_blob | pe-devops-dev-storage-blob | Private endpoint for Azure blob storage |
| azurerm_public_ip.this | pip-devops-dev-nat | Public IP address for the NAT gateway. |
| azurerm_resource_group.this | rg-devops-dev-xxx | Resource group for all resources in this configuration. |
| azurerm_role_assignment.keyvault_roles[*] | Assigns Key Vault Secrets Officer to both the service principal and the interactive user. |
|
| azurerm_role_assignment.storage_roles[*] | Assigns Storage Blob Data Contributor to both the service principal and the interactive user. |
|
| azurerm_storage_account.this | stdevopsdevxxx | Storage account for storing Terraform state files. |
| azurerm_storage_container.this | tfstate | Storage container for storing Terraform state files. |
| azurerm_subnet.devops | snet-devops-01 | Subnet for jumplinux2 VM |
| azurerm_subnet.privatelink | snet-privatelink-03 | Subnet for PrivateLink endpoints |
| azurerm_subnet_nat_gateway_association.devops | vnet-devops-dev-devops | Associates the NAT gateway with the subnet. |
| azurerm_subnet_network_security_group_association.this | Associates the NSG with the subnet. | |
| azurerm_virtual_network.this | vnet-devops-dev-devops | Virtual network for the Linux virtual machine. |
This section includes a list of output variables returned by the root module.
| Name | Comments |
|---|---|
| resource_ids | A map of resource IDs for key resources in the configuration. |
| resource_names | A map of resource names for key resources in the configuration. |
The following modules are included in this configuration:
| Name | Required | Depends On | Description |
|---|---|---|---|
| vm-jumpbox-linux | No | Root module | Creates a preconfigured Linux jumpbox VM in the DevOps virtual network. |
This configuration implements a virtual network. Default address ranges are set artificially high for demos and readability and can be overridden with smaller ranges to meet specific requirements. An firewall is not included in this configuration by design as it may be used as an startup environment to provision a platform landing zone or sandbox that does include a firewall. The virtual network includes two subnets:
- A subnet for the Linux virtual machine. The subnet is configured with a NAT gateway for outbound internet connectivity and an NSG to control inbound traffic.
- A subnet for PrivateLink endpoints for Azure Key Vault and Azure Blob Storage
This section covers the dependencies in this configuration.
- Source Control
- Terraform
- Scripting Technologies
- Configuration Management Technologies
- Operating Systems
The configuration is hosted on GitHub and uses Git for source control. The repository is organized into configurations and modules, making it easy to navigate and understand the structure of the project. The [vm-jumpbox-linux] module can be repurposed for use in other configurations.
This project is built built using Terraform, a cross platform, open-source Infrastructure as Code (IaC) tool that allows users to define, provision and version cloud infrastructure using a declarative configuration language called HCL (HashiCorp Configuration Language).
- Providers: The following Terraform providers are used in the project:
- azurerm: Used to manage Azure resources.
- cloudinit: Used to configure Linux virtual machines.
- random: Used to generate random values for resource attributes.
- time: Used to implement wait cycles and other time based operations.
- tls: Used to generate the SSH public and private key pairs for the Linux virtual machine.
- Modules: The following Terraform modules are used in this project:
- Azure/naming: A module for generating consistent and compliant Azure resource names.
The following cross-platform scripting technologies are used in this project:
- PowerShell: Used for general purpose scripting and to implement Terraform provisioners and Azure custom script extensions.
- Az PowerShell Module: Used to connect to and configure Azure resources from PowerShell scripts.
- Bash: Used for general purpose scripting.
- Azure CLI: Used for connecting to Azure resources from Bash scripts.
The following configuration management technologies are used in this project to configure virtual machines:
- cloud-init: Used to configure Linux virtual machines.
The following operating systems are used for the various virtual machines in the sandbox:
| Virtual Machine | Role | Module | Operating System |
|---|---|---|---|
| jumplinux2 | Linux Jumpbox VM | vm-jumpbox-linux | Ubuntu Server LTS 24.04 (Nobel Numbat) |