From 4a0b5898593c5f9fa2cb226d913cd5ca131bc450 Mon Sep 17 00:00:00 2001 From: Tine Date: Thu, 2 Jan 2025 16:27:42 +0100 Subject: [PATCH] feat(ingress.tjo.cloud): add dyndns support --- ingress.tjo.cloud/README.md | 15 ++ ingress.tjo.cloud/install.sh | 21 ++- ingress.tjo.cloud/justfile | 65 +++++---- ingress.tjo.cloud/root/etc/default/dyndns | 17 +++ .../root/systemd/system/dyndns.service | 8 ++ .../root/systemd/system/geo-ip-update.timer | 0 .../root/systemd/system/webhook.service | 8 -- ingress.tjo.cloud/root/usr/local/bin/dyndns | 132 ++++++++++++++++++ ingress.tjo.cloud/terraform/node.tf | 10 +- ingress.tjo.cloud/terraform/variables.tf | 5 + 10 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 ingress.tjo.cloud/root/etc/default/dyndns create mode 100644 ingress.tjo.cloud/root/systemd/system/dyndns.service delete mode 100644 ingress.tjo.cloud/root/systemd/system/geo-ip-update.timer delete mode 100644 ingress.tjo.cloud/root/systemd/system/webhook.service create mode 100755 ingress.tjo.cloud/root/usr/local/bin/dyndns diff --git a/ingress.tjo.cloud/README.md b/ingress.tjo.cloud/README.md index efe24b5..4ab5133 100644 --- a/ingress.tjo.cloud/README.md +++ b/ingress.tjo.cloud/README.md @@ -1,3 +1,18 @@ # ingress Handling all Ingress traffic + +## Rolling out changes + +```sh +# Apply code changes to single node. +# Make sure to commit and push the changes first. +just provision-only nevaroo + +# Apply infrastructure changes to single node. +just apply-only nevaroo + +# Apply to all nodes +just provision +just apply +``` \ No newline at end of file diff --git a/ingress.tjo.cloud/install.sh b/ingress.tjo.cloud/install.sh index 41c8f94..7fdc3f6 100755 --- a/ingress.tjo.cloud/install.sh +++ b/ingress.tjo.cloud/install.sh @@ -11,7 +11,7 @@ if [ ! -d .git ]; then --depth 1 \ --no-checkout \ --filter=tree:0 \ - https://code.tjo.space/tjo-cloud/infrastructure.git . + https://github.com/tjo-space/tjo-cloud-infrastructure.git . git sparse-checkout set --no-cone /ingress.tjo.cloud git checkout else @@ -31,6 +31,8 @@ SERVICE_ACCOUNT_PASSWORD=$(jq -r ".service_account.password" /etc/tjo.cloud/meta TAILSCALE_AUTH_KEY=$(jq -r ".tailscale.auth_key" /etc/tjo.cloud/meta.json) +DIGITALOCEAN_TOKEN=$(jq -r ".digitalocean.token" /etc/tjo.cloud/meta.json) + ## echo "== Install Dependencies" apt update -y @@ -59,7 +61,7 @@ apt install -y tailscale ## echo "== Ensure services are enabled" -systemctl enable --now nginx alloy tailscaled +systemctl enable --now nginx alloy tailscaled dydns ## echo "== Configure Grafana Alloy" @@ -71,13 +73,22 @@ ATTRIBUTES+="service.name=${SERVICE_NAME}," ATTRIBUTES+="service.version=${SERVICE_VERSION}," ATTRIBUTES+="cloud.region=${CLOUD_REGION}" echo "OTEL_RESOURCE_ATTRIBUTES=${ATTRIBUTES}" >>/etc/default/alloy -# Set Credentials +# set credentials { - echo "ALLOY_USERNAME=${SERVICE_ACCOUNT_USERNAME}" - echo "ALLOY_PASSWORD=${SERVICE_ACCOUNT_PASSWORD}" + echo "alloy_username=${SERVICE_ACCOUNT_USERNAME}" + echo "alloy_password=${SERVICE_ACCOUNT_PASSWORD}" } >>/etc/default/alloy systemctl restart alloy +## +echo "== Configure Dydns" +cp -r root/etc/default/dydns /etc/default/dydns +{ + echo "DIGITALOCEAN_TOKEN=${DIGITALOCEAN_TOKEN}" + echo "NAME=${CLOUD_REGION}" +} >>/etc/default/dydns +systemctl restart dydns + ## echo "== Configure Tailscale" if tailscale status --json | jq -e -r '.BackendState != "Running"' >/dev/null; then diff --git a/ingress.tjo.cloud/justfile b/ingress.tjo.cloud/justfile index 430a92a..407c305 100644 --- a/ingress.tjo.cloud/justfile +++ b/ingress.tjo.cloud/justfile @@ -1,8 +1,3 @@ -# Always use devbox environment to run commands. -set shell := ["devbox", "run"] -# Load dotenv -set dotenv-load - default: @just --list @@ -14,17 +9,54 @@ format: @tofu fmt -recursive . @tflint --recursive -deploy: +apply: #!/usr/bin/env sh - cd {{justfile_directory()}}/terraform + cd {{source_directory()}}/terraform tofu init tofu apply +apply-only node: + #!/usr/bin/env sh + cd {{source_directory()}}/terraform + tofu init + tofu apply --target 'proxmox_virtual_environment_vm.nodes["{{node}}"]' + + destroy: #!/usr/bin/env sh - cd {{justfile_directory()}}/terraform + cd {{source_directory()}}/terraform tofu destroy +provision: + #!/usr/bin/env sh + set -eou pipefail + + pushd {{source_directory()}}/terraform > /dev/null + NODES=$(tofu output -json | jq -r '.nodes.value[]') + popd > /dev/null + + for NODE in $NODES + do + echo "= Provisioning node ${NODE}" + cat install.sh | tailscale ssh ubuntu@${NODE} 'sudo bash -s' + done + +provision-only node: + #!/usr/bin/env sh + set -eou pipefail + + pushd {{source_directory()}}/terraform > /dev/null + NODES=$(tofu output -json | jq -r '.nodes.value[]') + popd > /dev/null + + for NODE in $NODES + do + if [ "$NODE" -eq "{{node}}" ] + echo "= Provisioning node ${NODE}" + cat install.sh | tailscale ssh ubuntu@${NODE} 'sudo bash -s' + fi + done + # Create a list of blocked IP ranges. Traffic we don't want. update-blocked-list: #!/usr/bin/env bash @@ -49,20 +81,3 @@ update-blocked-list: for ip in $IP_RANGES; do echo "deny $ip;" >> root/etc/nginx/partials/blocked.conf done - -provision: - #!/usr/bin/env sh - set -eou pipefail - - pushd {{justfile_directory()}}/terraform > /dev/null - NODES=$(tofu output -json | jq -r '.nodes.value[]') - popd > /dev/null - - for NODE in $NODES - do - echo "= Provisioning node ${NODE}" - cat install.sh | tailscale ssh ubuntu@${NODE} 'sudo bash -s' - done - -list-servers: - @cd terraform && tofu output -json | jq -r '.nodes.value[]' diff --git a/ingress.tjo.cloud/root/etc/default/dyndns b/ingress.tjo.cloud/root/etc/default/dyndns new file mode 100644 index 0000000..b18c3d9 --- /dev/null +++ b/ingress.tjo.cloud/root/etc/default/dyndns @@ -0,0 +1,17 @@ +## Path: +## Description: DynDNS settings +## Type: string +## Default: "" +## ServiceRestart: dydns + +# If set to "true", removes extra DNS records if more than one A record is found on a subdomain. +# Note that if this is not enabled, the script will NOT update subdomains with more than one A record +# (default: false) +REMOVE_DUPLICATES=true + +# Polling time in seconds. +# (default: 300) +SLEEP_INTERVAL=600 + +# The domain your subdomain is registered at. (i.e. foo.com for home.foo.com) +DOMAIN=ingress.tjo.cloud \ No newline at end of file diff --git a/ingress.tjo.cloud/root/systemd/system/dyndns.service b/ingress.tjo.cloud/root/systemd/system/dyndns.service new file mode 100644 index 0000000..5fd0a49 --- /dev/null +++ b/ingress.tjo.cloud/root/systemd/system/dyndns.service @@ -0,0 +1,8 @@ +[Unit] +Description=Dynamic DNS Updater + +[Service] +ExecStart=/usr/local/bin/dyndns + +[Install] +WantedBy=multi-user.target diff --git a/ingress.tjo.cloud/root/systemd/system/geo-ip-update.timer b/ingress.tjo.cloud/root/systemd/system/geo-ip-update.timer deleted file mode 100644 index e69de29..0000000 diff --git a/ingress.tjo.cloud/root/systemd/system/webhook.service b/ingress.tjo.cloud/root/systemd/system/webhook.service deleted file mode 100644 index 5aa7b0f..0000000 --- a/ingress.tjo.cloud/root/systemd/system/webhook.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -Description=Webhooks - -[Service] -ExecStart=/usr/bin/webhook -hooks /etc/webhook/hooks.json -port 7777 -verbose - -[Install] -WantedBy=multi-user.target diff --git a/ingress.tjo.cloud/root/usr/local/bin/dyndns b/ingress.tjo.cloud/root/usr/local/bin/dyndns new file mode 100755 index 0000000..302d445 --- /dev/null +++ b/ingress.tjo.cloud/root/usr/local/bin/dyndns @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# vim: set filetype=sh +set -euo pipefail + +api_host="https://api.digitalocean.com/v2" +sleep_interval=${SLEEP_INTERVAL:-300} +remove_duplicates=${REMOVE_DUPLICATES:-"false"} + +# Only services with ipv6 supported are listed here. +# And are not using cloudflare or similar services +# that may block requests from this script. +services=( + "ifconfig.io" +) + +info() { + echo "$1" >/dev/stdout +} + +warn() { + echo "$1" >/dev/stderr +} + +error() { + warn "$1" + exit 1 +} + +test -f "${DIGITALOCEAN_TOKEN_FILE:-}" && DIGITALOCEAN_TOKEN="$(cat "$DIGITALOCEAN_TOKEN_FILE")" +test -z "$DIGITALOCEAN_TOKEN" && error "DIGITALOCEAN_TOKEN not set!" +test -z "${DOMAIN}" && error "DOMAIN not set!" +test -z "${NAME}" && error "NAME not set!" + +dns_list="$api_host/domains/$DOMAIN/records" + +configure_record() { + # disable glob expansion + set -f + + ip=$1 + type=$2 + + for sub in ${NAME//;/ }; do + record_id=$(echo "$domain_records" | jq ".domain_records[] | select(.type == \"$type\" and .name == \"$sub\") | .id") + record_data=$(echo "$domain_records" | jq -r ".domain_records[] | select(.type == \"$type\" and .name == \"$sub\") | .data") + + if [ "$(echo "$record_id" | wc -l)" -ge 2 ]; then + if [[ "${remove_duplicates}" == "true" ]]; then + echo "'$sub' domain name has duplicate DNS records, removing duplicates" + record_id_to_delete=$(echo "$record_id" | tail -n +2) + record_id=$(echo "$record_id" | head -1) + record_data=$(echo "$record_data" | head -1) + + while IFS= read -r line; do + curl -s -X DELETE \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ + "$dns_list/$line" &>/dev/null + done <<<"$record_id_to_delete" + else + warn "Unable to update '$sub' domain name as it has duplicate DNS records. Set REMOVE_DUPLICATES='true' to remove them." + continue + fi + fi + + # re-enable glob expansion + set +f + + data="{\"type\": \"$type\", \"name\": \"$sub\", \"data\": \"$ip\"}" + url="$dns_list/$record_id" + + if [[ -z $record_id ]]; then + info "No record found with '$sub' domain name. Creating record, sending data=$data to url=$url" + + new_record=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ + -d "$data" \ + "$url") + + record_data=$(echo "$new_record" | jq -r ".data") + fi + + if [[ "$ip" != "$record_data" ]]; then + info "existing DNS record address ($record_data) doesn't match current IP ($ip), sending data=$data to url=$url" + + curl -s -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ + -d "$data" \ + "$url" &>/dev/null + else + info "existing DNS record address ($record_data) did not need updating" + fi + done +} + +while (true); do + domain_records=$(curl -s -X GET \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \ + "$dns_list?per_page=200") + + for service in "${services[@]}"; do + info "Trying with $service..." + + ipv4="$(curl -4 -s -f --connect-timeout 2 "$service" || echo "")" + ipv6="$(curl -6 -s -f --connect-timeout 2 "$service" || echo "")" + + if [[ -n "$ipv4$ipv6" ]]; then + break + else + warn "Failed to retrieve IP from $service" + fi + done + + if [[ -z $ipv4 ]]; then + warn "IPv4 wasn't retrieved within allowed interval. Will try $sleep_interval seconds later.." + else + info "Found IPv4 address $ipv4" + configure_record "$ipv4" "A" + fi + + if [[ -z $ipv6 ]]; then + warn "IPv6 wasn't retrieved within allowed interval. Will try $sleep_interval seconds later.." + else + info "Found IPv6 address $ipv6" + configure_record "$ipv6" "AAAA" + fi + + sleep "$sleep_interval" +done diff --git a/ingress.tjo.cloud/terraform/node.tf b/ingress.tjo.cloud/terraform/node.tf index 9d860d6..09e819e 100644 --- a/ingress.tjo.cloud/terraform/node.tf +++ b/ingress.tjo.cloud/terraform/node.tf @@ -14,6 +14,9 @@ locals { tailscale = { auth_key = tailscale_tailnet_key.key.key } + digitalocean = { + token = var.digitalocean_token + } } }) } @@ -60,8 +63,9 @@ resource "proxmox_virtual_environment_file" "userdata" { power_state: mode: reboot runcmd: - - git clone https://code.tjo.space/tjo-cloud/ingress.git /srv - - /srv/install.sh + - git clone --depth 1 --no-checkout --filter=tree:0 https://github.com/tjo-space/tjo-cloud-infrastructure.git /srv + - cd /srv && git sparse-checkout set --no-cone /ingress.tjo.cloud && git checkout + - /srv/ingress.tjo.cloud/install.sh EOF file_name = "${each.value.host}.ingress.tjo.cloud.userconfig.yaml" } @@ -87,7 +91,7 @@ Repo: https://code.tjo.space/tjo-cloud/ingress timeout_stop_vm = 60 timeout_shutdown_vm = 60 timeout_reboot = 60 - timeout_create = 600 + timeout_create = 60 cpu { cores = each.value.cores diff --git a/ingress.tjo.cloud/terraform/variables.tf b/ingress.tjo.cloud/terraform/variables.tf index d219c94..70fc9c8 100644 --- a/ingress.tjo.cloud/terraform/variables.tf +++ b/ingress.tjo.cloud/terraform/variables.tf @@ -37,3 +37,8 @@ variable "tailscale_apikey" { type = string sensitive = true } + +variable "digitalocean_token" { + type = string + sensitive = true +} \ No newline at end of file