How to Use Pulumi with Azure and Twingate
This guide provides step-by-step instructions on automating Twingate deployments with Pulumi on Microsoft Azure.
Open Source Contribution
The python CLI tool is an Open Source project developed and maintained outside of our product engineering teams. For support regarding this tool please visit the Github issues page.
Prerequisites
This guide assumes the following (on top of prerequisites for all Pulumi guides):
- You have a Azure account setup and an account with relevant access to create and delete Azure resources
- You are using an operating system that supports Bash
Additional practical examples
Beyond this guide, our team regularly releases new examples for Pulumi and Azure which you can find in our GitHub repository.
1. Set up the environment
First, create a new folder for our Pulumi code to reside:
mkdir twingate_pulumi_azure_democd twingate_pulumi_azure_demo
Next, we can use a template to set up the environment:
pulumi new typescript
You will be prompted to enter some values, which can be whatever you want:
project name: demoproject description: Deploying Twingate using Pulumistack name: azure
Once the project is set up, open the folder you created in your favorite code editor.
2. Install Node Modules
As we are using Typescript, Pulumi will use node.js modules to build the infrastructure for both Azure and Twingate. Install the following node.js modules:
npm install @pulumi/azure-nativenpm install @pulumi/azurenpm install @twingate/pulumi-twingate
3. Set up Azure Access for Pulumi
Connect Pulumi to your Azure environment:
az loginaz account listaz account set --subscription=<id>
4. Configuration
Pulumi requires a Twingate API key along with the name of the Twingate tenant we will be configuring.
You can generate a new API key from your Twingate account by following these instructions.
The name of your Twingate tenant is the initial portion of your Twingate URL, e.g. mycorp
in mycorp.twingate.com
.
Let’s store these two values in the Pulumi configuration:
pulumi config set twingate:network <younetwork>pulumi config set twingate:apiToken <yourToken> --secret
Managing Secrets
Although Pulumi encrypts any values labelled as secret, it is always good practice to exclude these type of files from source control. In this example, Pulumi has created a file called Pulumi.demo.yaml
which is used to store the encrypted secret.
Configure variables for our test virtual machine
Azure Password Requirements
Make sure your the password meet the Azure Password Requirements
pulumi config set twingate_pulumi_azure_demo:username tgadminpulumi config set twingate_pulumi_azure_demo:password --secret <superSecurePassword>
Configure Azure
You can change the region used for your Azure configuration, if desired:
pulumi config set azure-native:location uksouth
Write the configuration
Open the index.ts
file and import the modules we will be using:
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";import * as azure_native from "@pulumi/azure-native";
import * as twingate from "@twingate/pulumi-twingate";
Let’s also set some variables we will use when spinning up the VMs:
const config = new pulumi.Config();const username = config.require("username");const password = config.requireSecret("password");
Create Twingate Resources
Next, let’s create the components corresponding to our Twingate Connector and Twingate Group:
const tgazureNetwork = new twingate.TwingateRemoteNetwork("twingate-azure-demo", { name: "twingate-azure-demo",});
const tgazureConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tgazureNetwork.id });
const tgazureConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", { connectorId: tgazureConnector.id,});
const tggroup = new twingate.TwingateGroup("twingateGroup", { name: "azure demo group",});
Create an Azure Resource Group
Next, let’s create an Azure Resource Group which will be made up of a Connector VM and test VM:
const ResourceGroup = new azure.core.ResourceGroup("ResourceGroup", { location: "UK South" });
const resourceGroupName = ResourceGroup.name;
Set up an Azure Public IP
Next, let’s create the networking components in Azure by setting up a public IP address used by the Twingate Connector VM:
const PublicIp = new azure.network.PublicIp("PublicIp", { location: ResourceGroup.location, resourceGroupName: ResourceGroup.name, allocationMethod: "Static", sku: "Standard", zones: ["1"],});
Define an Azure Virtual Network
Next, let’s define a virtual network in Azure to host all components:
const virtualNetwork = new azure_native.network.VirtualNetwork("server-network", { resourceGroupName, addressSpace: { addressPrefixes: ["10.0.0.0/16"] }, subnets: [ { name: "default", addressPrefix: "10.0.1.0/24", }, ],});
Create an Azure Security Group (optional)
For this guide, we will create a group allowing inbound communication on the default ssh port (22
) to test connectivity.
const networkSecurityGroup = new azure_native.network.NetworkSecurityGroup("networkSecurityGroup", { location: ResourceGroup.location, networkSecurityGroupName: "testnsg", resourceGroupName: ResourceGroup.name, securityRules: [ { access: "Allow", destinationAddressPrefix: "*", direction: "Inbound", protocol: "*", destinationPortRanges: ["22", "80"], name: "SSHandHTTP", sourceAddressPrefix: "88.98.90.108/32", sourcePortRange: "*", priority: 100, }, ],});
Configure the Web Server test virtual machine
Declare a new network interface
Let’s declare a new network interface that will be used for the test VM that we will be accessing via Twingate:
const networkInterface_vm = new azure_native.network.NetworkInterface("server-nic-vm", { resourceGroupName, ipConfigurations: [ { name: "webserveripcfg", subnet: virtualNetwork.subnets.apply((subnet) => subnet![0]), privateIPAllocationMethod: azure_native.network.IPAllocationMethod.Dynamic, }, ],});
Create a startup script for web server
The startup script for our test VM is very simple - it listens to port 80 and returns a Hello, World!
message:
const initScript = pulumi.interpolate`#!/bin/bash\necho "Hello, World!" > index.htmlnohup python -m SimpleHTTPServer 80 &`;
Create the web server virtual machine
const vm = new azure_native.compute.VirtualMachine("server-vm", { resourceGroupName, networkProfile: { networkInterfaces: [{ id: networkInterface_vm.id }], }, hardwareProfile: { vmSize: azure_native.compute.VirtualMachineSizeTypes.Standard_B1ms, }, osProfile: { computerName: "VM", customData: initScript.apply((x) => Buffer.from(x).toString("base64")), adminUsername: username, adminPassword: password, linuxConfiguration: { disablePasswordAuthentication: false, }, }, storageProfile: { osDisk: { createOption: azure_native.compute.DiskCreateOption.FromImage, name: "server-vm-myosdisk", deleteOption: "Delete", }, imageReference: { publisher: "canonical", offer: "UbuntuServer", sku: "16.04-LTS", version: "latest", }, },});
Configure the Twingate Connector Virtual Machine
The machine we will configure below will be hosting the Twingate Connector.
Configure the network interface
const networkInterface_tg = new azure_native.network.NetworkInterface("server-nic-tg", { resourceGroupName, ipConfigurations: [ { name: "tgserveripcfg", subnet: virtualNetwork.subnets.apply((subnet) => subnet![0]), privateIPAllocationMethod: azure_native.network.IPAllocationMethod.Dynamic, publicIPAddress: { id: PublicIp.id }, }, ], networkSecurityGroup: { id: networkSecurityGroup.id, },});
Create a startup script
Our startup script will deploy a Connector and use the Connector tokens defined earlier.
const startupScript = pulumi.interpolate`#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | sudo TWINGATE_ACCESS_TOKEN="${tgazureConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tgazureConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;
Create the Twingate Connector virtual machine
Let’s now bundle it all together to create our Twingate Connector VM:
const tgconnector = new azure_native.compute.VirtualMachine("connector-vm", { resourceGroupName, networkProfile: { networkInterfaces: [{ id: networkInterface_tg.id }], }, hardwareProfile: { vmSize: azure_native.compute.VirtualMachineSizeTypes.Standard_B1ms, }, osProfile: { computerName: "TG-CONNECTOR", adminUsername: username, adminPassword: password, linuxConfiguration: { disablePasswordAuthentication: false, }, }, storageProfile: { osDisk: { createOption: azure_native.compute.DiskCreateOption.FromImage, name: "connector-vm-myosdisk1", deleteOption: "Delete", }, imageReference: { offer: "0001-com-ubuntu-server-jammy", publisher: "Canonical", sku: "22_04-lts-gen2", version: "latest", }, }, userData: startupScript.apply((x) => Buffer.from(x).toString("base64")),});
Configure Twingate to allow access to the test web server
Let’s get the new private IP address of the web server VM. We will need it to automatically create a corresponding Twingate Resource:
const privIP = networkInterface_vm.ipConfigurations.apply((x) => { return x != undefined ? x[0].privateIPAddress : null;});
Create a new Twingate Resource
Let’s now create a Resource in Twingate for our test VM. Don’t forget to allow ports 22
and 80
:
const tgresource = new twingate.TwingateResource("resource", { name: "Azure demo server", address: pulumi.interpolate`${privIP}`, remoteNetworkId: tgazureNetwork.id, accessGroups: [ { groupId: tggroup.id, }, ], protocols: { allowIcmp: true, tcp: { policy: "RESTRICTED", ports: ["22", "80"], }, udp: { policy: "ALLOW_ALL", }, },});
Final Code
We are now done with writing code! The final version of your index.ts
should look like the following:
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";import * as azure_native from "@pulumi/azure-native";
import * as twingate from "@twingate/pulumi-twingate";
const config = new pulumi.Config();const username = config.require("username");const password = config.requireSecret("password");
const tgazureNetwork = new twingate.TwingateRemoteNetwork("twingate-azure-demo", { name: "twingate-azure-demo",});
const tgazureConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tgazureNetwork.id });
const tgazureConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", { connectorId: tgazureConnector.id,});
const tggroup = new twingate.TwingateGroup("twingateGroup", { name: "azure demo group",});
const ResourceGroup = new azure.core.ResourceGroup("ResourceGroup", { location: "UK South" });
const resourceGroupName = ResourceGroup.name;
const PublicIp = new azure.network.PublicIp("PublicIp", { location: ResourceGroup.location, resourceGroupName: ResourceGroup.name, allocationMethod: "Static", sku: "Standard", zones: ["1"],});
const virtualNetwork = new azure_native.network.VirtualNetwork("server-network", { resourceGroupName, addressSpace: { addressPrefixes: ["10.0.0.0/16"] }, subnets: [ { name: "default", addressPrefix: "10.0.1.0/24", }, ],});
const networkSecurityGroup = new azure_native.network.NetworkSecurityGroup("networkSecurityGroup", { location: ResourceGroup.location, networkSecurityGroupName: "testnsg", resourceGroupName: ResourceGroup.name, securityRules: [ { access: "Allow", destinationAddressPrefix: "*", direction: "Inbound", protocol: "*", destinationPortRanges: ["22", "80"], name: "SSHandHTTP", sourceAddressPrefix: "88.98.90.108/32", sourcePortRange: "*", priority: 100, }, ],});
const networkInterface_vm = new azure_native.network.NetworkInterface("server-nic-vm", { resourceGroupName, ipConfigurations: [ { name: "webserveripcfg", subnet: virtualNetwork.subnets.apply((subnet) => subnet![0]), privateIPAllocationMethod: azure_native.network.IPAllocationMethod.Dynamic, }, ],});
const initScript = pulumi.interpolate`#!/bin/bash\necho "Hello, World!" > index.htmlnohup python -m SimpleHTTPServer 80 &`;
const vm = new azure_native.compute.VirtualMachine("server-vm", { resourceGroupName, networkProfile: { networkInterfaces: [{ id: networkInterface_vm.id }], }, hardwareProfile: { vmSize: azure_native.compute.VirtualMachineSizeTypes.Standard_B1ms, }, osProfile: { computerName: "VM", customData: initScript.apply((x) => Buffer.from(x).toString("base64")), adminUsername: username, adminPassword: password, linuxConfiguration: { disablePasswordAuthentication: false, }, }, storageProfile: { osDisk: { createOption: azure_native.compute.DiskCreateOption.FromImage, name: "server-vm-myosdisk", deleteOption: "Delete", }, imageReference: { publisher: "canonical", offer: "UbuntuServer", sku: "16.04-LTS", version: "latest", }, },});
const networkInterface_tg = new azure_native.network.NetworkInterface("server-nic-tg", { resourceGroupName, ipConfigurations: [ { name: "tgserveripcfg", subnet: virtualNetwork.subnets.apply((subnet) => subnet![0]), privateIPAllocationMethod: azure_native.network.IPAllocationMethod.Dynamic, publicIPAddress: { id: PublicIp.id }, }, ], networkSecurityGroup: { id: networkSecurityGroup.id, },});
const startupScript = pulumi.interpolate`#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | sudo TWINGATE_ACCESS_TOKEN="${tgazureConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tgazureConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;
const tgconnector = new azure_native.compute.VirtualMachine("connector-vm", { resourceGroupName, networkProfile: { networkInterfaces: [{ id: networkInterface_tg.id }], }, hardwareProfile: { vmSize: azure_native.compute.VirtualMachineSizeTypes.Standard_B1ms, }, osProfile: { computerName: "TG-CONNECTOR", adminUsername: username, adminPassword: password, linuxConfiguration: { disablePasswordAuthentication: false, }, }, storageProfile: { osDisk: { createOption: azure_native.compute.DiskCreateOption.FromImage, name: "connector-vm-myosdisk1", deleteOption: "Delete", }, imageReference: { offer: "0001-com-ubuntu-server-jammy", publisher: "Canonical", sku: "22_04-lts-gen2", version: "latest", }, }, userData: startupScript.apply((x) => Buffer.from(x).toString("base64")),});
const privIP = networkInterface_vm.ipConfigurations.apply((x) => { return x != undefined ? x[0].privateIPAddress : null;});
const tgresource = new twingate.TwingateResource("resource", { name: "Azure demo server", address: pulumi.interpolate`${privIP}`, remoteNetworkId: tgazureNetwork.id, accessGroups: [ { groupId: tggroup.id, }, ], protocols: { allowIcmp: true, tcp: { policy: "RESTRICTED", ports: ["22", "80"], }, udp: { policy: "ALLOW_ALL", }, },});
5. Run and apply the configuration
You can run the following to check your Pulumi config:
pulumi preview
Once you are happy with it, you can then run:
pulumi up
Once you select “YES”, you should see all the infrastructure being built!
You will see 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 ☕️
The only thing left to do is give your Twingate user access to the new Group that has been created.
6. Test access
Once you are done, you should be able to connect your browser to the IP address of our test web server VM and see the Hello, World!
message.
We can now destroy the infrastructure by using the following command:
pulumi down
Last updated 2 months ago