Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,23 @@ The above configuration ensures that the subnet router can establish direct conn
| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 4.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 6.0 |
| <a name="requirement_tailscale"></a> [tailscale](#requirement\_tailscale) | >= 0.13.7 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 4.0 |
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 6.0 |
| <a name="provider_tailscale"></a> [tailscale](#provider\_tailscale) | >= 0.13.7 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_ssm_policy"></a> [ssm\_policy](#module\_ssm\_policy) | cloudposse/iam-policy/aws | 2.0.1 |
| <a name="module_ssm_policy"></a> [ssm\_policy](#module\_ssm\_policy) | cloudposse/iam-policy/aws | 2.0.2 |
| <a name="module_ssm_state"></a> [ssm\_state](#module\_ssm\_state) | cloudposse/ssm-parameter-store/aws | 0.13.0 |
| <a name="module_tailscale_subnet_router"></a> [tailscale\_subnet\_router](#module\_tailscale\_subnet\_router) | masterpointio/ssm-agent/aws | 1.4.0 |
| <a name="module_tailscale_subnet_router"></a> [tailscale\_subnet\_router](#module\_tailscale\_subnet\_router) | masterpointio/ssm-agent/aws | 1.8.0 |
| <a name="module_this"></a> [this](#module\_this) | cloudposse/label/null | 0.25.0 |

## Resources
Expand All @@ -193,6 +193,8 @@ The above configuration ensures that the subnet router can establish direct conn
| <a name="input_additional_tag_map"></a> [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.<br/>This is for some rare cases where resources want additional configuration of tags<br/>and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
| <a name="input_additional_tags"></a> [additional\_tags](#input\_additional\_tags) | Additional Tailscale tags to apply to the Tailscale Subnet Router machine in addition to `primary_tag`. These should not include the `tag:` prefix. | `list(string)` | `[]` | no |
| <a name="input_advertise_routes"></a> [advertise\_routes](#input\_advertise\_routes) | The routes (expressed as CIDRs) to advertise as part of the Tailscale Subnet Router.<br/> Example: ["10.0.2.0/24", "0.0.1.0/24"] | `list(string)` | `[]` | no |
| <a name="input_allow_encrypted_uploads_only"></a> [allow\_encrypted\_uploads\_only](#input\_allow\_encrypted\_uploads\_only) | Whether or not to allow encrypted uploads only. If set to `true` this will create a bucket policy that `Deny` if encryption header is missing in the requests. | `bool` | `false` | no |
| <a name="input_allow_ssl_requests_only"></a> [allow\_ssl\_requests\_only](#input\_allow\_ssl\_requests\_only) | Whether or not to allow SSL requests only. If set to `true` this will create a bucket policy that `Deny` if SSL is not used in the requests using the `aws:SecureTransport` condition. | `bool` | `false` | no |
| <a name="input_ami"></a> [ami](#input\_ami) | The AMI to use for the Tailscale Subnet Router EC2 instance.<br/> If not provided, the latest Amazon Linux 2 AMI will be used.<br/> Note: This will update periodically as AWS releases updates to their AL2 AMI.<br/> Pin to a specific AMI if you would like to avoid these updates. | `string` | `""` | no |
| <a name="input_architecture"></a> [architecture](#input\_architecture) | The architecture of the AMI (e.g., x86\_64, arm64) | `string` | `"arm64"` | no |
| <a name="input_associate_public_ip_address"></a> [associate\_public\_ip\_address](#input\_associate\_public\_ip\_address) | Associate public IP address with subnet router | `bool` | `null` | no |
Expand Down Expand Up @@ -294,7 +296,7 @@ We're active members of the community and are always publishing content, giving

[![Open Source Initiative][osi-image]][license-url]

Copyright © 2016-2025 [Masterpoint Consulting LLC](https://masterpoint.io/)
Copyright © 2016-2026 [Masterpoint Consulting LLC](https://masterpoint.io/)

<!-- MARKDOWN LINKS & IMAGES -->

Expand Down
150 changes: 98 additions & 52 deletions userdata.sh.tmpl
Original file line number Diff line number Diff line change
@@ -1,92 +1,138 @@
#!/bin/bash -ex
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1
#!/bin/bash
# Amazon Linux 2023 ONLY
set -Eeuo pipefail

# Log to file and console
exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1
echo "Starting user-data script..."

echo "Enabling IP forwarding..."
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf
sysctl -p /etc/sysctl.conf
# --------------------- Configuration variables ----------------
PKG="dnf"
TS_REPO_ROOT="https://pkgs.tailscale.com/stable/amazon-linux/2023"

# ---- Guard: require AL2023 (robust) ----
if [ -r /etc/os-release ]; then
. /etc/os-release
if [ "$${ID:-}" != "amzn" ] || [[ "$${VERSION_ID:-}" != 2023* ]]; then
echo "ERROR: This user-data requires Amazon Linux 2023. Detected ID=$${ID:-?} VERSION_ID=$${VERSION_ID:-?}" >&2
exit 1
fi
else
grep -qiE 'Amazon Linux.*2023' /etc/system-release || {
echo "ERROR: This user-data requires Amazon Linux 2023." >&2
exit 1
}
fi

# --------------------- Networking sysctls ---------------------
echo "Enabling IP forwarding (IPv4+IPv6)..."
mkdir -p /etc/sysctl.d
cat >/etc/sysctl.d/99-forwarding.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system

# --------------------- journald tuning -----------------------
# In systemd, Administrator drop-ins should reside in /etc/systemd/, ensuring they
# are preserved across updates and have higher precedence than vendor defaults.
#
# We name our file 99-custom.conf so it loads last among any .conf files.
# That way, it overrides any settings that come earlier.

# Create the journald configs directory if it doesn't already exist
mkdir -p /etc/systemd/journald.conf.d

cat <<EOF > /etc/systemd/journald.conf.d/99-custom.conf
[Journal]
SystemMaxUse=${journald_system_max_use}
MaxRetentionSec=${journald_max_retention_sec}
EOF

# Restart journald so it picks up the new configuration
systemctl restart systemd-journald

# Function to retry a command up to a maximum number of attempts
retry_command() {
local cmd="$1"
local max_attempts="$2"
local attempt=1
local exit_code=0

while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt of $max_attempts: $cmd"
eval "$cmd"
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Command succeeded: $cmd"
return 0
systemctl restart systemd-journald || true

# --------------------- Helper functions ----------------------
wait_pkg_lock() {
# Wait while the RPM DB or package managers are busy (bounded wait).
local waited=0
local max_wait=300 # seconds
while true; do
busy=false
if command -v fuser >/dev/null 2>&1; then
if fuser /var/lib/rpm/.rpm.lock >/dev/null 2>&1; then busy=true; fi
else
echo "Command failed with exit code $exit_code: $cmd"
attempt=$((attempt + 1))
if [ $attempt -le $max_attempts ]; then
echo "Retrying in 2 seconds..."
sleep 2
fi
# Best-effort if fuser is missing
if [ -e /var/lib/rpm/.rpm.lock ]; then busy=true; fi
fi
if pgrep -x dnf >/dev/null 2>&1 || pgrep -x rpm >/dev/null 2>&1; then
busy=true
fi
if [ "$busy" = false ]; then
return 0
fi
if [ $waited -ge $max_wait ]; then
echo "WARNING: Package manager busy for $${max_wait}s; continuing."
return 0
fi
echo "Waiting for package manager / RPM DB lock..."
sleep 3
waited=$((waited+3))
done
}

echo "Command failed after $max_attempts attempts: $cmd"
return $exit_code
retry() {
local n=0 max=6
until "$@"; do
n=$((n+1))
echo "Command failed (attempt $n)."
[[ $n -ge $max ]] && return 1
sleep $((2**n)) # 2,4,8,16,32 sec
done
}

# Install CloudWatch Agent
pkg() { wait_pkg_lock; retry $PKG -y "$@"; }

# --------------------- CloudWatch Agent ----------------------
echo "Installing CloudWatch Agent..."
retry_command "dnf install -y amazon-cloudwatch-agent" 5
amazon-cloudwatch-agent-ctl -a start -m ec2
pkg install amazon-cloudwatch-agent
amazon-cloudwatch-agent-ctl -a start -m ec2 || true

# --------------------- Tailscale repo + install --------------
echo "Configuring Tailscale repo (AL2023)..."
pkg install dnf-plugins-core
wait_pkg_lock
retry dnf config-manager --add-repo "$${TS_REPO_ROOT}/tailscale.repo"

# Install Tailscale
echo "Installing Tailscale..."
retry_command "dnf install -y dnf-utils" 5
retry_command "dnf config-manager --add-repo https://pkgs.tailscale.com/stable/amazon-linux/2/tailscale.repo" 5
retry_command "dnf install -y tailscale" 5
pkg makecache || true
pkg install tailscale

# --------------------- Optional extra tailscaled FLAGS -------
%{ if tailscaled_extra_flags_enabled == true }
echo "Exporting FLAGS to /etc/default/tailscaled..."
sed -i "s|^FLAGS=.*|FLAGS=\"${tailscaled_extra_flags}\"|" /etc/default/tailscaled
echo "Writing FLAGS to /etc/sysconfig/tailscaled..."
install -d -m 0755 /etc/sysconfig
echo "FLAGS=\"${tailscaled_extra_flags}\"" > /etc/sysconfig/tailscaled
%{ endif }

# Setup Tailscale
echo "Enabling and starting tailscaled service..."
# --------------------- Enable + start tailscaled -------------
echo "Enabling and starting tailscaled..."
systemctl enable --now tailscaled

echo "Waiting for tailscaled to initialize..."
sleep 5
echo "Waiting for tailscaled to be ready..."
for i in {1..20}; do
if tailscale status >/dev/null 2>&1; then break; fi
sleep 2
done

# Start tailscale
# We pass --advertise-tags below even though the authkey being created with those tags should result
# in the same effect. This is to be more explicit because tailscale tags are a complicated topic.
# --------------------- tailscale up (hide secrets) -----------
set +x
echo "Running 'tailscale up'..."
tailscale up \
%{ if ssh_enabled == true }--ssh%{ endif } \
%{ if exit_node_enabled == true }--advertise-exit-node%{ endif } \
%{ if tailscale_up_extra_flags_enabled == true }${tailscale_up_extra_flags}%{ endif } \
--advertise-routes=${routes} \
--advertise-tags=${tags} \
--hostname=${hostname} \
--authkey=${authkey}
--authkey=${authkey} \
|| { echo "WARNING: 'tailscale up' failed; continuing."; }
set -x || true

echo "Tailscale setup completed."
echo "User-data script finished successfully."
2 changes: 1 addition & 1 deletion versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
version = ">= 6.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gberenice you mentioned that the AWS Provider constraint wasn't updated properly -- when did that happen and what was the usage of AWS that changed that required the bump? Technically this is should be a major provider rev I believe, so I want to make sure we're doing this right.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gowiem This happened within PR #76, and caused this recently reported issue: This object has no argument, nested block, or exported attribute named "region".

PR title is already configured for a major bump.

}
tailscale = {
source = "tailscale/tailscale"
Expand Down