added caddy provider support

This commit is contained in:
nick committed 2026-07-01 17:14:50 -07:00
1 parent 91e80ae6f8
commit 408b5d8652
6 files changed
+192 -3

No files matched your search

+77 -3
View File
@@ -2,6 +2,18 @@
- name: Set up caddy LXC - name: Set up caddy LXC
hosts: caddy hosts: caddy
become: yes become: yes
vars:
# Where the Caddy admin API listens. OpenTofu manages reverse-proxy routes
# through this endpoint (caddy_host / caddy_ssh_host in terraform.tfvars).
caddy_admin_listen: "0.0.0.0:2019"
handlers:
- name: restart caddy
ansible.builtin.systemd:
name: caddy
state: restarted
daemon_reload: yes
tasks: tasks:
- name: default_packages - name: default_packages
ansible.builtin.include_tasks: ../../templates/default_packages.yml ansible.builtin.include_tasks: ../../templates/default_packages.yml
@@ -12,9 +24,52 @@
username: nick username: nick
github_user: Chewbaccalakis github_user: Chewbaccalakis
# Config is deployed BEFORE the caddy package is installed: dpkg keeps a
# pre-existing /etc/caddy/Caddyfile instead of the package default (which
# serves a static page on :80 as server "srv0" — that would get autosaved
# and collide with the OpenTofu-managed :80 server forever after).
- name: configure caddy
block:
- name: create /etc/caddy
ansible.builtin.file:
path: /etc/caddy
state: directory
mode: '0755'
- name: deploy Caddyfile (seeds the admin API listener)
ansible.builtin.template:
src: templates/Caddyfile.j2
dest: /etc/caddy/Caddyfile
owner: root
group: root
mode: '0644'
notify: restart caddy
- name: create caddy systemd override directory
ansible.builtin.file:
path: /etc/systemd/system/caddy.service.d
state: directory
mode: '0755'
- name: override caddy service to resume OpenTofu-applied config
ansible.builtin.copy:
dest: /etc/systemd/system/caddy.service.d/resume.conf
content: |
# Managed by Ansible. --resume makes Caddy prefer its autosaved
# JSON config (written whenever OpenTofu applies changes via the
# admin API) over the Caddyfile, so managed routes survive
# restarts. The Caddyfile only seeds the initial config; to
# re-seed from it, delete
# /var/lib/caddy/.config/caddy/autosave.json and restart caddy.
[Service]
ExecStart=
ExecStart=/usr/bin/caddy run --environ --resume --config /etc/caddy/Caddyfile --adapter caddyfile
mode: '0644'
notify: restart caddy
- name: install caddy - name: install caddy
block: block:
- name: install xcaddy prerequisites - name: install apt prerequisites
ansible.builtin.apt: ansible.builtin.apt:
name: name:
- debian-keyring - debian-keyring
@@ -22,6 +77,16 @@
- apt-transport-https - apt-transport-https
state: present state: present
- name: download caddy gpg key
ansible.builtin.shell:
cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
- name: add caddy apt repository
ansible.builtin.shell:
cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
creates: /etc/apt/sources.list.d/caddy-stable.list
- name: download xcaddy gpg key - name: download xcaddy gpg key
ansible.builtin.shell: ansible.builtin.shell:
cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
@@ -32,8 +97,17 @@
cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-xcaddy.list cmd: curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-xcaddy.list
creates: /etc/apt/sources.list.d/caddy-xcaddy.list creates: /etc/apt/sources.list.d/caddy-xcaddy.list
- name: install xcaddy - name: install caddy and xcaddy
ansible.builtin.apt: ansible.builtin.apt:
name: xcaddy name:
- caddy
- xcaddy
state: present state: present
update_cache: yes update_cache: yes
- name: enable and start caddy
ansible.builtin.systemd:
name: caddy
enabled: yes
state: started
daemon_reload: yes
@@ -0,0 +1,14 @@
# {{ ansible_managed }}
#
# This file only seeds Caddy's initial configuration: the admin API listener
# that OpenTofu talks to. Reverse-proxy routes are managed by the caddy module
# via the admin API (caddy_proxies in terraform.tfvars); Caddy autosaves those
# changes and the systemd unit starts with --resume, so API-applied config
# survives restarts and this file is ignored once an autosave exists.
#
# To re-seed from this file (e.g. after changing the admin listener), delete
# /var/lib/caddy/.config/caddy/autosave.json and restart caddy.
{
admin {{ caddy_admin_listen }}
}
}
+12
View File
@@ -17,6 +17,7 @@ locals {
technitium_enabled = try(data.external.infisical.result["technitium_api_token"], "") != "" technitium_enabled = try(data.external.infisical.result["technitium_api_token"], "") != ""
ipam_enabled = try(data.external.infisical.result["ipam_username"], "") != "" ipam_enabled = try(data.external.infisical.result["ipam_username"], "") != ""
caddy_enabled = var.caddy_host != ""
# Shared inputs passed to every proxmox_node module call. # Shared inputs passed to every proxmox_node module call.
node_common = { node_common = {
@@ -102,6 +103,17 @@ module "technitium" {
records = var.dns_records records = var.dns_records
} }
module "caddy" {
source = "./opentofu/modules/caddy"
enabled = local.caddy_enabled
proxies = var.caddy_proxies
providers = {
caddy = caddy
}
}
resource "local_file" "ansible_inventory" { resource "local_file" "ansible_inventory" {
filename = "${path.module}/${var.ansible_inventory_path}" filename = "${path.module}/${var.ansible_inventory_path}"
content = templatefile("${path.module}/opentofu/inventory.tpl", { content = templatefile("${path.module}/opentofu/inventory.tpl", {
+24
View File
@@ -12,6 +12,10 @@ terraform {
source = "kenske/technitium" source = "kenske/technitium"
version = "~> 0.2" version = "~> 0.2"
} }
caddy = {
source = "conradludgate/caddy"
version = "~> 0.2"
}
# phpipam = { # phpipam = {
# source = "lord-kyron/phpipam" # source = "lord-kyron/phpipam"
# version = "1.6.2" # version = "1.6.2"
@@ -27,6 +31,26 @@ provider "technitium" {
token = coalesce(try(data.external.infisical.result["technitium_api_token"], ""), "token-not-yet-provisioned") token = coalesce(try(data.external.infisical.result["technitium_api_token"], ""), "token-not-yet-provisioned")
} }
provider "caddy" {
# Endpoint as seen from wherever the request lands: with the SSH tunnel below
# this is the caddy host's own loopback admin API; without it, a directly
# reachable admin endpoint. Falls back to the Caddy default so the provider
# configures cleanly before caddy_host is set (no resources use it until then).
host = coalesce(var.caddy_host, "http://localhost:2019")
# Tunnel to the admin API over SSH when it only listens on loopback (the
# default, recommended setup). Reuses the ansible key generated above. Leave
# caddy_ssh_host empty to talk to caddy_host directly.
dynamic "ssh" {
for_each = var.caddy_ssh_host != "" ? [1] : []
content {
host = var.caddy_ssh_host
key_file = local_sensitive_file.ssh_private_key.filename
host_key = var.caddy_ssh_host_key
}
}
}
data "external" "infisical" { data "external" "infisical" {
program = ["bash", "-c", "infisical export --template=./secrets.tmpl"] program = ["bash", "-c", "infisical export --template=./secrets.tmpl"]
} }
+36
View File
@@ -40,8 +40,44 @@ dns_records = [
{ zone = "oswaldtheotter.lan", name = "proxmox", type = "A", value = "192.168.0.240" }, { zone = "oswaldtheotter.lan", name = "proxmox", type = "A", value = "192.168.0.240" },
{ zone = "oswaldtheotter.lan", name = "nas", type = "CNAME", value = "portanas.oswaldtheotter.lan" }, { zone = "oswaldtheotter.lan", name = "nas", type = "CNAME", value = "portanas.oswaldtheotter.lan" },
{ zone = "oswaldtheotter.lan", name = "s3", type = "CNAME", value="portanas.oswaldtheotter.lan"}, { zone = "oswaldtheotter.lan", name = "s3", type = "CNAME", value="portanas.oswaldtheotter.lan"},
{ zone = "oswaldtheotter.lan", name = "hass", type = "A", value="192.168.0.70"},
{ zone = "oswaldtheotter.lan", name = "homeassistant", type = "CNAME", value="hass.oswaldtheotter.lan"},
{ zone = "oswaldtheotter.lan", name = "bookstack", type = "A", value="192.168.0.5"},
] ]
# ── Reverse proxy (Caddy) ───────────────────────────────────────────────────────
# Managed on the caddy LXC (192.168.0.5). The admin API listens on loopback, so
# reach it by tunneling SSH to the box as the ansible user. Clear caddy_host to
# disable Caddy management entirely.
caddy_host = "http://localhost:2019"
caddy_ssh_host = "[email protected]:22"
# From: ssh-keyscan 192.168.0.5 — must be refreshed if the LXC is rebuilt.
# Must be the ECDSA key: the provider's (old) Go SSH client prefers
# ecdsa-sha2-nistp256 during negotiation and pins the exact key it receives.
caddy_ssh_host_key = "192.168.0.5 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLjluBg9JCITeqG4NPad4VNTkAcZDY3y9FU9CP5BAuKIr3TDhTFYzuAdNnROCEhW1ddJI9ZFKI5y/3xqNTmkHbA="
# Each proxy serves plain HTTP on :80 by default; set protocol = "https" to
# serve it on :443 instead. Note: .lan hostnames can't get public certificates,
# so HTTPS sites use Caddy's internal CA — trust its root cert on your devices
# to avoid browser warnings.
caddy_proxies = {
# dns = {
# host = "dns.oswaldtheotter.lan"
# upstreams = ["192.168.0.2:5380"]
# }
# secure-example = {
# host = "secure.oswaldtheotter.lan"
# upstreams = ["192.168.0.202:80"]
# protocol = "https"
# }
bookstack = {
host = "bookstack.oswaldtheotter.lan"
upstreams = ["192.168.0.4:30214"]
}
}
# ── Node definitions ─────────────────────────────────────────────────────────── # ── Node definitions ───────────────────────────────────────────────────────────
nodes = { nodes = {
+29
View File
@@ -62,6 +62,35 @@ variable "dns_records" {
default = [] default = []
} }
variable "caddy_host" {
description = "Caddy admin API endpoint. When using the SSH tunnel this is the endpoint as seen from the caddy host (e.g. http://localhost:2019). Leave empty to disable Caddy management."
type = string
default = ""
}
variable "caddy_ssh_host" {
description = "SSH target (user@host:port) used to tunnel to the Caddy admin API. Empty to reach caddy_host directly."
type = string
default = ""
}
variable "caddy_ssh_host_key" {
description = "SSH host key of the caddy host, known_hosts format (get it with: ssh-keyscan <ip>). Required when caddy_ssh_host is set."
type = string
default = ""
}
variable "caddy_proxies" {
description = "Reverse-proxy sites keyed by name. Each maps a hostname to one or more upstream backends (Caddy dial targets, e.g. \"192.168.0.40:32400\"). protocol picks plain HTTP on :80 (default) or HTTPS on :443 with automatic certificates. Set path to restrict the route to specific request paths."
type = map(object({
host = string
upstreams = list(string)
protocol = optional(string, "http")
path = optional(list(string))
}))
default = {}
}
variable "ansible_inventory_path" { variable "ansible_inventory_path" {
description = "Path where the generated Ansible inventory file is written. Defaults to ../ansible/inventories/generated.yml relative to this module, which points to the parent repo when used as a git submodule." description = "Path where the generated Ansible inventory file is written. Defaults to ../ansible/inventories/generated.yml relative to this module, which points to the parent repo when used as a git submodule."
type = string type = string