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_demo
cd 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.

Setting Up the Terraform Providers

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"
}
}
}

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 provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you 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.

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 = ""

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

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 group
resource "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 group
resource "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 group
resource "azurerm_resource_group" "rg_twingate_azure_demo" {
name = "twingate-azure-demo-rg"
location = "West Europe"
}
# Create a virtual network within the resource group
resource "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.

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.4
testadmin@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: ESM
Learn more about enabling UA Infra: ESM service for Ubuntu 16.04 at
https://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.4
To 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 1 day ago