Pulumi - AWS
This guide provides step-by-step instructions on automating Twingate deployments with Pulumi on Amazon Web Services.
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_demo
cd 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 AWS
Created 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 --secret
pulumi 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-keygen
Generating 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_rsa
Your public key has been saved in /Users/username/.ssh/aws_id_rsa.pub
The 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 GCP and Twingate, therefore we need to install these modules.
npm install @pulumi/aws @twingate-labs/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 aws from "@pulumi/aws";
import { Connector } from "@pulumi/aws/mskconnect";
import * as twingate from "@twingate-labs/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:
// VPC
const mainvpc = new aws.ec2.Vpc("mainVPC", {
cidrBlock: "10.0.0.0/16",
});
// Subnet
const mainSubnet = new aws.ec2.Subnet("mainSubnet", {
vpcId: mainvpc.id,
cidrBlock: "10.0.1.0/24",
tags: {
Name: "Main",
},
});
// Gateway
const gw = new aws.ec2.InternetGateway("mainGW", {
vpcId: mainvpc.id,
tags: {
Name: "main",
},
});
// Routing table
const mainRouteTable = new aws.ec2.RouteTable("mainRT", {
vpcId: mainvpc.id,
routes: [
{
cidrBlock: "0.0.0.0/0",
gatewayId: gw.id,
},
],
tags: {
Name: "example",
},
});
// Routing table association
const routeTableAssociation = new aws.ec2.RouteTableAssociation("mainRTA", {
subnetId: mainSubnet.id,
routeTableId: mainRouteTable.id,
});
Next we will setup the Twingate components:
// Twingate Setup
const 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 = aws.getAmiOutput({
filters: [
{
name: "name",
values: ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"],
},
],
owners: ["099720109477"], // 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 startupScript = pulumi.interpolate`#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | TWINGATE_ACCESS_TOKEN="${tgawsConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tgawsConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;
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: startupScript,
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,
groupIds: [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-labs/pulumi-twingate";
let config = new pulumi.Config();
let publicKey = config.require("publicKey");
const deployer = new aws.ec2.KeyPair("deployer", {
publicKey: publicKey,
});
// VPC
const mainvpc = new aws.ec2.Vpc("mainVPC", {
cidrBlock: "10.0.0.0/16",
});
// Subnet
const mainSubnet = new aws.ec2.Subnet("mainSubnet", {
vpcId: mainvpc.id,
cidrBlock: "10.0.1.0/24",
tags: {
Name: "Main",
},
});
// Gateway
const gw = new aws.ec2.InternetGateway("mainGW", {
vpcId: mainvpc.id,
tags: {
Name: "main",
},
});
// Routing table
const mainRouteTable = new aws.ec2.RouteTable("mainRT", {
vpcId: mainvpc.id,
routes: [
{
cidrBlock: "0.0.0.0/0",
gatewayId: gw.id,
},
],
tags: {
Name: "example",
},
});
// Routing table assoc
const routeTableAssociation = new aws.ec2.RouteTableAssociation("mainRTA", {
subnetId: mainSubnet.id,
routeTableId: mainRouteTable.id,
});
// Twingate Setup
const 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 AMI
const size = "t2.micro";
const ami = aws.getAmiOutput({
filters: [
{
name: "name",
values: ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"],
},
],
owners: ["099720109477"], // This owner ID is Amazon
mostRecent: true,
});
// Startup script
const startupScript = pulumi.interpolate`#!/bin/bash
curl "https://binaries.twingate.com/connector/setup.sh" | TWINGATE_ACCESS_TOKEN="${tgawsConnectorTokens.accessToken}" TWINGATE_REFRESH_TOKEN="${tgawsConnectorTokens.refreshToken}" TWINGATE_URL="https://${twingate.config.network}.twingate.com" bash
`;
// Demo server
const 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 VM
const tgserver = new aws.ec2.Instance("twingate-connector", {
instanceType: size,
associatePublicIpAddress: true,
ami: ami.id,
subnetId: mainSubnet.id,
userData: startupScript,
tags: {
Name: "Twingate-Connector",
},
});
// Twingate resource
const tgresource = new twingate.TwingateResource("resource", {
name: "aws demo server",
address: demoserver.privateIp,
remoteNetworkId: tgawsNetwork.id,
groupIds: [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!
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 ☕️
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.164
The 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])? yes
Warning: 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 the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable 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 2 months ago