How to Use Pulumi with AWS and Twingate
This guide provides step-by-step instructions on automating Twingate deployments with Pulumi on Amazon Web Services.
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 an AWS account setup and an account with relevant access to create and delete resources
- You are using an Operating System supporting Bash
Additional practical examples
Beyond this guide, our team regularly releases new examples for Pulumi and AWS which you can find in our GitHub repository.
Getting Started
First let’s setup a new folder for our Pulumi code to reside:
mkdir twingate_pulumi_aws_democd twingate_pulumi_aws_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: (twingate_pulumi_aws_demo)project description: (A minimal TypeScript Pulumi program) Using Pulumi to deploy Twingate on AWSCreated project 'twingate_pulumi_aws_demo'
Please enter your desired stack name.To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).stack name: (dev) demo
Once the project is setup, open the directory in your favorite code editor.
Authentication with AWS
To allow us to create resources in AWS, you must be authenticated. As with other cloud providers there are a few different ways to authenticate. For simplicity we will be storing our credentials as environment variables. This also ensures these values are kept local on your machine:
(The following commands can be used to store environment variables on OSX / Linux)
export AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>export AWS_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>export AWS_REGION=<YOUR_AWS_REGION>
Setup & 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 following the instructions here.
The name of your Twingate tenant is the prefix on your URL, i.e. mycorp
in mycorp.twingate.com
.
Let’s store these two values in the Pulumi configuration:
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.
pulumi config set twingate:apiToken YOUR_TOKEN --secretpulumi config set twingate:network democompany
Generating a key pair and setting a public key
In order to connect to our test VM, we will be using an SSH key pair for authentication, below is an example of how to do this, for more information please see this link.
Let’s use the ssh-keygen
command in a terminal session:
ssh-keygenGenerating public/private rsa key pair.
Enter file in which to save the key (/Users/username]/.ssh/id_rsa): /Users/username/.ssh/aws_id_rsa
Enter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in /Users/username/.ssh/aws_id_rsaYour public key has been saved in /Users/username/.ssh/aws_id_rsa.pubThe key fingerprint is:
[Your unique fingerprint]
The key's randomart image is:
[Your unique image]
Next we want to store the public key as a Pulumi configuration value:
cat /Users/username/.ssh/aws_id_rsa.pub | pulumi config set publicKey
You can view and check the the Pulumi config values by running:
Pulumi config
Building our Configuration
One of the main reasons people prefer Pulumi over other IaC tools is Pulumi’s flexibility around the programming languages you can use. For this example we will be using TypeScript/JavaScript
to configure our 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 AWS and Twingate, therefore we need to install these modules.
npm install @pulumi/aws @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 aws from "@pulumi/aws";import { Connector } from "@pulumi/aws/mskconnect";import * as twingate from "@twingate/pulumi-twingate";
First we will configure the public key to allow us to access the test server.
let config = new pulumi.Config();let publicKey = config.require("publicKey");
const deployer = new aws.ec2.KeyPair("deployer", { publicKey: publicKey,});
Next we will create the networking components:
// VPCconst mainvpc = new aws.ec2.Vpc("mainVPC", { cidrBlock: "10.0.0.0/16",});
// Subnetconst mainSubnet = new aws.ec2.Subnet("mainSubnet", { vpcId: mainvpc.id, cidrBlock: "10.0.1.0/24", tags: { Name: "Main", },});
// Gatewayconst gw = new aws.ec2.InternetGateway("mainGW", { vpcId: mainvpc.id, tags: { Name: "main", },});
// Routing tableconst mainRouteTable = new aws.ec2.RouteTable("mainRT", { vpcId: mainvpc.id, routes: [ { cidrBlock: "0.0.0.0/0", gatewayId: gw.id, }, ], tags: { Name: "example", },});
// Routing table associationconst routeTableAssociation = new aws.ec2.RouteTableAssociation("mainRTA", { subnetId: mainSubnet.id, routeTableId: mainRouteTable.id,});
Next we will setup the Twingate components:
// Twingate Setupconst tgawsNetwork = new twingate.TwingateRemoteNetwork("twingate-aws-demo", { name: "twingate-aws-demo",});
const tgawsConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tgawsNetwork.id });const tgawsConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", { connectorId: tgawsConnector.id,});
const tggroup = new twingate.TwingateGroup("twingateGroup", { name: "aws demo group",});
Then we build out the virtual machines; this will locate the most recent AMI for us to use:
const size = "t2.micro";const ami = pulumi.output( aws.ec2.getAmi({ filters: [ { name: "name", values: ["twingate/images/hvm-ssd/twingate-amd64-*"], }, ], owners: ["617935088040"], // This owner ID is Amazon mostRecent: true, }),);
We can now specify the startup script to install and setup Twingate on the Virtual Machine which will be hosting the Connector:
const user_data = pulumi.all([tgawsConnectorTokens.accessToken, tgawsConnectorTokens.refreshToken]).apply( ([accessToken, refreshToken]) => `#!/bin/bashsudo mkdir -p /etc/twingate/HOSTNAME_LOOKUP=$(curl http://169.254.169.254/latest/meta-data/local-hostname)EGRESS_IP=$(curl https://checkip.amazonaws.com){echo TWINGATE_URL="https://${twingate.config.network}.twingate.com"echo TWINGATE_ACCESS_TOKEN="${accessToken}"echo TWINGATE_REFRESH_TOKEN="${refreshToken}"echo TWINGATE_LOG_ANALYTICS=v1echo TWINGATE_LABEL_HOSTNAME=$HOSTNAME_LOOKUPecho TWINGATE_LABEL_EGRESSIP=$EGRESS_IPecho TWINGATE_LABEL_DEPLOYEDBY=tg-pulumi-aws-ec2} > /etc/twingate/connector.confsudo systemctl enable --now twingate-connector`,);
We can now create the virtual machines:
const webserver = new aws.ec2.Instance("demo-server", { instanceType: size, associatePublicIpAddress: false, keyName: deployer.id, ami: ami.id, subnetId: mainSubnet.id, tags: { Name: "Demo Server", },});
const tgserver = new aws.ec2.Instance("twingate-connector", { instanceType: size, associatePublicIpAddress: true, ami: ami.id, subnetId: mainSubnet.id, userData: user_data, tags: { Name: "Twingate-Connector", },});
And finally the Twingate resource:
const tgresource = new twingate.TwingateResource("resource", { name: "aws demo server", address: webserver.privateIp, remoteNetworkId: tgawsNetwork.id, accessGroups: [ { groupId: tggroup.id, }, ], protocols: { allowIcmp: true, tcp: { policy: "RESTRICTED", ports: ["22", "80"], }, udp: { policy: "ALLOW_ALL", }, },});
The complete index.ts
file should look like this:
import * as pulumi from "@pulumi/pulumi";import * as aws from "@pulumi/aws";import { Connector } from "@pulumi/aws/mskconnect";import * as twingate from "@twingate/pulumi-twingate";
let config = new pulumi.Config();let publicKey = config.require("publicKey");
const deployer = new aws.ec2.KeyPair("deployer", { publicKey: publicKey,});
// VPCconst mainvpc = new aws.ec2.Vpc("mainVPC", { cidrBlock: "10.0.0.0/16",});
// Subnetconst mainSubnet = new aws.ec2.Subnet("mainSubnet", { vpcId: mainvpc.id, cidrBlock: "10.0.1.0/24", tags: { Name: "Main", },});
// Gatewayconst gw = new aws.ec2.InternetGateway("mainGW", { vpcId: mainvpc.id, tags: { Name: "main", },});
// Routing tableconst mainRouteTable = new aws.ec2.RouteTable("mainRT", { vpcId: mainvpc.id, routes: [ { cidrBlock: "0.0.0.0/0", gatewayId: gw.id, }, ], tags: { Name: "example", },});
// Routing table assocconst routeTableAssociation = new aws.ec2.RouteTableAssociation("mainRTA", { subnetId: mainSubnet.id, routeTableId: mainRouteTable.id,});
// Twingate Setupconst tgawsNetwork = new twingate.TwingateRemoteNetwork("twingate-aws-demo", { name: "twingate-aws-demo",});
const tgawsConnector = new twingate.TwingateConnector("twingateConnector", { remoteNetworkId: tgawsNetwork.id });
const tgawsConnectorTokens = new twingate.TwingateConnectorTokens("twingateConnectorTokens", { connectorId: tgawsConnector.id,});
const tggroup = new twingate.TwingateGroup("twingateGroup", { name: "aws demo group",});
// Find latest AMIconst size = "t2.micro";const ami = pulumi.output( aws.ec2.getAmi({ filters: [ { name: "name", values: ["twingate/images/hvm-ssd/twingate-amd64-*"], }, ], owners: ["617935088040"], // This owner ID is Amazon mostRecent: true, }),);
// Startup scriptconst user_data = pulumi.all([tgawsConnectorTokens.accessToken, tgawsConnectorTokens.refreshToken]).apply( ([accessToken, refreshToken]) => `#!/bin/bashsudo mkdir -p /etc/twingate/HOSTNAME_LOOKUP=$(curl http://169.254.169.254/latest/meta-data/local-hostname)EGRESS_IP=$(curl https://checkip.amazonaws.com){echo TWINGATE_URL="https://${twingate.config.network}.twingate.com"echo TWINGATE_ACCESS_TOKEN="${accessToken}"echo TWINGATE_REFRESH_TOKEN="${refreshToken}"echo TWINGATE_LOG_ANALYTICS=v1echo TWINGATE_LABEL_HOSTNAME=$HOSTNAME_LOOKUPecho TWINGATE_LABEL_EGRESSIP=$EGRESS_IPecho TWINGATE_LABEL_DEPLOYEDBY=tg-pulumi-aws-ec2} > /etc/twingate/connector.confsudo systemctl enable --now twingate-connector`,);
// Demo serverconst demoserver = new aws.ec2.Instance("demo-server", { instanceType: size, associatePublicIpAddress: false, keyName: deployer.id, ami: ami.id, subnetId: mainSubnet.id, tags: { Name: "Demo Server", },});
// Twingate Connector VMconst tgserver = new aws.ec2.Instance("twingate-connector", { instanceType: size, associatePublicIpAddress: true, ami: ami.id, subnetId: mainSubnet.id, userData: user_data, tags: { Name: "Twingate-Connector", },});
// Twingate resourceconst tgresource = new twingate.TwingateResource("resource", { name: "aws demo server", address: demoserver.privateIp, remoteNetworkId: tgawsNetwork.id, accessGroups: [ { groupId: tggroup.id, }, ], protocols: { allowIcmp: true, tcp: { policy: "RESTRICTED", ports: ["22", "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 AWS 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.
Testing access
Then you can test you can reach the demo server over the private Twingate connection by browsing to the private server IP:
ssh -i ~/.ssh/aws_id_rsa ubuntu@10.0.1.164The authenticity of host '10.0.1.264 (10.0.1.164)' can't be established.ECDSA key fingerprint is SHA256:VFOTKrZvzwK5hgCwCZApGCDrrzFp3ISTaZoKaBG9218.Are you sure you want to continue connecting (yes/no/[fingerprint])? yesWarning: Permanently added '10.0.1.214' (ECDSA) to the list of known hosts.
You should then see the VM logged in:
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.0-1019-aws x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
System information as of Tue Sep 6 06:39:14 UTC 2022
System load: 0.0 Processes: 102 Usage of /: 19.6% of 7.57GB Users logged in: 0 Memory usage: 21% IPv4 address for ens5: 10.0.1.214 Swap usage: 0%
0 updates can be applied immediately.
The programs included with the Ubuntu system are free software;the exact distribution terms for each program are described in theindividual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted byapplicable law.
To run a command as administrator (user "root"), use "sudo <command>".See "man sudo_root" for details.
ubuntu@ip-10-0-1-164:~$
We can now destroy the infrastructure by using the following command:
pulumi down
Last updated 3 months ago