Azure Bastion provides a critical security layer for organizations managing virtual machines (VMs) in the cloud. By facilitating secure RDP (Remote Desktop Protocol) and SSH (Secure Shell) connectivity directly through the Azure portal, it eliminates the need to expose virtual machines to the public internet via public IP addresses or to maintain a complex VPN infrastructure. When integrated with Terraform, the Infrastructure as Code (IaC) tool developed by HashiCorp, the deployment of Azure Bastion becomes a repeatable, version-controlled process that ensures consistency across development, staging, and production environments.
The core value proposition of Azure Bastion is the reduction of the attack surface. Traditional remote access often requires opening port 3389 for RDP or port 22 for SSH on the VM's network security group (NSG), which leaves the machine vulnerable to brute-force attacks and scanning bots. Azure Bastion acts as a managed proxy, allowing administrators to connect to their VMs using HTML5-based browser sessions, ensuring that the VM remains locked down within a private network.
Understanding the Azure Bastion Architecture
To deploy Azure Bastion using Terraform, one must first understand the underlying resource dependencies. Azure Bastion is not a standalone service but is provisioned directly into a virtual network (VNet). This allows it to support all virtual machines residing within that network.
The architecture required for a successful deployment consists of several interlocking components:
- Resource Group: A logical container that holds all the related resources for the Bastion deployment.
- Virtual Network (VNet): The private network environment where the Bastion host and the target VMs reside.
- AzureBastionSubnet: A dedicated subnet specifically reserved for the Bastion service. This subnet must be named exactly
AzureBastionSubnetfor the service to function. - Public IP Address: A static, standard SKU public IP that serves as the entry point for the Bastion service.
- Azure Bastion Host: The actual compute resource that manages the RDP and SSH sessions.
When deploying via automation tools like Terraform, the service defaults to the Standard SKU. For those seeking a lower-cost entry point for development purposes, the Bastion Developer SKU is available, though it is typically configured through the Azure portal rather than the automated quickstart paths.
Technical Requirements and Prerequisites
Before initiating the Terraform workflow, several administrative and technical prerequisites must be met to ensure the deployment does not fail during the terraform apply phase.
Administrative Permissions
The user executing the Terraform commands must have Owner privileges on the Azure subscription. This is necessary because the deployment creates networking infrastructure, assigns public IP addresses, and manages resource groups, all of which require high-level RBAC (Role-Based Access Control) permissions.
Technical Tooling
The environment from which Terraform is run—whether a local workstation or the Azure Cloud Shell—must have the following installed and configured:
- Terraform CLI: The core engine used to execute HCL (HashiCorp Configuration Language) files.
- Azure CLI: Required for authentication and for verifying the deployment using commands like
az network bastion show. - SSH Key Pair: For deployments involving Linux servers (e.g., Ubuntu 20.04), an SSH key pair is required. By default, Terraform configurations typically look for the public key at
~/.ssh/id_rsa.pub.
Infrastructure Specifications
| Component | Requirement | Specification/Detail |
|---|---|---|
| Subnet Name | Mandatory | Must be AzureBastionSubnet |
| Public IP SKU | Mandatory | Standard |
| Public IP Allocation | Mandatory | Static |
| Terraform Provider | Required | azurerm version ~>3.0 |
| Random Provider | Required | random version ~>3.0 |
Terraform Configuration Implementation
Implementing Azure Bastion involves creating a modular set of HCL files. This separation of concerns—providers, variables, main logic, and outputs—is a best practice in DevOps to ensure the code is maintainable and scalable.
Provider Configuration (providers.tf)
The provider block tells Terraform which plugins are needed to interact with the Azure API. For a Bastion deployment, the azurerm provider is essential, along with the random provider for generating unique resource names.
```hcl
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~>3.0"
}
}
}
provider "azurerm" {
features {}
}
```
Variable Definitions (variables.tf)
Variables allow the configuration to be flexible. Instead of hard-coding values, administrators can define the resource group location, virtual network address space, and subnet prefixes. This allows the same code to be used across different Azure regions.
Resource Logic (main.tf)
The main.tf file contains the actual resource definitions. The following sequence is critical: the Resource Group must be created first, followed by the Virtual Network, then the specific Bastion Subnet, then the Public IP, and finally the Bastion Host itself.
The definition for the AzureBastionSubnet is particularly strict. It must be assigned a specific address prefix (for example, 10.0.1.0/24) to accommodate the Bastion nodes.
```hcl
Create the Bastion Subnet
resource "azurermsubnet" "bastionsubnet" {
name = "AzureBastionSubnet"
resourcegroupname = azurermresourcegroup.rg.name
virtualnetworkname = azurermvirtualnetwork.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
Create Public IP for Azure Bastion
resource "azurermpublicip" "bastionpip" {
name = "example-pip"
location = azurermresourcegroup.rg.location
resourcegroupname = azurermresourcegroup.rg.name
allocationmethod = "Static"
sku = "Standard"
}
Create Azure Bastion Host
resource "azurermbastionhost" "bastion" {
name = "example-bastion"
location = azurermresourcegroup.rg.location
resourcegroupname = azurermresourcegroup.rg.name
ipconfiguration {
name = "configuration"
subnetid = azurermsubnet.bastionsubnet.id
publicipaddressid = azurermpublicip.bastionpip.id
}
}
```
Defining Outputs (outputs.tf)
To avoid hunting through the Azure portal to find the details of the deployed infrastructure, the outputs.tf file exports critical data points. These outputs can be captured by other automation scripts or used to verify the deployment.
```hcl
output "resourcegroupname" {
value = azurermresourcegroup.rg.name
}
output "bastionhostname" {
value = azurermbastionhost.bastion.name
}
output "bastionhostip" {
value = azurermpublicip.bastionpip.ipaddress
}
```
Deployment Workflow and Execution
The lifecycle of a Terraform deployment follows a strict sequence of initialization, planning, and application. This process ensures that the desired state described in the HCL files is mirrored exactly in the Azure cloud environment.
Step-by-Step Execution Process
Environment Setup:
Create a dedicated directory for the project and move into it. If using a pre-made example from a repository, the process begins with cloning:git clone https://github.com/terraform-azurerm-examples/bastioncd bastion
Initialization:
Runterraform init. This command downloads the necessary provider plugins (AzureRM and Random) from the Terraform Registry and initializes the backend.Variable Customization (Optional):
Users can create aterraform.tfvarsfile to override default values. This is where sensitive information or environment-specific settings are placed. For instance, if a Windows password is not specified for an administrator account, Terraform may generate one and store it in a key vault alongside the SSH private key. This is intended as a "break glass" scenario, as the primary intended access method is through Azure Active Directory (AAD) authentication.Execution Plan:
Runterraform plan. Terraform compares the current state of the Azure environment with the configuration files and generates an execution plan. This allows the engineer to preview exactly which resources will be created, modified, or destroyed before any changes are committed.Deployment:
Runterraform apply. This command executes the plan. Terraform will call the Azure APIs to provision the Resource Group, VNet, Subnet, Public IP, and the Bastion Host.
Post-Deployment Verification and Management
Once the terraform apply command completes, the resources are live. However, technical verification is required to ensure the Bastion host is operational and correctly configured.
Using the Azure CLI for Verification
The Azure CLI can be used to retrieve the deployed resources using the output variables generated by Terraform.
Capture Resource Details:
bash resource_group_name=$(terraform output -raw resource_group_name) bastion_host_name=$(terraform output -raw bastion_host_name) bastion_host_ip=$(terraform output -raw bastion_host_ip)Inspect Bastion State:
Run the following command to see the detailed configuration of the Bastion host:
bash az network bastion show --name $bastion_host_name --resource-group $resource_group_name
Using PowerShell for Verification
For those working in a Windows-centric environment, the Azure PowerShell module provides a similar verification capability:
powershell
Get-AzBastionHost -ResourceGroupName $resource_group_name -Name $bastion_host_name
Advanced Scenarios: Multi-OS Environments and AAD Integration
A robust Bastion deployment often involves connecting to a variety of operating systems. For example, an environment might contain a Windows 2022 Azure Edition server for Active Directory management and an Ubuntu 20.04 server for web hosting.
AAD Extensions and Authentication
Modern Azure deployments prioritize Azure Active Directory (AAD) for authentication over local administrator passwords. By configuring AAD extensions on both Windows and Linux VMs, administrators can utilize their corporate credentials to gain access.
In a Terraform-led deployment:
- AAD object IDs for Virtual Machine Administrator or User Login roles are assigned at the resource group level.
- If passwords are generated by Terraform, they are stored securely in an Azure Key Vault to prevent exposure in plain text logs.
- Native SSH and RDP clients can still be used through the Bastion tunnel, with Terraform providing the necessary connection commands as outputs.
Comparison of Bastion SKUs for Terraform Deployments
When choosing how to deploy Azure Bastion, it is important to understand the differences between the available SKUs, as this affects both the Terraform configuration and the cost.
| SKU | Deployment Method | Primary Use Case | Feature Set |
|---|---|---|---|
| Standard | Terraform / Portal | Production Environments | Full feature set, high availability |
| Developer | Azure Portal | Testing / Dev Workloads | Basic connectivity, cost-optimized |
Note that when deploying "automatically" through the provided Terraform quickstart patterns, the Standard SKU is the default. The Developer SKU is specifically targeted for those who can manage their deployment through the manual portal interface.
Conclusion
Deploying Azure Bastion via Terraform transforms a complex networking task into a streamlined, programmatic operation. By enforcing the use of a dedicated AzureBastionSubnet and a Standard SKU public IP, the infrastructure is hardened against external threats from the moment of creation. The transition from manual VM access—which requires risky public IP exposure—to a managed proxy service reduces the attack surface significantly and simplifies the audit trail for administrative access.
The synergy between Terraform's state management and Azure's managed Bastion service allows organizations to scale their remote access capabilities without sacrificing security. Whether deploying a simple lab with a single Ubuntu 20.04 server or a massive enterprise environment with hundreds of Windows 2022 Azure Edition instances, the use of HCL ensures that the environment remains reproducible. The ability to integrate AAD authentication further enhances the security posture, moving the organization toward a Zero Trust architecture where identity, rather than network location, is the primary perimeter.