From 6fdfd338a1915d7d75d325c3448f953e502b496f Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Sun, 6 Oct 2024 17:08:25 +0200
Subject: [PATCH] Add healthchecksio provider and refactor ip-for-host
 collection

---
 .terraform.lock.hcl                      | 18 ++++++++++
 Makefile                                 |  7 +++-
 inventory/serguzim.net.yml               | 30 ++++++-----------
 main.tf                                  |  2 ++
 modules/infrastructure/healthchecksio.tf | 32 ++++++++++++++++++
 modules/infrastructure/main.tf           |  8 +++++
 modules/infrastructure/output.tf         | 33 +++++++++++++++----
 modules/infrastructure/ovh.tf            | 42 ++++++++++++------------
 modules/infrastructure/tailscale.tf      |  6 ++++
 modules/infrastructure/variables.tf      |  5 +++
 output.tf                                |  4 +++
 roles/backup/vars/main.yml               |  2 +-
 secrets.auto.tfvars.example              |  2 ++
 variables.tf                             |  5 +++
 14 files changed, 147 insertions(+), 49 deletions(-)
 create mode 100644 modules/infrastructure/healthchecksio.tf

diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl
index 874aa6c..e83bfba 100644
--- a/.terraform.lock.hcl
+++ b/.terraform.lock.hcl
@@ -136,6 +136,24 @@ provider "registry.opentofu.org/hetznercloud/hcloud" {
   ]
 }
 
+provider "registry.opentofu.org/kristofferahl/healthchecksio" {
+  version     = "1.6.3"
+  constraints = "~> 1.6.0"
+  hashes = [
+    "h1:UG8BANZc208Tjw/Byraf+1W7waDCusGmolAxeiGZ9eQ=",
+    "zh:12cd30c19472bcecbd81f9fb6555f055d8a665477a1b2aadd20a9d43b95df0cf",
+    "zh:174bbe2e2f49132fb4fb9c2b5abaa5743ba49c29f970daae32b7774c8eb259fd",
+    "zh:214c06082f93bbd11919f73e5bdded8f916c990a2c2c46103c827e070120a6ea",
+    "zh:21db5fa48fc61cd88e72d34414063e15d99677d871bdb4d231800c8f7daf6bc1",
+    "zh:39b8d77b8849b3bdd0167ecd465d1980dd42bc1f3cbb9136c9cf8281d36d446a",
+    "zh:5b81782d766a41272767b6804520eb8d07e82986f5d90818b3f000b26e4efdf2",
+    "zh:8c045b836f4e6d86518f12c4b113db43715bad412f7f3cde7afbcc87a21bbfe1",
+    "zh:9c3c6cedd21298d78c93c8a76d8a144a519664bd9def34c5b4a830f2aeeabe6d",
+    "zh:9f1e6a1aba1d21b90c6bf4520c4b5b5d2fce20bd988852a6429acbc6365fa151",
+    "zh:a5d15ed31ac4bc194bc7a32e96e902d7b322ab032c1637c67b16dbee968d1fae",
+  ]
+}
+
 provider "registry.opentofu.org/ovh/ovh" {
   version     = "0.48.0"
   constraints = "~> 0.48.0"
diff --git a/Makefile b/Makefile
index 0c635e7..6491fa4 100644
--- a/Makefile
+++ b/Makefile
@@ -22,12 +22,17 @@ PWD := $(shell pwd)
 		| yq -y '{opentofu: with_entries(.value |= .value)}' \
 		> ./inventory/group_vars/all/opentofu.yaml
 
-outputs: ./dns/hosts.json ./dns/services.json ./inventory/group_vars/all/opentofu.yaml
+output: ./dns/hosts.json ./dns/services.json ./inventory/group_vars/all/opentofu.yaml
 
 
 ./types-dnscontrol.d.ts:
 	dnscontrol write-types
 
+tofu:
+	tofu apply
+	echo "\n=====\n"
+	$(MAKE) output
+
 dns: ./types-dnscontrol.d.ts ./dns/hosts.json ./dns/services.json
 	dnscontrol push
 
diff --git a/inventory/serguzim.net.yml b/inventory/serguzim.net.yml
index 563f930..d717402 100644
--- a/inventory/serguzim.net.yml
+++ b/inventory/serguzim.net.yml
@@ -8,38 +8,28 @@ all:
     local-dev:
       ansible_connection: local
 
-    node001:
-      ansible_host: node001.vpn.serguzim.net
-      ansible_port: "{{ vault_node001.ansible_port }}"
-      ansible_user: "{{ vault_node001.ansible_user }}"
-      interactive_user: "{{ vault_node001.interactive_user }}"
-      host_vpn:
-        domain: node001.vpn.serguzim.net
-        ip: 100.64.0.1
-      host_backup:
-        hc_uid: "{{ vault_node001.backup.hc_uid }}"
-        uptime_kuma_token: "{{ vault_node001.backup.uptime_kuma_token }}"
-
     node002:
-      ansible_host: node002.vpn.serguzim.net
+      ansible_host: "{{ opentofu.hosts.node002.fqdn_vpn }}"
       ansible_port: "{{ vault_node002.ansible_port }}"
       ansible_user: "{{ vault_node002.ansible_user }}"
       interactive_user: "{{ vault_node002.interactive_user }}"
       host_vpn:
-        domain: node002.vpn.serguzim.net
-        ip: 100.64.0.2
+        domain: "{{ opentofu.hosts.node002.fqdn_vpn }}"
+        ip: "{{ opentofu.hosts.node002.ipv4_address_vpn }}"
       host_backup:
-        hc_uid: "{{ vault_node002.backup.hc_uid }}"
+        hc_uid: "{{ opentofu.healthchecksio.backup.node002.id }}"
+        hc_url: "{{ opentofu.healthchecksio.backup.node002.ping_url }}"
         uptime_kuma_token: "{{ vault_node002.backup.uptime_kuma_token }}"
 
     node003:
-      ansible_host: node003.vpn.serguzim.net
+      ansible_host: "{{ opentofu.hosts.node003.fqdn_vpn }}"
       ansible_port: "{{ vault_node003.ansible_port }}"
       ansible_user: "{{ vault_node003.ansible_user }}"
       interactive_user: "{{ vault_node003.interactive_user }}"
       host_vpn:
-        domain: node003.vpn.serguzim.net
-        ip: 100.110.16.30
+        domain: "{{ opentofu.hosts.node003.fqdn_vpn }}"
+        ip: "{{ opentofu.hosts.node003.ipv4_address_vpn }}"
       host_backup:
-        hc_uid: "{{ vault_node003.backup.hc_uid }}"
+        hc_uid: "{{ opentofu.healthchecksio.backup.node003.id }}"
+        hc_url: "{{ opentofu.healthchecksio.backup.node003.ping_url }}"
         uptime_kuma_token: "{{ vault_node003.backup.uptime_kuma_token }}"
diff --git a/main.tf b/main.tf
index 5054fd4..8dbc4cb 100644
--- a/main.tf
+++ b/main.tf
@@ -42,6 +42,8 @@ module "infrastructure" {
 
   hcloud_token = var.hcloud_token
 
+  healthchecksio_api_key = var.healthchecksio_api_key
+
   ovh_application_key = var.ovh_application_key
   ovh_application_secret = var.ovh_application_secret
   ovh_consumer_key = var.ovh_consumer_key
diff --git a/modules/infrastructure/healthchecksio.tf b/modules/infrastructure/healthchecksio.tf
new file mode 100644
index 0000000..748a1f1
--- /dev/null
+++ b/modules/infrastructure/healthchecksio.tf
@@ -0,0 +1,32 @@
+data "healthchecksio_channel" "email" {
+  kind = "email"
+}
+
+data "healthchecksio_channel" "signal" {
+  kind = "signal"
+}
+
+data "healthchecksio_channel" "ntfy" {
+  kind = "ntfy"
+}
+
+resource "healthchecksio_check" "backup" {
+  for_each = var.hosts
+
+  name = "backup@${each.value.hostname}"
+  desc = "A check for the backup on ${each.value.hostname}"
+
+  tags = [
+    "backup",
+    each.value.hostname,
+  ]
+
+  channels = [
+    data.healthchecksio_channel.email.id,
+    data.healthchecksio_channel.signal.id,
+    data.healthchecksio_channel.ntfy.id,
+  ]
+
+  timeout = 86400
+  grace = 1800
+}
diff --git a/modules/infrastructure/main.tf b/modules/infrastructure/main.tf
index e34ce79..a184c5a 100644
--- a/modules/infrastructure/main.tf
+++ b/modules/infrastructure/main.tf
@@ -8,6 +8,10 @@ terraform {
       source = "hetznercloud/hcloud"
       version = "~> 1.45.0"
     }
+    healthchecksio = {
+      source = "kristofferahl/healthchecksio"
+      version = "~> 1.6.0"
+    }
     ovh = {
       source = "ovh/ovh"
       version = "~> 0.48.0"
@@ -34,6 +38,10 @@ provider "hcloud" {
   token = var.hcloud_token
 }
 
+provider "healthchecksio" {
+  api_key = var.healthchecksio_api_key
+}
+
 provider "ovh" {
   endpoint = "ovh-eu"
   application_key = var.ovh_application_key
diff --git a/modules/infrastructure/output.tf b/modules/infrastructure/output.tf
index 8e3d2b1..075f7e5 100644
--- a/modules/infrastructure/output.tf
+++ b/modules/infrastructure/output.tf
@@ -1,17 +1,38 @@
 output "hosts" {
   value = {
-    for subdomain in distinct([for record in ovh_domain_zone_record.server_records : record.subdomain]) : 
-    subdomain => {
-      "hostname" = subdomain
-      "fqdn" = "${subdomain}.${ovh_domain_zone_record.server_records["${subdomain}:ipv4"].zone}"
+    for key, host in var.hosts :
+    key => {
+      "hostname" = host.hostname
+      "fqdn" = "${host.hostname}.serguzim.net"
+      "fqdn_vpn" = "${host.hostname}.vpn.serguzim.net"
       "ipv4_address" = try(
-        ovh_domain_zone_record.server_records["${subdomain}:ipv4"].target, 
+        local.server_addresses_separated["${key}:ipv4"].address,
         null
       )
       "ipv6_address" = try(
-        ovh_domain_zone_record.server_records["${subdomain}:ipv6"].target, 
+        local.server_addresses_separated["${key}:ipv6"].address,
         null
       )
+
+      ipv4_address_vpn = try(
+        local.tailscale_host_addresses_separated["${key}:ipv4"].address,
+        null
+      )
+      ipv6_address_vpn = try(
+        local.tailscale_host_addresses_separated["${key}:ipv6"].address,
+        null
+      )
+    }
+  }
+}
+
+output "healthchecksio" {
+  value = {
+    backup = {
+      for key, check in healthchecksio_check.backup : key => {
+        "id" = check.id
+        "ping_url" = check.ping_url
+      }
     }
   }
 }
diff --git a/modules/infrastructure/ovh.tf b/modules/infrastructure/ovh.tf
index ffc11ef..ff2957d 100644
--- a/modules/infrastructure/ovh.tf
+++ b/modules/infrastructure/ovh.tf
@@ -1,8 +1,9 @@
 locals {
   server_addresses = flatten([
     [
-      for host in contabo_instance.nodes : [
+      for key, host in contabo_instance.nodes : [
         {
+          key = key
           hostname = host.display_name
           ipv4_address = host.ip_config[0].v4[0].ip
           ipv6_address = host.ip_config[0].v6[0].ip
@@ -10,8 +11,9 @@ locals {
       ]
     ],
     [
-      for host in hcloud_server.nodes : [
+      for key, host in hcloud_server.nodes : [
         {
+          key = key
           hostname = host.name
           ipv4_address = host.ipv4_address
           ipv6_address = host.ipv6_address
@@ -20,34 +22,32 @@ locals {
     ]
   ])
 
-  server_addresses_separated = flatten([
-    for host in local.server_addresses : [
-      {
+  server_addresses_separated = merge([
+    for host in local.server_addresses : {
+      "${host.key}:ipv4" = {
         hostname = host.hostname
-        key      = "${host.hostname}:ipv4"
         address  = host.ipv4_address
       },
-      {
+      "${host.key}:ipv6" = {
         hostname = host.hostname
-        key      = "${host.hostname}:ipv6"
         address  = host.ipv6_address
       },
-    ]
-  ])
+    }
+  ]...)
 
-  tailscale_host_addresses = flatten([
-    for host in data.tailscale_devices.nodes.devices : [
-      for index, address in host.addresses : {
-        hostname = host.hostname
-        key      = "${host.hostname}:${index}"
-        address  = address
-      }
-    ]
-  ])
+  tailscale_host_addresses_separated = merge([
+    for host in data.tailscale_devices.nodes.devices : {
+      for address in host.addresses :
+        "${host.hostname}:${strcontains(address, ":") ? "ipv6" : "ipv4"}" => {
+          hostname = host.hostname
+          address  = address
+        }
+    }
+  ]...)
 }
 
 resource "ovh_domain_zone_record" "server_records" {
-  for_each  = { for entry in local.server_addresses_separated: entry.key => entry }
+  for_each  = local.server_addresses_separated
   zone      = "serguzim.net"
   subdomain = each.value.hostname
   fieldtype = strcontains(each.value.address, ":") ? "AAAA" : "A"
@@ -56,7 +56,7 @@ resource "ovh_domain_zone_record" "server_records" {
 }
 
 resource "ovh_domain_zone_record" "tailscale_vpn" {
-  for_each  = { for entry in local.tailscale_host_addresses: entry.key => entry }
+  for_each  = local.tailscale_host_addresses_separated
   zone      = "serguzim.net"
   subdomain = "${each.value.hostname}.vpn"
   fieldtype = strcontains(each.value.address, ":") ? "AAAA" : "A"
diff --git a/modules/infrastructure/tailscale.tf b/modules/infrastructure/tailscale.tf
index a9250bd..64debee 100644
--- a/modules/infrastructure/tailscale.tf
+++ b/modules/infrastructure/tailscale.tf
@@ -9,3 +9,9 @@ resource "tailscale_tailnet_key" "cloud_init_key" {
 data "tailscale_devices" "nodes" {
   name_prefix = "node"
 }
+
+locals {
+  tailscale_devices = {
+    for host in data.tailscale_devices.nodes.devices : host.hostname => host
+  }
+}
diff --git a/modules/infrastructure/variables.tf b/modules/infrastructure/variables.tf
index 7728ad6..9cc6dbc 100644
--- a/modules/infrastructure/variables.tf
+++ b/modules/infrastructure/variables.tf
@@ -20,6 +20,11 @@ variable "hcloud_token" {
 }
 
 
+variable "healthchecksio_api_key" {
+  sensitive = true
+}
+
+
 variable "ovh_application_key" {
   sensitive = true
 }
diff --git a/output.tf b/output.tf
index 03c8cae..a99a181 100644
--- a/output.tf
+++ b/output.tf
@@ -7,6 +7,10 @@ output "authentik_data" {
   sensitive = true
 }
 
+output "healthchecksio" {
+  value = module.infrastructure.healthchecksio
+}
+
 output "postgresql_data" {
   value = module.services.postgresql_data
   sensitive = true
diff --git a/roles/backup/vars/main.yml b/roles/backup/vars/main.yml
index aa554d9..06dd8a6 100644
--- a/roles/backup/vars/main.yml
+++ b/roles/backup/vars/main.yml
@@ -13,7 +13,7 @@ backup_msg_success: "Backup successful"
 backup_curl_base: 'curl -L -m 10 --retry 5'
 backup_hc_curl_base: '{{ backup_curl_base }} -X POST -H "Content-Type: text/plain"'
 backup_uk_curl_base: '{{ backup_curl_base }}'
-backup_hc_url: 'https://hc-ping.com/{{ host_backup.hc_uid }}'
+backup_hc_url: '{{ host_backup.hc_url }}'
 backup_uk_url: 'https://status.serguzim.me/api/push/{{ host_backup.uptime_kuma_token }}'
 
 backup_hc_command_start: '{{ backup_hc_curl_base }} --data "{{ backup_msg_start }}" {{ backup_hc_url }}/start'
diff --git a/secrets.auto.tfvars.example b/secrets.auto.tfvars.example
index 41ee2b5..8e4fbe3 100644
--- a/secrets.auto.tfvars.example
+++ b/secrets.auto.tfvars.example
@@ -14,6 +14,8 @@ contabo_pass = ""
 
 hcloud_token = ""
 
+healthchecksio_api_key = ""
+
 ovh_application_key = ""
 ovh_application_secret = ""
 ovh_consumer_key = ""
diff --git a/variables.tf b/variables.tf
index b1908fc..6e87e35 100644
--- a/variables.tf
+++ b/variables.tf
@@ -50,6 +50,11 @@ variable "hcloud_token" {
 }
 
 
+variable "healthchecksio_api_key" {
+  sensitive = true
+}
+
+
 variable "ovh_application_key" {
   sensitive = true
 }