Initial infrastructure mono-repo scaffold

Consolidates nixos, docker, raspi, and debian-configuration into a single
infrastructure-as-code repo. Includes:

- ansible/: full inventory + proxmox-hardening, freeipa, and raspberrypi
  roles (converted from debian-configuration bash scripts)
- terraform/: Proxmox VMs, Dynu DNS, Pi-hole (decommissioned stub),
  Docker container catalog — migrated from docker/infrastructure/terraform/
- stacks/docker/, stacks/raspi/, nixos/: placeholder READMEs pending
  git subtree population (see implementation plan)
- docs/: internal MkDocs site with architecture, network topology, runbooks,
  and drift-detection guide; external sanitized site
- scripts/: drift-detect.sh, docs-build.sh, install-hooks.sh, check-secrets.sh
- CI: secret-scan (push/PR), drift-detect (daily), docs-build (on change)
- Pi-hole removed throughout — DNS is FreeIPA, DHCP is router

See docs/internal/implementation-plan.md for the phased rollout after
pushing to Gitea.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvNjoxTWEDkhXsd1Dq2ETP
This commit is contained in:
beatzaplentyandClaude Sonnet 4.6 committed 2026-07-30 07:07:47 +10:00
commit f85c65870f
125 files changed
+8186

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
# Dynu Terraform Layer (Brownfield DNS Reconciliation)
This Terraform root is for **Dynu DNS brownfield reconciliation**. The intended pattern is:
1. Import the existing root domain object.
2. Read inventory through `data.dynu_dns_records.root`.
3. Generate reviewable `dynu_dns_record` resources and import commands.
4. Import every existing DNS record into matching Terraform resources.
5. Use `terraform plan` as the reconciliation check before any apply.
## Provider behavior to keep in mind
- Source: `beatz174-bit/dynu`
- `dynu_domain` import requires a **numeric Dynu domain ID**.
- Importing `dynu_domain` imports only the root domain object.
- It **does not** import DNS records/subdomains.
- `dynu_dns_record` imports require `<domain_id>/<record_id>`.
## Variables
- `dynu_root_domain` (default: `lan.ddnsgeek.com`)
- `dynu_api_key` (sensitive)
- `dynu_username` / `dynu_password` (optional)
## Safe validation commands
```bash
cd infrastructure/terraform/dynu
terraform fmt -check -recursive
terraform init -backend=false -input=false
terraform validate
python3 -m py_compile scripts/generate-brownfield-records.py
```
## Brownfield workflow
```bash
cd infrastructure/terraform/dynu
terraform init
terraform import dynu_domain.lan_ddnsgeek_com '<numeric-dynu-domain-id>'
terraform apply -refresh-only
terraform output -json dynu_dns_records > /tmp/dynu-records.json
python3 scripts/generate-brownfield-records.py --dry-run
python3 scripts/generate-brownfield-records.py --overwrite
# Review generated/dynu_dns_records.generated.tf
# Review generated/import-dynu-dns-records.sh
bash generated/import-dynu-dns-records.sh
terraform plan
```
## What each component means
- `data.dynu_dns_records.root`: read-only live inventory from Dynu.
- `generated/dynu_dns_records.generated.tf`: generated management-intent resources; includes `prevent_destroy = true` on each record.
- `generated/import-dynu-dns-records.sh`: imports each discovered record to its generated `dynu_dns_record` address using `<domain_id>/<record_id>`.
- `terraform plan` after imports: reconciliation checkpoint. Any create/update/delete must be reviewed manually before apply.
## Generated artifacts
The helper script writes these files under `generated/`:
- `generated/dynu_dns_records_inventory.json`
- `generated/dynu_dns_records.generated.tf`
- `generated/import-dynu-dns-records.sh`
These are generated outputs meant for operator review before use in production.
### Generator output selection (interactive + automation)
The brownfield generator defaults to Terraform output `dynu_dns_records`:
```bash
python3 scripts/generate-brownfield-records.py --dry-run
```
If the default output is missing/unusable and stdin is interactive, the script shows a picker of available Terraform outputs and indicates which ones are usable for DNS imports.
```bash
# Interactive mode: choose from available Terraform outputs
python3 scripts/generate-brownfield-records.py --dry-run
# Non-interactive mode: specify output explicitly
python3 scripts/generate-brownfield-records.py \
--records-output dynu_dns_inventory \
--dry-run
# Disable menu and fail fast
python3 scripts/generate-brownfield-records.py \
--no-interactive \
--dry-run
# Use saved terraform output JSON and choose interactively
terraform output -json > generated/terraform-output.json
python3 scripts/generate-brownfield-records.py \
--from-file generated/terraform-output.json \
--dry-run
```
Notes:
- The menu shows Terraform outputs currently stored in state.
- If newly added outputs do not appear, run:
```bash
terraform apply -refresh-only
```
- The selected output must contain real Dynu provider record fields:
- `id`
- `domain_id`
- `hostname`
- `record_type`
## Troubleshooting
### Plan shows a large wall of `+` values under outputs
Cause:
Terraform is planning to save **new output values** to state (for example, live records from `data.dynu_dns_records.root`). This is not creating DNS records by itself.
How to verify:
- Output-only changes appear under `Changes to Outputs`.
- Real DNS changes appear as `dynu_dns_record` resource create/update/delete actions.
Use:
```bash
terraform apply -refresh-only
```
to persist refreshed data source and output values only.
### Error: `There is no function named "regexreplace"`
Cause:
`regexreplace` is not a Terraform function. Resource-name slugification should not be implemented in Terraform HCL for this workflow.
Fix:
- Keep `inventory.tf` focused on reading live records via `data.dynu_dns_records.root`.
- Keep Terraform outputs simple (for example, `<domain_id>/<record_id>` mappings).
- Let `scripts/generate-brownfield-records.py` generate Terraform-safe resource names with Python `tf_name(record)`.
### Error: `'"'"'dynu_dns_records'"'"'`
Cause:
The helper script reads `terraform output -json` and expects an output named `dynu_dns_records`.
Fix:
```bash
cd infrastructure/terraform/dynu
terraform init
terraform apply -refresh-only
terraform output -json | jq 'keys'
```
Confirm `dynu_dns_records` appears in the key list.
If it does not, check that the Terraform config contains:
```hcl
data "dynu_dns_records" "root" {
hostname = var.dynu_root_domain
}
output "dynu_dns_records" {
value = data.dynu_dns_records.root.records
}
```
Then rerun:
```bash
python3 scripts/generate-brownfield-records.py --dry-run
```
+7
View File
@@ -0,0 +1,7 @@
data "dynu_domain" "lan" {
hostname = "lan.ddnsgeek.com"
}
output "dynu_domain_id" {
value = data.dynu_domain.lan.domain.id
}
+11
View File
@@ -0,0 +1,11 @@
locals {
dynu_domain = var.dynu_root_domain
}
# Import-first resource skeleton for the production Dynu zone.
# `name` is required by provider schema and can be reconciled after import.
resource "dynu_domain" "lan_ddnsgeek_com" {
name = local.dynu_domain
}
+518
View File
@@ -0,0 +1,518 @@
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
hostname = "kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 90
enabled = true
content = "120.155.99.146"
node_name = "kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
hostname = "lan.ddnsgeek.com"
record_type = "SOA"
ttl = 120
enabled = true
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
@@ -0,0 +1,518 @@
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
hostname = "auth.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "auth"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
hostname = "edge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "edge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
hostname = "familytree.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "familytree"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
hostname = "gitea.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gitea"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
hostname = "gotify.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "gotify"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
hostname = "grafana.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "grafana"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
hostname = "influxdb.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "influxdb"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
hostname = "kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 60
enabled = true
content = "120.155.99.146"
node_name = "kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
hostname = "lan.ddnsgeek.com"
record_type = "SOA"
ttl = 120
enabled = true
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
hostname = "monitor-kuma.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "monitor-kuma"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
hostname = "mtls-bridge.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "mtls-bridge"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
hostname = "nextcloud.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "nextcloud"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
hostname = "node-red.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "node-red"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
hostname = "passbolt.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "passbolt"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
hostname = "portainer.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "portainer"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
hostname = "prometheus.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "prometheus"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
hostname = "searxng.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "searxng"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
hostname = "shifts.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "shifts"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
hostname = "stockfill.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "stockfill"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
dynamic = true
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
hostname = "traefik.lan.ddnsgeek.com"
record_type = "A"
ttl = 120
enabled = true
content = "167.179.167.166"
group = "home"
node_name = "traefik"
lifecycle {
prevent_destroy = true
}
}
@@ -0,0 +1 @@
{"domain":"lan.ddnsgeek.com","provider":"dynu","record_count":15,"records":{"auth":{"fqdn":"auth.lan.ddnsgeek.com","hostname":"auth","proxied":null,"purpose":"Authentication portal","record_type":null,"service":"authelia","source":"core/authelia/docker-compose.yml","target":null,"ttl":null},"familytree":{"fqdn":"familytree.lan.ddnsgeek.com","hostname":"familytree","proxied":null,"purpose":"Family tree application","record_type":null,"service":"gramps","source":"apps/gramps/docker-compose.yml","target":null,"ttl":null},"gitea":{"fqdn":"gitea.lan.ddnsgeek.com","hostname":"gitea","proxied":null,"purpose":"Gitea service endpoint","record_type":null,"service":"gitea","source":"apps/gitea/docker-compose.yml","target":null,"ttl":null},"gotify":{"fqdn":"gotify.lan.ddnsgeek.com","hostname":"gotify","proxied":null,"purpose":"Gotify notifications","record_type":null,"service":"gotify","source":"monitoring/gotify/docker-compose.yml","target":null,"ttl":null},"grafana":{"fqdn":"grafana.lan.ddnsgeek.com","hostname":"grafana","proxied":null,"purpose":"Grafana monitoring UI","record_type":null,"service":"grafana","source":"monitoring/grafana/docker-compose.yml","target":null,"ttl":null},"influxdb":{"fqdn":"influxdb.lan.ddnsgeek.com","hostname":"influxdb","proxied":null,"purpose":"InfluxDB metrics endpoint","record_type":null,"service":"influxdb","source":"monitoring/influxdb/docker-compose.yml","target":null,"ttl":null},"monitor_kuma":{"fqdn":"monitor-kuma.lan.ddnsgeek.com","hostname":"monitor-kuma","proxied":null,"purpose":"Uptime Kuma monitoring UI","record_type":null,"service":"uptime-kuma","source":"monitoring/uptime-kuma/docker-compose.yml","target":null,"ttl":null},"mtls_bridge":{"fqdn":"mtls-bridge.lan.ddnsgeek.com","hostname":"mtls-bridge","proxied":null,"purpose":"mTLS bridge API","record_type":null,"service":"mtls-bridge","source":"monitoring/mtls-bridge/docker-compose.yml","target":null,"ttl":null},"nextcloud":{"fqdn":"nextcloud.lan.ddnsgeek.com","hostname":"nextcloud","proxied":null,"purpose":"Nextcloud service endpoint","record_type":null,"service":"nextcloud-webapp","source":"apps/nextcloud/docker-compose.yml","target":null,"ttl":null},"node_red":{"fqdn":"node-red.lan.ddnsgeek.com","hostname":"node-red","proxied":null,"purpose":"Node-RED automation UI/API","record_type":null,"service":"node-red","source":"monitoring/node-red/docker-compose.yml","target":null,"ttl":null},"passbolt":{"fqdn":"passbolt.lan.ddnsgeek.com","hostname":"passbolt","proxied":null,"purpose":"Passbolt password management","record_type":null,"service":"passbolt-webapp","source":"apps/passbolt/docker-compose.yml","target":null,"ttl":null},"portainer":{"fqdn":"portainer.lan.ddnsgeek.com","hostname":"portainer","proxied":null,"purpose":"Portainer admin endpoint","record_type":null,"service":"portainer","source":"monitoring/portainer/docker-compose.yml","target":null,"ttl":null},"prometheus":{"fqdn":"prometheus.lan.ddnsgeek.com","hostname":"prometheus","proxied":null,"purpose":"Prometheus metrics endpoint","record_type":null,"service":"prometheus","source":"monitoring/prometheus/docker-compose.yml","target":null,"ttl":null},"searxng":{"fqdn":"searxng.lan.ddnsgeek.com","hostname":"searxng","proxied":null,"purpose":"SearXNG search endpoint","record_type":null,"service":"searxng","source":"apps/searxng/docker-compose.yml","target":null,"ttl":null},"traefik":{"fqdn":"traefik.lan.ddnsgeek.com","hostname":"traefik","proxied":null,"purpose":"Traefik dashboard/API endpoint","record_type":null,"service":"traefik","source":"core/traefik/docker-compose.yml","target":null,"ttl":null}}}
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Imports existing Dynu DNS records into Terraform state.
# Does not apply changes.
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${TF_ROOT}"
# Re-running imports will fail for resources already in state.
# This script skips imports when state already contains the resource address.
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099'
else
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' '9695470/18483099'
fi
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048'
else
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' '9695470/19646048'
fi
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241'
else
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' '9695470/10453241'
fi
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062'
else
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' '9695470/19646062'
fi
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685'
else
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' '9695470/17017685'
fi
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056'
else
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' '9695470/19646056'
fi
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463'
else
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' '9695470/14682463'
fi
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063'
else
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' '9695470/19646063'
fi
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061'
else
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' '9695470/17439061'
fi
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047'
else
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' '9695470/19646047'
fi
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762'
else
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' '9695470/18113762'
fi
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050'
else
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' '9695470/19646050'
fi
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198'
else
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' '9695470/18562198'
fi
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059'
else
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' '9695470/19646059'
fi
if terraform state show 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978'
else
terraform import 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' '9695470/17454978'
fi
if terraform state show 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.lan_ddnsgeek_com_soa_8299670'
else
terraform import 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' '9695470/8299670'
fi
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342'
else
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' '9695470/17462342'
fi
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051'
else
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' '9695470/19646051'
fi
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643'
else
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' '9695470/19232643'
fi
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058'
else
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' '9695470/19646058'
fi
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260'
else
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' '9695470/10453260'
fi
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057'
else
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' '9695470/19646057'
fi
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230'
else
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' '9695470/19041230'
fi
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053'
else
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' '9695470/19646053'
fi
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262'
else
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' '9695470/10453262'
fi
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049'
else
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' '9695470/19646049'
fi
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810'
else
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' '9695470/17458810'
fi
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046'
else
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' '9695470/19646046'
fi
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311'
else
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' '9695470/18483311'
fi
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061'
else
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' '9695470/19646061'
fi
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263'
else
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' '9695470/10453263'
fi
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055'
else
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' '9695470/19646055'
fi
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565'
else
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' '9695470/15901565'
fi
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052'
else
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' '9695470/19646052'
fi
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867'
else
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' '9695470/17081867'
fi
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060'
else
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' '9695470/19646060'
fi
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240'
else
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' '9695470/10453240'
fi
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' >/dev/null 2>&1; then
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054'
else
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' '9695470/19646054'
fi
+16
View File
@@ -0,0 +1,16 @@
# Copy this file to imports.tf and adjust IDs after confirming the
# published provider docs for import ID formats.
# For dynu_domain, import ID is commonly the root domain name.
import {
to = dynu_domain.lan_ddnsgeek_com
id = var.dynu_root_domain
}
# DNS record imports are intentionally examples only because the provider
# requires explicit record_type/hostname in config before import.
#
# import {
# to = dynu_dns_record.grafana_lan_ddnsgeek_com
# id = var.dynu_record_import_id
# }
+3
View File
@@ -0,0 +1,3 @@
data "dynu_dns_records" "root" {
hostname = var.dynu_root_domain
}
+47
View File
@@ -0,0 +1,47 @@
output "dynu_domain" {
description = "Primary Dynu domain represented by this Terraform root."
value = local.dynu_domain
}
output "dynu_dns_records_catalog" {
description = "Documentation catalog of expected Dynu DNS records discovered from repo service exposure."
value = local.dynu_dns_records_catalog
}
output "dynu_dns_inventory" {
description = "Documentation-friendly Dynu DNS inventory for export and merge into broader infrastructure docs."
value = {
provider = "dynu"
domain = local.dynu_domain
record_count = length(local.dynu_dns_records_catalog)
records = local.dynu_dns_records_catalog
}
}
output "dynu_root_domain_id" {
description = "Dynu numeric domain ID resolved from dynu_root_domain."
value = data.dynu_dns_records.root.domain_id
}
output "dynu_root_domain_name" {
description = "Dynu root domain name resolved from dynu_root_domain."
value = data.dynu_dns_records.root.domain_name
}
output "dynu_dns_records" {
description = "Full read-only DNS record inventory returned by Dynu."
value = data.dynu_dns_records.root.records
}
output "dynu_dns_hostnames" {
description = "Sorted hostname list discovered for dynu_root_domain."
value = sort(distinct([for record in data.dynu_dns_records.root.records : record.hostname]))
}
output "dynu_dns_record_import_ids" {
description = "Map of Dynu DNS record identity to provider import IDs in domain_id/record_id format."
value = {
for record in data.dynu_dns_records.root.records :
format("%s/%s/%s", record.hostname, record.record_type, record.id) => format("%s/%s", record.domain_id, record.id)
}
}
+4
View File
@@ -0,0 +1,4 @@
provider "dynu" {
# Keep auth local-only; do not commit credentials.
api_key = var.dynu_api_key
}
+161
View File
@@ -0,0 +1,161 @@
locals {
dynu_dns_records_catalog_base = {
auth = {
hostname = "auth"
service = "authelia"
source = "core/authelia/docker-compose.yml"
purpose = "Authentication portal"
record_type = null
ttl = null
target = null
proxied = null
}
gitea = {
hostname = "gitea"
service = "gitea"
source = "apps/gitea/docker-compose.yml"
purpose = "Gitea service endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
gotify = {
hostname = "gotify"
service = "gotify"
source = "monitoring/gotify/docker-compose.yml"
purpose = "Gotify notifications"
record_type = null
ttl = null
target = null
proxied = null
}
grafana = {
hostname = "grafana"
service = "grafana"
source = "monitoring/grafana/docker-compose.yml"
purpose = "Grafana monitoring UI"
record_type = null
ttl = null
target = null
proxied = null
}
familytree = {
hostname = "familytree"
service = "gramps"
source = "apps/gramps/docker-compose.yml"
purpose = "Family tree application"
record_type = null
ttl = null
target = null
proxied = null
}
influxdb = {
hostname = "influxdb"
service = "influxdb"
source = "monitoring/influxdb/docker-compose.yml"
purpose = "InfluxDB metrics endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
monitor_kuma = {
hostname = "monitor-kuma"
service = "uptime-kuma"
source = "monitoring/uptime-kuma/docker-compose.yml"
purpose = "Uptime Kuma monitoring UI"
record_type = null
ttl = null
target = null
proxied = null
}
mtls_bridge = {
hostname = "mtls-bridge"
service = "mtls-bridge"
source = "monitoring/mtls-bridge/docker-compose.yml"
purpose = "mTLS bridge API"
record_type = null
ttl = null
target = null
proxied = null
}
nextcloud = {
hostname = "nextcloud"
service = "nextcloud-webapp"
source = "apps/nextcloud/docker-compose.yml"
purpose = "Nextcloud service endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
node_red = {
hostname = "node-red"
service = "node-red"
source = "monitoring/node-red/docker-compose.yml"
purpose = "Node-RED automation UI/API"
record_type = null
ttl = null
target = null
proxied = null
}
passbolt = {
hostname = "passbolt"
service = "passbolt-webapp"
source = "apps/passbolt/docker-compose.yml"
purpose = "Passbolt password management"
record_type = null
ttl = null
target = null
proxied = null
}
portainer = {
hostname = "portainer"
service = "portainer"
source = "monitoring/portainer/docker-compose.yml"
purpose = "Portainer admin endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
prometheus = {
hostname = "prometheus"
service = "prometheus"
source = "monitoring/prometheus/docker-compose.yml"
purpose = "Prometheus metrics endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
searxng = {
hostname = "searxng"
service = "searxng"
source = "apps/searxng/docker-compose.yml"
purpose = "SearXNG search endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
traefik = {
hostname = "traefik"
service = "traefik"
source = "core/traefik/docker-compose.yml"
purpose = "Traefik dashboard/API endpoint"
record_type = null
ttl = null
target = null
proxied = null
}
}
dynu_dns_records_catalog = {
for key, record in local.dynu_dns_records_catalog_base :
key => merge(record, {
fqdn = format("%s.%s", record.hostname, local.dynu_domain)
})
}
}
@@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""Generate Terraform dynu_dns_record resources/import commands from Dynu inventory outputs."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve()
TF_ROOT = SCRIPT_PATH.parents[1]
GENERATED_DIR = TF_ROOT / "generated"
TF_FILE = GENERATED_DIR / "dynu_dns_records.generated.tf"
IMPORT_SCRIPT = GENERATED_DIR / "import-dynu-dns-records.sh"
INVENTORY_FILE = GENERATED_DIR / "dynu_dns_records_inventory.json"
DEFAULT_RECORDS_OUTPUT = "dynu_dns_records"
REQUIRED_RECORD_FIELDS = ("id", "domain_id", "hostname", "record_type")
HEADER_TF = """# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Generated from Dynu brownfield DNS inventory.
# Do not blindly apply this file to production DNS.
# Import records into Terraform state before allowing Terraform to manage them.
# ---------------------------------------------------------------------------
"""
HEADER_SH = """#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# GENERATED FILE - REVIEW BEFORE USE
#
# Imports existing Dynu DNS records into Terraform state.
# Does not apply changes.
# ---------------------------------------------------------------------------
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${TF_ROOT}"
# Re-running imports will fail for resources already in state.
# This script skips imports when state already contains the resource address.
"""
OPTIONAL_FIELDS = ["group", "host", "priority", "weight", "port", "flags", "tag", "value", "node_name"]
def run_terraform_output() -> dict:
if not (TF_ROOT / ".terraform").exists():
raise RuntimeError("Terraform is not initialized in infrastructure/terraform/dynu. Run: terraform init")
cmd = ["terraform", "output", "-json"]
proc = subprocess.run(cmd, cwd=TF_ROOT, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"Failed to run {' '.join(cmd)}:\n{proc.stderr.strip()}")
return json.loads(proc.stdout)
def type_shape_name(value: object) -> str:
if isinstance(value, list):
return "list"
if isinstance(value, dict):
return "object"
return type(value).__name__
def extract_records(payload: object, output_name: str) -> list[dict]:
source = payload
if isinstance(payload, list):
source = payload
elif isinstance(payload, dict):
if isinstance(payload.get("value"), list):
source = payload["value"]
elif isinstance(payload.get("records"), list):
source = payload["records"]
elif isinstance(payload.get("value"), dict) and isinstance(payload["value"].get("records"), list):
source = payload["value"]["records"]
elif output_name in payload and isinstance(payload[output_name], dict):
output_wrapper = payload[output_name]
if isinstance(output_wrapper.get("value"), list):
source = output_wrapper["value"]
elif isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
source = output_wrapper["value"]["records"]
elif isinstance(output_wrapper.get("records"), list):
source = output_wrapper["records"]
else:
raise RuntimeError(f"Output '{output_name}' does not contain a records list.")
else:
raise RuntimeError(f"Output '{output_name}' not found and no records list discovered.")
else:
raise RuntimeError(f"Unsupported JSON payload type: {type(payload).__name__}")
if not isinstance(source, list):
raise RuntimeError(f"Output '{output_name}' did not resolve to a list of records.")
return source
def validate_records(records: list[dict], output_name: str) -> None:
for i, record in enumerate(records):
if not isinstance(record, dict):
raise RuntimeError(f"Selected output '{output_name}' has non-object record at index {i}: {type(record).__name__}.")
missing = [field for field in REQUIRED_RECORD_FIELDS if field not in record]
if missing:
missing_text = ", ".join(missing)
raise RuntimeError(
f"Selected output '{output_name}' contains records, but they are not importable Dynu provider records. "
f"Record #{i} is missing required fields: {missing_text}. "
"Choose an output sourced from data.dynu_dns_records.root, such as dynu_dns_records or dynu_dns_inventory."
)
def describe_output(output_name: str, output_wrapper: object, full_outputs: dict) -> dict:
details = {
"name": output_name,
"usable": False,
"shape": type_shape_name(output_wrapper),
"record_count": "none",
"error": "no records list found",
}
if isinstance(output_wrapper, dict) and "value" in output_wrapper:
details["shape"] = type_shape_name(output_wrapper.get("value"))
try:
records = extract_records(full_outputs, output_name)
except RuntimeError as exc:
details["error"] = str(exc)
return details
details["record_count"] = len(records)
try:
validate_records(records, output_name)
except RuntimeError as exc:
details["error"] = str(exc)
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
details["shape"] = "object with records list"
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
details["shape"] = "list"
return details
details["usable"] = True
details["error"] = None
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
details["shape"] = "object with records list"
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
details["shape"] = "list"
return details
def choose_output_interactively(outputs: dict, descriptions: list[dict]) -> str | None:
print("\nAvailable Terraform outputs:\n")
indexed = {str(i): item for i, item in enumerate(descriptions, 1)}
by_name = {item["name"]: item for item in descriptions}
for i, item in enumerate(descriptions, 1):
print(f" {i}) {item['name']}")
print(f" usable: {'yes' if item['usable'] else 'no'}")
print(f" shape: {item['shape']}")
print(f" record count: {item['record_count']}")
if item["error"]:
print(f" reason: {item['error']}")
print()
attempts = 0
while attempts < 3:
attempts += 1
try:
selection = input(f"Choose an output to use for DNS records [1-{len(descriptions)}], or press Enter to cancel: ").strip()
except KeyboardInterrupt:
print("\nSelection cancelled.")
return None
if selection == "":
print("Selection cancelled.")
return None
candidate = indexed.get(selection) or by_name.get(selection)
if candidate is None:
print("Invalid selection. Enter a number from the list or an exact output name.")
continue
if not candidate["usable"]:
print(f"Output '{candidate['name']}' is not usable: {candidate['error']}")
continue
return candidate["name"]
raise RuntimeError("Too many invalid selections. Exiting without writing files.")
def tf_name(record: dict) -> str:
base = f"{record.get('hostname', '')}_{record.get('record_type', '')}_{record.get('id', '')}".lower()
base = base.replace("*", "wildcard")
base = re.sub(r"[^a-z0-9_]+", "_", base)
base = re.sub(r"_+", "_", base).strip("_")
if not base or not re.match(r"^[a-z]", base):
base = f"record_{base}" if base else "record"
if not base.endswith(str(record.get("id", ""))):
base = f"{base}_{record.get('id', '')}"
return base
def hcl_value(value):
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
return json.dumps(value)
def generate_resources(records: list[dict]) -> str:
chunks = [HEADER_TF.rstrip(), ""]
for rec in records:
name = tf_name(rec)
lines = [f'resource "dynu_dns_record" "{name}" {{']
lines.append(f" hostname = {hcl_value(rec.get('hostname'))}")
lines.append(f" record_type = {hcl_value(rec.get('record_type'))}")
if rec.get("ttl") is not None:
lines.append(f" ttl = {hcl_value(rec.get('ttl'))}")
enabled = rec.get("enabled")
if enabled is None:
enabled = rec.get("state")
if enabled is not None:
lines.append(f" enabled = {hcl_value(enabled)}")
content = rec.get("content")
rtype = str(rec.get("record_type", "")).upper()
if content in (None, "") and rtype in {"A", "AAAA"}:
lines.append(" dynamic = true")
elif content not in (None, ""):
lines.append(f" content = {hcl_value(content)}")
for field in OPTIONAL_FIELDS:
value = rec.get(field)
if value not in (None, ""):
lines.append(f" {field.ljust(11)}= {hcl_value(value)}")
lines.extend([
"",
" lifecycle {",
" prevent_destroy = true",
" }",
"}",
"",
])
chunks.extend(lines)
return "\n".join(chunks).rstrip() + "\n"
def generate_import_script(records: list[dict]) -> str:
lines = [HEADER_SH.rstrip(), ""]
for rec in records:
name = tf_name(rec)
import_id = f"{rec['domain_id']}/{rec['id']}"
addr = f"dynu_dns_record.{name}"
lines.append(f"if terraform state show '{addr}' >/dev/null 2>&1; then")
lines.append(f" echo 'Skipping already imported: {addr}'")
lines.append("else")
lines.append(f" terraform import '{addr}' '{import_id}'")
lines.append("fi")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def write_file(path: Path, content: str, dry_run: bool, overwrite: bool) -> None:
if path.exists() and not overwrite:
raise RuntimeError(f"Refusing to overwrite existing file: {path}. Re-run with --overwrite.")
if dry_run:
print(f"[dry-run] Would write {path}")
return
path.write_text(content, encoding="utf-8")
print(f"Wrote {path}")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", help="Print intended output paths without writing files.")
parser.add_argument("--overwrite", "--force", action="store_true", dest="overwrite", help="Overwrite existing generated files.")
parser.add_argument("--from-file", type=Path, help="Load inventory JSON from a file instead of calling terraform output.")
parser.add_argument(
"--records-output",
default=None,
help=(
"Terraform output name containing Dynu DNS records. "
f"Defaults to {DEFAULT_RECORDS_OUTPUT}; if missing in an interactive terminal, "
"the script prompts you to choose from available outputs."
),
)
parser.add_argument("--no-interactive", action="store_true", help="Disable interactive output selection.")
args = parser.parse_args()
records_output_explicit = args.records_output is not None
records_output = args.records_output or DEFAULT_RECORDS_OUTPUT
try:
payload = json.loads(args.from_file.read_text(encoding="utf-8")) if args.from_file else run_terraform_output()
selected_output = records_output
descriptions: list[dict] = []
if isinstance(payload, dict):
descriptions = [describe_output(name, payload[name], payload) for name in sorted(payload)]
try:
records = extract_records(payload, selected_output)
validate_records(records, selected_output)
except RuntimeError as exc:
is_interactive = sys.stdin.isatty() and not args.no_interactive
should_prompt = isinstance(payload, dict) and not records_output_explicit and is_interactive
if should_prompt:
print(f"Terraform output '{selected_output}' was not found or is unusable.\n")
chosen = choose_output_interactively(payload, descriptions)
if chosen is None:
print("Exiting without writing files.")
return 1
selected_output = chosen
records = extract_records(payload, selected_output)
validate_records(records, selected_output)
else:
if isinstance(payload, dict):
available = ", ".join(sorted(payload.keys())) or "(none)"
if records_output_explicit:
raise RuntimeError(
f"Missing or unusable Terraform output '{selected_output}'. "
f"Available outputs: {available}. Details: {exc}"
)
raise RuntimeError(
f"Missing or unusable Terraform output '{selected_output}'. "
f"Available outputs: {available}.\n\n"
"Run interactively to choose an output, or pass one explicitly, for example:\n\n"
" python3 scripts/generate-brownfield-records.py --records-output dynu_dns_inventory --dry-run"
)
raise
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
write_file(INVENTORY_FILE, json.dumps(records, indent=2, sort_keys=True) + "\n", args.dry_run, args.overwrite)
write_file(TF_FILE, generate_resources(records), args.dry_run, args.overwrite)
write_file(IMPORT_SCRIPT, generate_import_script(records), args.dry_run, args.overwrite)
if not args.dry_run:
IMPORT_SCRIPT.chmod(0o755)
return 0
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+7
View File
@@ -0,0 +1,7 @@
# Local-only credentials. Do not commit real values.
dynu_api_key = "replace-with-dynu-api-key"
dynu_username = null
dynu_password = null
dynu_root_domain = "lan.ddnsgeek.com"
dynu_record_import_id = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
+33
View File
@@ -0,0 +1,33 @@
variable "dynu_root_domain" {
description = "Dynu root domain name to reconcile/import (for example: lan.ddnsgeek.com)."
type = string
default = "lan.ddnsgeek.com"
}
variable "dynu_api_key" {
description = "Dynu API key/token used by the Dynu Terraform provider."
type = string
sensitive = true
default = null
}
variable "dynu_username" {
description = "Optional Dynu username, only if required by the provider."
type = string
sensitive = true
default = null
}
variable "dynu_password" {
description = "Optional Dynu password, only if required by the provider."
type = string
sensitive = true
default = null
}
variable "dynu_record_import_id" {
description = "Placeholder import ID for a single dynu_dns_record during one-at-a-time reconciliation."
type = string
default = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
}
+9
View File
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.6.0"
required_providers {
dynu = {
source = "beatz174-bit/dynu"
}
}
}