This basic implementation of Azure Virtual Desktop will be deployed using Terraform for Azure resource provisioning, Ansible for Windows session host configuration, and GitHub Actions to orchestrate it all.
Only the resources resources in the Azure Virtual Desktop Resource Group (illustrated in the middle of the diagram below) are within scope for this deployment.
To deploy this demo AVD solution within your environment, I am assuming you have the following resources in place:
- A Windows Active Directory Domain Services Domain Controller or Azure Active Directory Domain Services deployed in Azure.
- A GitHub Account to clone this repo or create a new repo from this template.
- A Ubuntu Virtual Machine deployed in Azure with the following tools installed:
The Terraform script will dynamically peer the AVD virtual network with any virtual network with the tag
role=azops
so make sure the virtual networks that host your AD and DEVOPS machines are tagged as such.
The Terraform configuration will generate a "random pet name" to be used in naming Azure resources. This is fun for a demo, but you can change this as needed.
All Azure resources will be named using a naming convention of 2-4 character code based on the Azure service as the name prefix followed by a dash and the "random pet name". Again, if you don't like these names, you should change it.
This deployment also assumes you have full control of your subscription and have the proper permissions to create two sets of Azure Virtual Network Peerings; one between the AVD Virtual Network and AADDS Virtual Network and another between the AVD Virtual Network and DevOps virtual networks. If you don't have this level of access, you'll need to re-evaluate how much of this you can automate. I am working out of my sandbox Azure subscription and do not have limitations which you may run into in a production scenario.
The following resources will be deployed using Terraform:
- Azure Resource Group
- Azure Virtual Network with a single subnet and Network Security Group wrapped around it
The virtual network will also have custom DNS configured so that your AVD session host VM can communicate with the domain controller when it comes time to domain join.
- Azure Virtual Network peerings to and from AVD virtual network for AADDS and DevOps
- Azure Virtual Desktop Host Pool and the host pool registration token will be exported as an output in the Terraform configuration
- Azure Virtual Desktop Application Group
- Azure Virtual Desktop Workspace
- Windows Virtual Machine(s) with a Custom Script Extension to configure WinRM for Ansible
- A local Ansible inventory file which will include host name and IP to run the Ansible playbook against
Terraform requires you to manage state files. You can choose to store remote state in Azure Storage Account or in Terraform Cloud. Whichever solution you choose, be sure to update the backend.tf
file to reflect your remote state solution. This repo uses Terraform Cloud and for the GitHub Action to work with your Terraform Cloud account, you will need to create an API token and save it as a GitHub Secret named TF_API_TOKEN
. The backend.tf
file is a partial configuration. The rest of my backend configuration is kept in a file called config.remote.tfbackend
per Terraform's recommended naming pattern. This file is purposely NOT commited to the repo and passed in during terraform init
(at runtime).
In Terraform Cloud, you also have the option of running your Terraform script on Hashicorp's cloud infrastructure, but I chose to run it locally on the GitHub self-hosted runner installed on my DevOps VM in Azure. This will enable the GitHub Action to use the Ansible inventory file when it comes time to call the Ansible playbook and reach AVD Session Host VMs by private IP as the self-hosted runner is deployed in a peered virtual network. Taking this route will require the use of an Azure Service Principal to run the Terraform commands. Once you have the secret values, enter them in GitHub Secrets using the following names
ARM_CLIENT_ID
ARM_CLIENT_SECRET
ARM_SUBSCRIPTION_ID
ARM_TENANT_ID
NOTE: You could name the GitHub secrets anything you want but you'll need to make sure they are consistent with what is in the
terraform.yml
workflow file.
This repo also includes variables for re-usability. The variable definitions can be found in the variables.tf
file. The vaules for each deployment are maintained in a *.tfvars
file and I've included a sample.tfvars
file so you will need to update based on what is deployed in your environment.
To run the Terrafrom script locally, take a look at the terraform.yml
workflow file. There you'll find a terraform plan
and terraform apply
command with all the arguments you'll need.
NOTE: If you decide to change the name of the sample.tfvars file, you'll also need to update the filename in the workflow.
You may run into a situation where the virtual networks you need to peer with are in different subscriptions. In my lab environment, I have a DevOps VM in a DevOps subscription and a AD DS VM in a Network Operations subscription (Hub Virtual Network or NetOps). In order for this pipeline to run successfully, it will need to be able to peer out to the virtual networks in the external subscriptions and peer back in from the virtual networks in the external subscriptions.
To do this, we'll use separate AzureRM providers for each subscription we need to interact with: one for DevOps and another for NetOps. The providers will be aliased and given a subscription id. Be sure to set the subscription id in your *.tfvars file. If your DevOps and NetOps resources are in the same subscription, you should be able to simply pass in the same subscription id for each.
When Terraform does the peering, it will query the DevOps and NetOps subscriptions for any virtual networks that have a specific tag. This way we can dynamically peer with any virtual network and not have to hard-code resource ids in the *.tfvars file. So make sure your DevOps and NetOps virtual networks have been tagged. I used role=azops
as a key-value pair.
The site.yml
Ansible Playbook found in this repo relies on a few variables needed to connect to your VM, install the RDSAgent software for registering it as a AVD Session Host, and performing a domain join. Rather then saving credentials to the repo (which is never a good thing), we'll use ansible-vault
to encrypt contents leveraging Ansible Vault. The encrypted vault will be commited to the repo as secrets.yml
.
NOTE:
secrets.yml
file in this repo contains info specific to my deployment so you'll need to overwrite it with your own.
Let's start by creating a vault file:
ansible-vault create secrets.yml
You will be prompted for a password. Enter a super-secret password. Make it hard to brute force ;-)
NOTE: You will also need to save the vault password as a GitHub repo Secret named
ANSIBLE_VAULT_PASSWORD
for the GitHub Action workflow to use.
After the vault password has been set, a VI editor will open.
NOTE: Be sure to hit the
i
key to put yourself ininsert
mode and enter the following:
ansible_user: <YOUR_VM_USERNAME>
ansible_password: <YOUR_VM_PASSWORD>
dns_domain_name: <YOUR_DOMAIN_NAME>
domain_admin_user: <YOUR_DOMAIN_USERNAME>
domain_admin_password: <YOUR_DOMAIN_PASSWORD>
domain_ou_path: <YOUR_DOMAIN_DISTINGUISHED_OU_PATH>
NOTE: Save the file using the following command
:wq!
If you need to update the vault, you can run the following command to edit the file:
ansible-vault edit secrets.yml
NOTE: You will be prompted to enter your vault password
With the vault file saved to the repo, the GitHub Action workflow will use the ANSIBLE_VAULT_PASSWORD
to unlock the vault when the Ansible playbook is invoked.
To view the Ansible playbook command, take a look at the terraform.yml
workflow file and look for the Ansible Playbook task. You'll see that the command passes extra variables for the Host Pool registratin token and passes the variables found in secrets.yml
vault file into the playbook.
More on Ansible Secrets here:
If you configured all the GitHub secrets (listed in steps above), you will see a GitHub Action workflow running each time you do a push or pull request into the main branch. At this point, there's nothing else you need to do here. Now, go watch it run and have fun!!
When you are ready to clean things up, you can run the following command:
terraform destroy -var-file=sample.tfvars -var=username=user -var=password=pass