How to Use Pulumi with GCP and Twingate

This guide provides step-by-step instructions on automating Twingate deployments with Pulumi on Google Cloud Platform.

Prerequisites

This guide assumes the following (on top of prerequisites for all Pulumi guides):

  • You have a GCP account setup and an account with relevant access to create and delete resources
  • GCP CLI setup and configured
  • You are using an Operating System supporting Bash

Getting Started

First let’s setup a new folder for our Pulumi code to reside:

mkdir twingate_pulumi_gcp_demo
cd twingate_pulumi_gcp_demo

Next we can use a template to set up the environment:

pulumi new typescript

You are then prompted to enter some values, these can be whatever you want:

project name: demo
project description: (A minimal TypeScript Pulumi program)
stack name: (dev)

Authentication with GCP

To allow us to create resources in GCP, you must be authenticated. The following command will prompt you to login to your GCP account. Ensure this is with an account that has the permissions to create the required resources, see here for more details.

gcloud auth application-default login

Setting Pulumi Config

You could also store these as environment variables if you preferred.

Pulumi requires some configuration to be able to connect to GCP and Twingate.

First we need to tell Pulumi which GCP project and region we are using (you will want to alter these to suit your setup):

pulumi config set gcp:project your-gcp-project-id
pulumi config set gcp:region europe-west2
pulumi config set gcp:zone europe-west2-c

And also the Twingate config (replacing with your generated token and your network name (Shown in the admin panel)):

$ pulumi config set twingate:apiToken YOUR_TOKEN --secret
$ pulumi config set twingate:network democompany

You can view and check the the Pulumi config values by running:

Pulumi config

Building our configuration

One of the main reasons people prefer to use Pulumi over other IaC tools is it’s flexibility around the programming languages you can use. For this example I will be using TypeScript/JavaScript to configure my resources.

This guide assumes you have nodejs installed on your device, you can check this by running the following:

node -v

If you do not have node installed please refer to the installation instructions here.

Installing Node Modules

As we are using Typescript, Pulumi will use modules to build the infrastructure for both GCP and Twingate, therefore we need to install these modules.

npm install @pulumi/gcp @twingate/pulumi-twingate

Now we have everything ready to start writing our code.

Writing the configuration

Open the index.ts file and import the modules we will be using:

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";
import * as twingate from "@twingate/pulumi-twingate";

First we will create the new network in Twingate:

const tggcpNetwork = new twingate.TwingateRemoteNetwork("twingate-gcp-demo", {
name: "twingate-gcp-demo",
});

Then we will create the new connector:

const tggcpConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tggcpNetwork.id });

Then the tokens for this connector:

const tggcpConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", {
connectorId: tggcpConnector.id,
});

And then a group which will have access to the new resource:

const tggroup = new twingate.TwingateGroup("twingateGroup", {
name: "gcp demo group",
});

GCP Infrastructure

Next we need to build the GCP resources:

First the new VPC network:

const computeNetwork = new gcp.compute.Network("twingate-demo-network", {
name: "twingate-demo-network",
autoCreateSubnetworks: false,
});

Then a subnet where our virtual machines will reside:

const computeSubnetwork = new gcp.compute.Subnetwork("twingate-demo-subnetwork", {
name: "twingate-demo-subnetwork",
ipCidrRange: "172.16.0.0/24",
region: "europe-west2",
network: computeNetwork.id,
});

And finally a firewall:

This firewall is set to only allow internal port 80 using the source tag “demo”. You will want to adapt these rules to suit your needs.

const demoFirewall = new gcp.compute.Firewall("demoFirewall", {
name: "demo-firewall",
network: computeNetwork.name,
allows: [
{
protocol: "icmp",
},
{
protocol: "tcp",
ports: ["80"],
},
],
sourceTags: ["demo"],
});

Creating the virtual machines

Now we can build the test virtual machine and the Twingate connector virtual machine, using a script to setup a webserver and a basic page:

const webserverScript = `
#!/bin/bash
apt-get -y update
apt-get -y install nginx
echo "
<style>
h1 {text-align: center;}
</style>
<h1>Pulumi &#128153; TWINGATE </h1>
" > /var/www/html/index.html
service nginx start
sudo rm -f index.nginx-debian.html`;

We will then build the web server and set this script to run when the machine starts.

const computeInstance = new gcp.compute.Instance("instance-vm", {
name: "twingate-demo-server",
machineType: "e2-micro",
tags: ["demo"],
metadataStartupScript: webserverScript,
bootDisk: {
initializeParams: {
image: "ubuntu-os-cloud/ubuntu-2204-lts",
},
},
networkInterfaces: [
{
network: computeNetwork.id,
subnetwork: computeSubnetwork.id,
accessConfigs: [{}], // must be empty to request an ephemeral IP
},
],
serviceAccount: {
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
},
});

The Twingate connector requires values generated from the created infrastructure above, which we can also define in a startup script:

We are using pulumi.interpolate to allow us to pass in values which are generated as part of the script.

const startupScript = pulumi.interpolate`
#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | sudo TWINGATE_ACCESS_TOKEN="${tggcpConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tggcpConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;

Next the Twingate connector VM including the startup script:

const computeInstance2 = new gcp.compute.Instance("instance-connector", {
name: "twingate-demo-connector",
machineType: "e2-micro",
tags: ["demo"],
metadataStartupScript: startupScript,
bootDisk: {
initializeParams: {
image: "ubuntu-os-cloud/ubuntu-2204-lts",
},
},
networkInterfaces: [
{
network: computeNetwork.id,
subnetwork: computeSubnetwork.id,
accessConfigs: [{}], // must be empty to request an ephemeral IP
},
],
serviceAccount: {
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
},
});

Finally we can complete the Twingate configuration by adding a Twingate resource:

const tgresource = new twingate.TwingateResource("resource", {
name: "gcp demo server",
address: computeInstance.networkInterfaces[0].networkIp,
remoteNetworkId: tggcpNetwork.id,
accessGroups: [
{
groupId: tggroup.id,
},
],
protocols: {
allowIcmp: true,
tcp: {
policy: "RESTRICTED",
ports: ["80"],
},
udp: {
policy: "ALLOW_ALL",
},
},
});

The final index.ts file should look like this:

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";
import * as twingate from "@twingate/pulumi-twingate";
const tggcpNetwork = new twingate.TwingateRemoteNetwork("twingate-gcp-demo", {
name: "twingate-gcp-demo",
});
const tggcpConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tggcpNetwork.id });
const tggcpConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", {
connectorId: tggcpConnector.id,
});
const tggroup = new twingate.TwingateGroup("twingateGroup", {
name: "gcp demo group",
});
const computeNetwork = new gcp.compute.Network("twingate-demo-network", {
name: "twingate-demo-network",
autoCreateSubnetworks: false,
});
const computeSubnetwork = new gcp.compute.Subnetwork("twingate-demo-subnetwork", {
name: "twingate-demo-subnetwork",
ipCidrRange: "172.16.0.0/24",
region: "europe-west2",
network: computeNetwork.id,
});
const demoFirewall = new gcp.compute.Firewall("demoFirewall", {
name: "demo-firewall",
network: computeNetwork.name,
allows: [
{
protocol: "icmp",
},
{
protocol: "tcp",
ports: ["80"],
},
],
sourceTags: ["demo"],
});
const webserverScript = `
#!/bin/bash
apt-get -y update
apt-get -y install nginx
echo "
<style>
h1 {text-align: center;}
</style>
<h1>Pulumi &#128153; TWINGATE </h1>
" > /var/www/html/index.html
service nginx start
sudo rm -f index.nginx-debian.html
`;
const computeInstance = new gcp.compute.Instance("instance-vm", {
name: "twingate-demo-server",
machineType: "e2-micro",
tags: ["demo"],
metadataStartupScript: webserverScript,
bootDisk: {
initializeParams: {
image: "ubuntu-os-cloud/ubuntu-2204-lts",
},
},
networkInterfaces: [
{
network: computeNetwork.id,
subnetwork: computeSubnetwork.id,
accessConfigs: [{}], // must be empty to request an ephemeral IP
},
],
serviceAccount: {
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
},
});
const startupScript = pulumi.interpolate`
#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | sudo TWINGATE_ACCESS_TOKEN="${tggcpConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tggcpConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;
const computeInstance2 = new gcp.compute.Instance("instance-connector", {
name: "twingate-demo-connector",
machineType: "e2-micro",
tags: ["demo"],
metadataStartupScript: startupScript,
bootDisk: {
initializeParams: {
image: "ubuntu-os-cloud/ubuntu-2204-lts",
},
},
networkInterfaces: [
{
network: computeNetwork.id,
subnetwork: computeSubnetwork.id,
accessConfigs: [{}], // must be empty to request an ephemeral IP
},
],
serviceAccount: {
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
},
});
const tgresource = new twingate.TwingateResource("resource", {
name: "gcp demo server",
address: computeInstance.networkInterfaces[0].networkIp,
remoteNetworkId: tggcpNetwork.id,
accessGroups: [
{
groupId: tggroup.id,
},
],
protocols: {
allowIcmp: true,
tcp: {
policy: "RESTRICTED",
ports: ["80"],
},
udp: {
policy: "ALLOW_ALL",
},
},
});

Running and applying the configuration

You can run the following to check your Pulumi config:

pulumi preview

And once you are happy you can run:

pulumi up

Once you select YES you should see all the infrastructure being built!

You will see the resources in both Twingate and GCP 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.

Test access

Then you can test you can reach the web server over the private Twingate connection by browsing to the private server IP:

We can now destroy the infrastructure by using the following command:

pulumi down

Last updated 3 months ago