Skip to content

Instantly share code, notes, and snippets.

@MarkArts
Created December 10, 2024 15:59
Show Gist options
  • Save MarkArts/0c3851eecf8f985a09602c77b6daf326 to your computer and use it in GitHub Desktop.
Save MarkArts/0c3851eecf8f985a09602c77b6daf326 to your computer and use it in GitHub Desktop.
Pulumi Tailnet subnet router example
/*
Adapted from: https://github.com/jaxxstorm/tailscale-examples/blob/main/aws/pulumi/ec2-ts-subnetrouter/index.ts
example asumming you already have a vpc with public subnets:
const tailscaleSubnetRouter = setupTailscaleSubnetRouter(
`mytailscalestuff`,
{
vpc: {
vpcId: network.vpc.id,
publicSubnetIds: network.publicSubnets.map((x) => x.id),
cidrBlock: network.vpc.cidrBlock,
},
tailscaleConfig: {
authKey: cfg.requireSecret("tailscaleOauthClientSecret"),
hostname: `pulumi-example-subnet-router`,
advertiseExitNode: true,
advertiseRoutes: [network.vpc.cidrBlock],
},
},
opts,
);
*/
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as cloudinit from "@pulumi/cloudinit";
type SetupTailscaleSubnetRouterArgs = {
vpc: {
vpcId: pulumi.Input<string>;
publicSubnetIds: pulumi.Input<string>[];
cidrBlock: pulumi.Input<string>;
};
tailscaleConfig: {
advertiseExitNode?: pulumi.Input<boolean>;
advertiseConnector?: pulumi.Input<boolean>;
acceptDns?: pulumi.Input<boolean>;
acceptRoutes?: pulumi.Input<boolean>;
authKey: pulumi.Input<string>;
advertiseRoutes?: pulumi.Input<pulumi.Input<string>[]>;
advertiseTags?: pulumi.Input<pulumi.Input<string>[]>;
hostname?: pulumi.Input<string>;
exitNode?: pulumi.Input<string>;
exitNodeAllowLanAccess?: pulumi.Input<boolean>;
json?: pulumi.Input<boolean>;
loginServer?: pulumi.Input<string>;
reset?: pulumi.Input<boolean>;
shieldsUp?: pulumi.Input<boolean>;
tailscaleSsh?: pulumi.Input<boolean>;
snatSubnetRoutes?: pulumi.Input<boolean>;
netfilterMode?: pulumi.Input<string>;
statefulFiltering?: pulumi.Input<boolean>;
timeout?: pulumi.Input<string>;
forceReauth?: pulumi.Input<boolean>;
operator?: pulumi.Input<string>;
};
};
const tailscaleDefaults = {
advertiseExitNode: false,
advertiseConnector: false,
acceptDns: true,
acceptRoutes: false,
advertiseRoutes: [],
advertiseTags: ["tag:example"],
hostname: "pulumi",
exitNode: "",
exitNodeAllowLanAccess: false,
json: false,
loginServer: "https://controlplane.tailscale.com",
reset: false,
shieldsUp: false,
tailscaleSsh: false,
snatSubnetRoutes: true,
netfilterMode: "on",
statefulFiltering: false,
timeout: "30s",
forceReauth: false,
operator: "",
};
export function setupTailscaleSubnetRouter(
prefix: string,
args: SetupTailscaleSubnetRouterArgs,
opts: pulumi.CustomResourceOptions,
) {
// couldn't find a better way to have the args typed and have default values
// for some of the values. probably missing something
args = {
...args,
tailscaleConfig: {
...tailscaleDefaults,
...args.tailscaleConfig,
},
};
const ami = aws.ec2.getAmi({
mostRecent: true,
filters: [
{
name: "name",
values: ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"],
},
{
name: "virtualization-type",
values: ["hvm"],
},
],
owners: ["099720109477"], // Canonical
});
const MAX_RETRIES = 5;
const RETRY_DELAY = 10;
const TRACK = "stable";
const installTailscaleTmpl = pulumi.interpolate`#!/bin/sh
# Retry in case we don't have internet connectivity
max_retries=${MAX_RETRIES}
retry_delay=${RETRY_DELAY}
i=1
# Export the TRACK variable
TRACK="${TRACK}"
export TRACK
echo "Starting Tailscale installation with TRACK=$TRACK"
while [ $i -le $max_retries ]
do
# Download and run the Tailscale install script
if curl -fsSL https://tailscale.com/install.sh | sh; then
echo "Tailscale installation script executed successfully"
else
echo "Error: Tailscale installation script failed. Retry attempt $i"
sleep $retry_delay
i=$((i + 1))
continue
fi
# Check if Tailscale is installed correctly
if command -v tailscale >/dev/null 2>&1; then
echo "Tailscale installed successfully"
tailscale version
exit 0
else
echo "Error: Tailscale installation could not be verified. Retry attempt $i"
sleep $retry_delay
i=$((i + 1))
fi
done
echo "Tailscale installation completed"
exit 0
`;
// the set up Tailscale template
const setupTailscaleTmpl = pulumi.interpolate`#!/bin/sh
sudo systemctl enable --now tailscaled
# Construct the tailscale up command based on the presence of advertise_tags variable
tailscale_cmd="tailscale up --advertise-exit-node=\\\"${args.tailscaleConfig.advertiseExitNode}\\\" \
--advertise-connector=\\\"${args.tailscaleConfig.advertiseConnector}\\\" \
--accept-dns=\\\"${args.tailscaleConfig.acceptDns}\\\" \
--accept-routes=\\\"${args.tailscaleConfig.acceptRoutes}\\\" \
--authkey \\\"${args.tailscaleConfig.authKey}\\\" \
--advertise-routes=\\\"${args.tailscaleConfig.advertiseRoutes}\\\" \
--snat-subnet-routes=\\\"${args.tailscaleConfig.snatSubnetRoutes}\\\" \
--exit-node=\\\"${args.tailscaleConfig.exitNode}\\\" \
--exit-node-allow-lan-access=\\\"${args.tailscaleConfig.exitNodeAllowLanAccess}\\\" \
--json=\\\"${args.tailscaleConfig.json}\\\" \
--login-server=\\\"${args.tailscaleConfig.loginServer}\\\" \
--reset=\\\"${args.tailscaleConfig.reset}\\\" \
--shields-up=\\\"${args.tailscaleConfig.shieldsUp}\\\" \
--ssh=\\\"${args.tailscaleConfig.tailscaleSsh}\\\" \
--netfilter-mode=\\\"${args.tailscaleConfig.netfilterMode}\\\" \
--stateful-filtering=\\\"${args.tailscaleConfig.statefulFiltering}\\\" \
--timeout=\\\"${args.tailscaleConfig.timeout}\\\" \
--force-reauth=\\\"${args.tailscaleConfig.forceReauth}\\\""
if [ -n "${args.tailscaleConfig.advertiseTags}" ]; then
tailscale_cmd="$tailscale_cmd --advertise-tags=\\\"${args.tailscaleConfig.advertiseTags}\\\""
fi
if [ -n "${args.tailscaleConfig.hostname}" ]; then
tailscale_cmd="$tailscale_cmd --hostname=\\\"${args.tailscaleConfig.hostname}\\\""
fi
if [ -n "${args.tailscaleConfig.operator}" ]; then
tailscale_cmd="$tailscale_cmd --operator=\\\"${args.tailscaleConfig.operator}\\\""
fi
if [ -z "${args.tailscaleConfig.authKey}" ]; then
echo "Error: authKey is required but not set. Exiting."
exit 1
fi
# Execute the tailscale up command
eval "$tailscale_cmd"
# Check the exit status of the previous command
if [ $? -eq 0 ]; then
echo "Tailscale installation and configuration succeeded"
exit 0
else
echo "Tailscale installation and configuration failed. Retry attempt $i"
exit 1
fi
`;
// read the static file
const ipForwardingContent = `#!/bin/sh
echo "set required IP forwarding kernel values"
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1`;
// Turn the above templates into cloud-init data
const userData = cloudinit.getConfigOutput({
gzip: false,
base64Encode: true,
parts: [
{
contentType: "text/x-shellscript",
content: installTailscaleTmpl,
},
{
contentType: "text/x-shellscript",
content: setupTailscaleTmpl,
},
{
contentType: "text/x-shellscript",
content: ipForwardingContent,
},
],
});
const instanceSecurityGroups = new aws.ec2.SecurityGroup(
`${prefix}-instance-securitygroup`,
{
vpcId: args.vpc.vpcId,
description: "Allow all ports from same subnet",
ingress: [
{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: [args.vpc.cidrBlock],
},
{
protocol: "udp",
fromPort: 41641,
toPort: 41641,
cidrBlocks: ["0.0.0.0/0"],
},
],
egress: [
{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
},
],
},
opts,
);
// create a launch template to be used for ASG
const launchTemplate = new aws.ec2.LaunchTemplate(
`${prefix}-launch-template`,
{
imageId: ami.then((ami) => ami.id),
instanceType: "t3.small",
namePrefix: `${prefix}-launch-template`,
networkInterfaces: [
{
deleteOnTermination: "true",
securityGroups: [instanceSecurityGroups.id],
associatePublicIpAddress: "true",
},
],
monitoring: {
enabled: true,
},
blockDeviceMappings: [
{
deviceName: "/dev/xvda",
ebs: {
volumeSize: 8,
deleteOnTermination: "true",
volumeType: "gp2",
},
},
],
metadataOptions: {
httpTokens: "required", // required for Tailscale's AWS account, not always required
httpPutResponseHopLimit: 2, // required for Tailscale's AWS account, not always required
httpEndpoint: "enabled",
},
userData: userData.rendered,
tagSpecifications: [
{
resourceType: "instance",
tags: pulumi
.all([
args.tailscaleConfig.hostname,
aws.getDefaultTagsOutput({}, opts).tags,
])
.apply(([hostname, tags]) => ({
...tags,
Name: hostname || "",
})),
},
],
},
opts,
);
// Create a self-healing autoscaling group
const asg = new aws.autoscaling.Group(
`${prefix}-asg`,
{
maxSize: 2,
minSize: 1,
desiredCapacity: 2,
launchTemplate: {
id: launchTemplate.id,
version: launchTemplate.latestVersion.apply((v) => v.toString()),
},
instanceRefresh: {
strategy: "Rolling",
preferences: {
minHealthyPercentage: 50,
},
},
vpcZoneIdentifiers: args.vpc.publicSubnetIds,
},
opts,
);
return {
instanceSecurityGroups,
launchTemplate,
asg,
tailscaleConfig: args.tailscaleConfig,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment