How to Use Terraform with Azure and Twingate
This guide provides step-by-step instructions on automating Twingate deployments with Terraform on Azure.
Getting Started - What we are going to build
The goal of this guide is to use Terraform to deploy Twingate on an Azure vNet including all required components (Connector, Remote Network) and Configuration Items (Resource, Group, etc.): First let’s setup a new folder for our Terraform code to reside:
mkdir twingate_azure_democd twingate_azure_demo
All commands below will be run from within the folder created, we can now open this folder in our editor of choice.
Storing Terraform Code
For simplicity and readability all the code will be included in a single main.tf file. For more information on Terraform code structure, please see this guide and this guide.
Setting Up the Terraform Providers
On Terraform Providers
A Terraform Provider is a Terraform Plugin that leverages an external API (like the Twingate API) and makes certain functions available right from within Terraform without having to know anything about the external API itself.
First let’s setup the provider configuration: create a new file called main.tf (amending each value to match your environment/requirements):
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "=3.0.0" } twingate = { source = "twingate/twingate" } random = { source = "hashicorp/random" version = "3.3.2" } }}
Twingate Terraform Provider
The latest Terraform Provider for Twingate is always available here.
We need 2 providers in this case: One for Twingate (it will allow us to create and configure Twingate specific Configuration Items) and one for Azure (it will allow us to spin up the required infrastructure / VPC).
Once this is in place we can run the following command to download and install those providers locally on your system:
terraform init
You should see the provider plugins being initialized and installed successfully:
Initializing the backend...
Initializing provider plugins...- Finding twingate/twingate versions matching "0.1.10"...- Finding latest version of hashicorp/google...- Installing twingate/twingate v0.1.10...- Installed twingate/twingate v0.1.10 (self-signed, key ID E8EBBE80BA0C9369)
Partner and community providers are signed by their developers.If you'd like to know more about provider signing, you can read about it here:https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the providerselections it made above. Include this file in your version control repositoryso that Terraform can guarantee to make the same selections by default whenyou run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running “terraform plan” to see any changes that are required for your infrastructure. All Terraform commands should now work.
If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
Azure Authentication
There are a couple of ways to authenticate with Azure; for simplicity we will be specifying the connection details in variables stored in terraform.tfvars. We encourage you to review the documentation here and pick a method which best suits your environment & needs.
Creating the Twingate infrastructure
To create the Twingate infrastructure we will need a way to authenticate with Twingate. To do this we will create a new API key which we will use with Terraform.
Navigate to Settings → API then Generate a new Token:
You will need to set your token with Read, Write & Provision permissions, but you may want to restrict the allowed IP range to only where you will run your Terraform commands from.
Click on generate and copy the token.
Terraform vars file
Like all programming languages, terraform can use variables to define values. Let’s create a new file called terraform.tfvars, we will use it to define useful variables.
Add the following lines to this file, adding in the value of the API Token into tg_api_key and the name of your Twingate Tenant. (The Tenant is the mycorp
part of the URL to your Twingate Console if the full URL was https://mycorp.twingate.com
)
tg_api_key="Copied API key"tg_network="mycorp"
# You only need the following if you are storing the service principal credentials in this file
subscription_id = "The azure subscription you are using"tenant_id = "The azure tenant ID"client_id = ""client_secret = ""
Using Source Control
If you are using or thinking of using source control, please ensure you exclude this file for security reasons. (You don’t want your API tokens visible in clear text on a public repository!)
We can then add these variables to the main.tf file.
variable "subscription_id" {}variable "tenant_id" {}variable "client_id" {}variable "client_secret" {}
variable "tg_api_key" {}variable "tg_network" {}
Then we can use these variables to configure the Azure and Twingate providers.
provider "azurerm" { features {}
subscription_id = var.subscription_id tenant_id = var.tenant_id client_id = var.client_id client_secret = var.client_secret}
Twingate provider:
provider "twingate" { api_token = var.tg_api_key network = var.tg_network}
The following section of code uses a terraform provider to generate a random string. This string will be used to set the password on the demo virtual machine we are creating. You may wish to use an alternative authentication method, for example SSH keys.
resource "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?"}
Now we can start creating resources in Twingate, let’s first start with the highest level concept: the Twingate Remote Network:
resource "twingate_remote_network" "azure_demo_network" { name = "azure demo remote network"}
Then we need to create the connector.
resource "twingate_connector" "azure_demo_connector" { remote_network_id = twingate_remote_network.azure_demo_network.id}
Some clarification here as well:
- twingate_connector refers to a Connector as per the Terraform provider for Twingate
- remote_network_id is the only parameter required to create a Connector: this is consistent with creating a connector from the Admin Console: you need to attach it to a remote network.
- twingate_remote_network.azure_demo_network.id is read as
<Terraform resource type>
.<Terraform resource name>
.<internal ID of that object>
And finally, we need to create the tokens which the remote connector will use to communicate with Twingate.
resource "twingate_connector_tokens" "twingate_connector_tokens" { connector_id = twingate_connector.azure_demo_connector.id}
It’s a good idea at this point to do a quick check on our Terrform script by running:
terraform plan
On Terraform Plan
Terraform plan is a non destructive command: it runs a simulation of what Terraform needs to do: it is therefore safe to run and is useful towards troubleshooting your code.
You should see a response similar to this:
Terraform will perform the following actions:
# twingate_connector.azure_demo_connector will be created + resource "twingate_connector" "azure_demo_connector" { + id = (known after apply) + name = (known after apply) + remote_network_id = (known after apply) }
# twingate_connector_tokens.twingate_connector_tokens will be created + resource "twingate_connector_tokens" "twingate_connector_tokens" { + access_token = (sensitive value) + connector_id = (known after apply) + id = (known after apply) + refresh_token = (sensitive value) }
# twingate_remote_network.azure_demo_network will be created + resource "twingate_remote_network" "azure_demo_network" { + id = (known after apply) + name = "azure demo remote network" }
Plan: 3 to add, 0 to change, 0 to destroy.
If this is consistent with what you are seeing, we can then move onto doing the same for the Azure infrastructure needed.
Creating the Azure Infrastructure
First we will create a new resource group to “house” this demo.
(Note: this is a very simple demonstration on how we can provision resources in Terraform. You will need to review and change items to suit your environment, for example regions and IP address ranges.)
# Create a resource groupresource "azurerm_resource_group" "rg_twingate_azure_demo" { name = "twingate-azure-demo-rg" location = "West Europe"}
Then we need a new Azure vNet.
# Create a virtual network within the resource groupresource "azurerm_virtual_network" "network_twingate_azure_demo" { name = "twingate-azure-demo-network" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name location = azurerm_resource_group.rg_twingate_azure_demo.location address_space = ["10.0.0.0/16"]}
As per the diagram from the beginning of the article, we need to create 2 subnets for our VM and the container instance, a Container Subnet and a General Subnet (for the VM):
resource "azurerm_subnet" "subnet_twingate_azure_demo_container" { name = "twingate-azure-demo-continaer-subnet" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name virtual_network_name = azurerm_virtual_network.network_twingate_azure_demo.name address_prefixes = ["10.0.2.0/24"]
delegation { name = "delegation"
service_delegation { name = "Microsoft.ContainerInstance/containerGroups" actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action"] } }}
resource "azurerm_subnet" "subnet_twingate_azure_demo" { name = "twingate-azure-demo-subnet" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name virtual_network_name = azurerm_virtual_network.network_twingate_azure_demo.name address_prefixes = ["10.0.1.0/24"]
}
Next we need to create a network profile which will be used by the container instance.
resource "azurerm_network_profile" "twingate_network_profile" { name = "twingatenetprofile" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name
container_network_interface { name = "twingatenic"
ip_configuration { name = "twingateipconfig" subnet_id = azurerm_subnet.subnet_twingate_azure_demo_container.id } }}
Then we can create the container instance:
resource "azurerm_container_group" "twin_uk_container" {
name = "azure-demo-twingate-connector" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name ip_address_type = "Private" network_profile_id = azurerm_network_profile.twingate_network_profile.id os_type = "Linux"
container { name = "twingateconnector" image = "twingate/connector:1" cpu = "1" memory = "1.5" environment_variables = { "TWINGATE_NETWORK" = "${var.tg_network}" "TWINGATE_ACCESS_TOKEN" = twingate_connector_tokens.twingate_connector_tokens.access_token "TWINGATE_REFRESH_TOKEN" = twingate_connector_tokens.twingate_connector_tokens.refresh_token "TWINGATE_TIMESTAMP_FORMAT" = "2" } ports { port = 9999 protocol = "UDP" } }}
The test virtual machine we are creating will require a network interface so let’s also add a Terraform resource for that:
resource "azurerm_network_interface" "azure_twingate_demo_vm_nic" { name = "demo-webserver-nic" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name
ip_configuration { name = "vmnetconfiguration" subnet_id = azurerm_subnet.subnet_twingate_azure_demo.id private_ip_address_allocation = "Dynamic" }}
With all prerequisites in place, we can now create the test VM:
resource "azurerm_virtual_machine" "demo_webserver" { name = "demo-webserver-vm" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name network_interface_ids = [azurerm_network_interface.azure_twingate_demo_vm_nic.id] vm_size = "Standard_B1ls"
# Uncomment this line to delete the OS disk automatically when deleting the VM delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM delete_data_disks_on_termination = true
storage_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } storage_os_disk { name = "myosdisk1" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } os_profile { computer_name = "hostname" admin_username = "testadmin" admin_password = random_password.password.result } os_profile_linux_config { disable_password_authentication = false } tags = { environment = "demo" }}
Finally we can make add the required group and resource configurations to Twingate.
Twingate group:
resource "twingate_group" "azure_demo" { name = "azure demo group"}
Twingate Resource:
resource "twingate_resource" "azure_demo_resource" { name = "azure demo web sever" address = azurerm_network_interface.azure_twingate_demo_vm_nic.private_ip_address remote_network_id = twingate_remote_network.azure_demo_network.id group_ids = [twingate_group.azure_demo.id] protocols { allow_icmp = true tcp { policy = "RESTRICTED" ports = ["80","22"] } udp { policy = "ALLOW_ALL" } }}
(Note: As you can see port 80 and 22 are allowed, you may want to change this depending on your circumstances.)
And finally, we want to output our password and mark it as sensitive:
output "password" { value = random_password.password.result sensitive = true}
Finished Scripts
This completes our Terraform configuration, so your files should look a bit like this:
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "=3.0.0" } twingate = { source = "twingate/twingate" } random = { source = "hashicorp/random" version = "3.3.2" } }}
variable "tg_api_key" {}variable "tg_network" {}
variable "subscription_id" {}variable "tenant_id" {}variable "client_id" {}variable "client_secret" {}
provider "azurerm" { features {}
subscription_id = var.subscription_id tenant_id = var.tenant_id client_id = var.client_id client_secret = var.client_secret}
provider "twingate" { api_token = var.tg_api_key network = var.tg_network}
resource "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?"}
resource "twingate_remote_network" "azure_demo_network" { name = "azure demo remote network"}
resource "twingate_connector" "azure_demo_connector" { remote_network_id = twingate_remote_network.azure_demo_network.id}
resource "twingate_connector_tokens" "twingate_connector_tokens" { connector_id = twingate_connector.azure_demo_connector.id}
# Create a resource groupresource "azurerm_resource_group" "rg_twingate_azure_demo" { name = "twingate-azure-demo-rg" location = "West Europe"}
# Create a virtual network within the resource groupresource "azurerm_virtual_network" "network_twingate_azure_demo" { name = "twingate-azure-demo-network" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name location = azurerm_resource_group.rg_twingate_azure_demo.location address_space = ["10.0.0.0/16"]}
resource "azurerm_subnet" "subnet_twingate_azure_demo_container" { name = "twingate-azure-demo-continaer-subnet" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name virtual_network_name = azurerm_virtual_network.network_twingate_azure_demo.name address_prefixes = ["10.0.2.0/24"]
delegation { name = "delegation"
service_delegation { name = "Microsoft.ContainerInstance/containerGroups" actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action"] } }}
resource "azurerm_subnet" "subnet_twingate_azure_demo" { name = "twingate-azure-demo-subnet" resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name virtual_network_name = azurerm_virtual_network.network_twingate_azure_demo.name address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_profile" "twingate_network_profile" { name = "twingatenetprofile" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name
container_network_interface { name = "twingatenic"
ip_configuration { name = "twingateipconfig" subnet_id = azurerm_subnet.subnet_twingate_azure_demo_container.id } }}
resource "azurerm_container_group" "twin_uk_container" {
name = "azure-demo-twingate-connector" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name ip_address_type = "Private" network_profile_id = azurerm_network_profile.twingate_network_profile.id os_type = "Linux"
container { name = "twingateconnector" image = "twingate/connector:1" cpu = "1" memory = "1.5" environment_variables = { "TWINGATE_NETWORK" = "${var.tg_network}" "TWINGATE_ACCESS_TOKEN" = twingate_connector_tokens.twingate_connector_tokens.access_token "TWINGATE_REFRESH_TOKEN" = twingate_connector_tokens.twingate_connector_tokens.refresh_token "TWINGATE_TIMESTAMP_FORMAT" = "2" } ports { port = 9999 protocol = "UDP" } }}
resource "azurerm_network_interface" "azure_twingate_demo_vm_nic" { name = "demo-webserver-nic" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name
ip_configuration { name = "vmnetconfiguration" subnet_id = azurerm_subnet.subnet_twingate_azure_demo.id private_ip_address_allocation = "Dynamic" }}
resource "azurerm_virtual_machine" "demo_webserver" { name = "demo-webserver-vm" location = azurerm_resource_group.rg_twingate_azure_demo.location resource_group_name = azurerm_resource_group.rg_twingate_azure_demo.name network_interface_ids = [azurerm_network_interface.azure_twingate_demo_vm_nic.id] vm_size = "Standard_B1ls"
# Uncomment this line to delete the OS disk automatically when deleting the VM delete_os_disk_on_termination = true
# Uncomment this line to delete the data disks automatically when deleting the VM delete_data_disks_on_termination = true
storage_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } storage_os_disk { name = "myosdisk1" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } os_profile { computer_name = "hostname" admin_username = "testadmin" admin_password = random_password.password.result } os_profile_linux_config { disable_password_authentication = false } tags = { environment = "demo" }}
resource "twingate_group" "azure_demo" { name = "azure demo group"}
resource "twingate_resource" "azure_demo_resource" { name = "azure demo web sever" address = azurerm_network_interface.azure_twingate_demo_vm_nic.private_ip_address remote_network_id = twingate_remote_network.azure_demo_network.id group_ids = [twingate_group.azure_demo.id] protocols { allow_icmp = true tcp { policy = "RESTRICTED" ports = ["80","22"] } udp { policy = "ALLOW_ALL" } }}
output "password" { value = random_password.password.result sensitive = true}
/ terraform.tfvars
tg_api_key=""tg_network=""
# You only need the following if you are storing the service principal credentials in this file
subscription_id = ""tenant_id = ""client_id = ""client_secret = ""
Deploying It All
We can now run Terraform to check or “plan” our config:
terraform plan
All being well this will run and show you all the resources which will be added to both Twingate and Terraform.
As this is a brand new infrastructure you should not see anything being destroyed. Please make sure you double check what is being added and where!
Once you are happy with what is being created, you can now apply this.
terraform apply
You will need to confirm you are happy to apply the changes:
Plan: 14 to add, 0 to change, 0 to destroy.
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
Enter a value: yes
Then you should see any the resources in both Twingate and Azure being created. This will take a few minutes so now is a good time to reward yourself with a ☕️.
Checking What Has Been Created
Next we want to check our infrastructure has been built and we can connect to the Azure test VM.
If you open the Twingate admin panel, you will see a new network: And a new Connector: and a new Group: and a new Resource: If you then look at the Azure portal, you will see a new resource group with all the resources in it:
Testing our Connection
At this point we need to add a Twingate user to the new group that has been created: After a few moments, you should then see the new resource in your Twingate client. We can now try connecting to connect to the VM via ssh:
You can retrieve the password by using the following command (Although please be aware that passwords are kept in the state file and therefore it is recommended to use an external password safe, for example key vault in Azure.)
terraform output password
And now for the connection via ssh:
ssh testadmin@10.0.1.4testadmin@10.0.1.4's password:Welcome to Ubuntu 16.04.7 LTS (GNU/Linux 4.15.0-1113-azure x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
UA Infra: Extended Security Maintenance (ESM) is not enabled.
0 updates can be applied immediately.
52 additional security updates can be applied with UA Infra: ESMLearn more about enabling UA Infra: ESM service for Ubuntu 16.04 athttps://ubuntu.com/16-04
New release '18.04.6 LTS' available.Run 'do-release-upgrade' to upgrade to it.
Last login: Thu Aug 25 13:42:22 2022 from 10.0.2.4To run a command as administrator (user "root"), use "sudo <command>".See "man sudo_root" for details.
testadmin@hostname:~$
We have now successfully created our infrastructure and we can now take it all down just as quickly.
Tearing It All Down
To remove the infrastructure you just created, just run:
terraform destroy
This will show you what will be destroyed, please check that this is correct before agreeing to the prompt.
You can now create and destroy this Twingate secured infrastructure with just a few keystrokes!
Last updated 8 days ago