In this blog, we will learn how to schedule the provisioning of an Azure Bastion Host using Azure Functions to optimize cloud spend. This blog also covers the provisioning of all resources with Terraform.
Introduction
Azure Bastion
Azure Bastion is a managed platform-as-a-service (PaaS) offering in Microsoft Azure that provides secure and seamless remote access to virtual machines (VMs) over the Secure Shell (SSH) or Remote Desktop Protocol (RDP) without the need for a public IP address or a VPN connection. It is a fully managed platform that simplifies administrative tasks by providing an easy-to-use, browser-based interface for managing VMs.
Azure Bastion provides a more secure way to access VMs by using Remote Desktop Protocol (RDP) and SSH over Transport Layer Security (TLS) instead of opening ports in the network firewall to allow direct connections. It eliminates the need to expose RDP and SSH ports to the public internet and reduces the risk of cyber-attacks such as brute-force attacks, malware, and ransomware.
With Azure Bastion, you can connect to VMs securely from anywhere in the world using just a web browser. Azure Bastion provides a consistent user experience regardless of the location, device, or operating system of the client machine. It supports all major web browsers and does not require any additional software to be installed on the client machine.
Azure Functions
Azure Functions is a serverless compute service offered by Microsoft Azure. It allows developers to create and run event-driven, serverless applications, without the need to manage the underlying infrastructure. With Azure Functions, developers can write code in a variety of programming languages, such as C#, Java, JavaScript, Python, and PowerShell, and deploy it quickly and easily.
Azure Functions is designed for building microservices, which are small, independent, and loosely-coupled services that work together to provide a larger application or system. Each microservice is responsible for a specific task or function, and can be developed, tested, and deployed independently of other microservices. This approach makes it easier to maintain, update, and scale the application, as each microservice can be scaled independently based on its workload.
Azure Functions can be triggered by a variety of events, such as an HTTP request, a message on a queue, a change in a database, or a timer. When a function is triggered, it runs in a stateless container and performs its specified task. Azure Functions can be integrated with other Azure services, such as Azure Storage, Azure Event Hubs, and Azure Service Bus, as well as with third-party services and APIs.
Automated Bastion Host Provisioning
In the following steps, we are going to create a straightforward virtual network in Azure, with a subnet dedicated to Azure Bastion. We will also set up an Azure Function with a managed identity and a storage account, and develop two functions within it. The first function will be responsible for deploying a Bastion Host every weekday at 8:30 AM, while the second function will be tasked with removing that same Bastion Host every weekday at 6:00 PM. These functions will automate the deployment and management of the Bastion Host, saving time and effort for the IT team. The managed identity will provide secure access to the resource group and ensure that the functions can run securely without requiring any additional authentication.
Overall, these steps will demonstrate the power of Azure’s services and show how they can be used to automate routine tasks and improve IT efficiency.
Prerequisites
Let’s get started with automating the provisioning of a Bastion Host with Azure Functions. We’ll deploy the components with Terraform. How to use Terraform or build CI/CD pipelines will not be covered in this blog article. Before you can start deploying, there are some prerequisites to fulfill:
- Azure tenant and user account: You’ll need an Azure tenant, an Azure Active Directory (Azure AD) instance. This instance is the foundation of the environment. And it allows you to create an identity (user account) to connect to Azure, set up the environment, and deploy the resources.
- Subscriptions: You’ll need a subscription and owner permissions to deploy the resources.
- Terraform: You’ll need the Terraform command-line interface to deploy and manage Azure resources. You can find more information about Terraform and the AzureRM provider in the documentation.
Once you have fulfilled the prerequisites, we are ready to move forward and deploy and automate the provisioning!
Variables
First, download the Terraform files from this GitHub repository for the rest of this guide.
Review the files and adjust where necessary. Specifically the following variables in the variables.tf file apply to the following resources:
- ServiceName: This describes the name of the service in the defined naming convention, which is {ResourceType}-{ServiceName}-{EnvironmentPrefix}-###. This applies to all resources.
- EnvironmentPrefix: This describes the prefix of all resources in the defined naming convention, which is {ResourceType}-{ServiceName}-{EnvironmentPrefix}-###, referring to environment type. For example, choose p for production, t for testing and a for acceptance.
- Location: This value defines the selected region the resource is being deployed, for all resources being deployed.
- VNetPrefixSecondOctet: This value defines the second octet of the IP address space for the Virtual Network. For example: if you need a address space of 172.20.8.0/22, choose 20.
- VNetPrefixThirdOctet: This value defines the third octet IP address space for the Virtual Network. For example: if you need a address space of 172.20.8.0/22, choose 8.
- Timezone: This value defines the timezone for your Azure Function to be in, and is actually representing the
WEBSITE_TIME_ZONE
value in your Function. - CreateSchedule: This value defines the schedule when the Bastion Host should be created. It is a NCRONTAB expression, which is explained here in the Microsoft documentation.
- DeleteSchedule: This value defines the schedule when the Bastion Host should be deleted. It is a NCRONTAB expression, which is explained here in the Microsoft documentation.
Resources
When executing the terraform plan
and terraform apply
commands after adjusting your variables.tf file, the following resources are being created:
- Resource Group: A resource group is being created. This group is being used to store all resources that are created as part of this guide.
- Virtual Network: A /22 virtual network is being created within the resource group.
- Subnet: A /26 subnet named AzureBastionSubnet is being created in that same virtual network.
- Public IP: A static public IP address with the Standard sku is being created, which will be used as public IP address for the Bastion Host.
- Storage Account: When creating a function app, you must create or link to a general-purpose Azure Storage account that supports Blob, Queue, and Table storage. This requirement exists because Functions relies on Azure Storage for operations such as managing triggers and logging function executions. In our case we create a standard storage account, with the LRS replication type.
- Service Plan: A service plan, which represents the Azure Function hosting plan, is being created within the resource group. In this case, a dynamic consumption-based plan is being deployed with the Windows OS type.
- Function App: A function app, which is linked with the mentioned storage account and service plan. The function app is provided with a (SystemAssigned) managed identity. This identity is given the “Contributor” rights to the resource group, so it is authorized to deploy the Bastion Host. In addition, the function app is provided with some environment variables for proper operation, and configured as a PowerShell function app.
- Create Function: A function, created within the function app, named Create-Azure-Bastion. This function contains a PowerShell script, which switches to the context of the current subscription, and according to the specified schedule (specified in the variables.tf file) provisiones a Bastion Host.
- Delete Function: A function, created within the function app, named Delete-Azure-Bastion. This function contains a PowerShell script, which switches to the context of the current subscription, and according to the specified schedule (specified in the variables.tf file) deletes the provisioned Bastion Host.
The specified variables for the schedule can be found in the Azure portal in the function’s function.json file (see screenshot above). The PowerShell script can be found in the run.ps1 file. You can look into these files to go to your Function App resource, click Functions in the left menu, and click the specific function. You don’t have to adjust anything here.
If you want to test the function, you need to adjust the schedule. The schedule is passed as a parameter in the PowerShell script. Running the script manually, at a time that does not match the schedule, will never result in the provisioning of a Bastion Host!
Dependencies
The PowerShell script relies on two Azure PowerShell modules, Az.Accounts and Az.Network. These dependencies must be specified in the requirements.psd1 under App files in the function app, so that they are downloaded before the script is executed.
We will add these dependencies manually. First, go into your host.json file (see screenshot above). You can look into this file to go to your Function App resource, click App files in the left menu, and choose the host.json file from the dropdown-menu. Important here is that the managedDependency
value is set to True
, as seen on the screenshot above.
Now, go into your requirements.psd1 file (see screenshot above). Make sure this file contains the Az.Accounts and Az.Network PowerShell-modules with the (major) version numbers. In my case, the file looks like below:
# This file enables modules to be automatically managed by the Functions service.
# See https://aka.ms/functionsmanageddependency for additional information.
#
@{
# For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'.
# To use the Az module in your function app, please uncomment the line below.
#'Az' = '9.*'
'Az.Accounts' = '2.*'
'Az.Network' = '5.*'
}
After making the above changes, it is important that you restart the function app. If you have successfully completed all steps, a Bastion Host will be provisioned at the next time according to the specified schedule.
Closing words
In Microsoft Azure, temporarily pausing or deprovisioning a resource during periods when it is not needed, is a great way to save costs. Using Azure Functions, it is possible to automate the provisioning and deprovisioning of resources during office hours. In this article we used a time-based triggered Azure Function to automatically provision a Bastion host at the start of office hours, and deprovision it at the end of office hours. We saved up approx. € 124,67 per month, by running the Bastion Host 264 hours a month instead of 730 hours a month. Overall, the concept of “snoozing” in Azure, is a powerful way to reduce costs and optimize resource utilization, and can be easily automated using Azure Functions. To learn more about the topics that were covered in this blog article, refer to the links below:
- Azure Functions PowerShell developer guide
- Azure PowerShell modules
- Deploy Bastion using Azure PowerShell
- host.json reference for Azure Functions 2.x and later
- Terraform azurerm_windows_function_app
Thank you for taking the time to go through this post and making it to the end. Stay tuned because we’ll keep continuing providing more content on topics like these in the future.