diff --git a/config/config.libsonnet b/config/config.libsonnet deleted file mode 100644 index d563532..0000000 --- a/config/config.libsonnet +++ /dev/null @@ -1,5 +0,0 @@ -local credentials = import "../credentials.libsonnet"; -local servers = import "servers.libsonnet"; -local networking = import "networking.libsonnet"; - -credentials + servers + networking diff --git a/config/defaults.libsonnet b/config/defaults.libsonnet new file mode 100644 index 0000000..5d4fe4d --- /dev/null +++ b/config/defaults.libsonnet @@ -0,0 +1,10 @@ +{ + infraDomain: "m5w.de", + # needs to be /96 + ipSubnet: "fdc2:d459:3f8a:84a3:coffe:coffe", + defaultTTL: 3600, + defaultZoneTTL: 86400, + defaultSshKeys: { + martin: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzXsN8jgzF51mQS5gfo4H7QKNhDKDEyXZSGen83MYw9GyIMi+AdH1fuhnYBlN2fTlHjs88otZkBMhVzE5lbkutz07j+ZpF6AdUvxqesqkXa2hdXFBRRwnG7u0Pxbi7vhr7uUWMa1WzJYynwmYBLL0yNEK6dI1qJcpwaK6v8UOZymiSJh04Sqd1LfEKd7R3BdzRCqkeKab1351OmJSswN+HRsAsDbdOIDBXpUMomvYAxJud4Wv90NcXfYikI7lhaAILBPTSUQqgTFFHhjfw9pe6Uhxb5URVS5ENjYVDyD2Lo1daZwy+sSYvA1LKZLQVEBKyx1o6SLLsuYqOuOIxiy8UEQ9vLHBdYQ+Ca0m2TruPtxEIu67WQFMBjMXcja4p516UkiuFqr0sQftI0HvVIZHS95DTK2BygkOy9Aok/fQ4IBeraN9EjIRkAB5Hn0z8vxBQMf9ZKUisMbN8nk22YpGte1RD9BFS9Swm7IE1c55QD30S6tD5z0lMUcU+ol3rOIh/013hNj9ZLsYxOtGJtIX3Xc+tIbUgXKou1sjPGQx4M2t9RRZTJ8L4l2DYw4joNoFXGiwFW586DBMw6wb9YeikA+Nuy0RFY8ytgBD5Qdh7IbF7+aA8f0ZkGHkmf/VLM1UkO5XXh3bNlz03IPcav091mAAlu/OHCdOhN54V9vE1FQ== cardno:4268913' + }, +} diff --git a/config/domains.libsonnet b/config/domains.libsonnet new file mode 100644 index 0000000..320ded3 --- /dev/null +++ b/config/domains.libsonnet @@ -0,0 +1,3 @@ +[ + 'example.com' +] diff --git a/config/networking.libsonnet b/config/networking.libsonnet deleted file mode 100644 index 9fedc65..0000000 --- a/config/networking.libsonnet +++ /dev/null @@ -1,7 +0,0 @@ -{ - infraDomain: "m5w.de", - # needs to be /96 - ipSubnet: "fdc2:d459:3f8a:84a3:coffe:coffe", - defaultTTL: 3600, - defaultZoneTTL: 86400, -} diff --git a/config/servers.libsonnet b/config/servers.libsonnet index 0d573dd..b02b72a 100644 --- a/config/servers.libsonnet +++ b/config/servers.libsonnet @@ -1,24 +1,16 @@ +local deployments = import "../lib/deployments.libsonnet"; local terraform = import "../lib/terraform.libsonnet"; -local networking = import "networking.libsonnet"; +local server = import "../lib/servers.libsonnet"; -local hashIp(name) = std.substr(std.md5(name), 0, 4) + ":" + std.substr(std.md5(name), 4, 4); -local serverMeta(name, instance) = { - [name]: { - name: name, - publicSubdomain: name + ".infra", - internalSubdomain: name + ".i.infra", - publicDomain: self.publicSubdomain + "." + networking.infraDomain, - internalDomain: self.internalSubdomain + "." + networking.infraDomain, - wireguardIp: networking.ipSubnet + ":" + hashIp(name), - instance: instance + { name: name } - } -}; - -{ - servers: serverMeta("dust2", terraform.HcloudInstance { +server.meta( + "dust2", + terraform.HcloudInstance { server_type: "cx11" - }), - sshKeys: { - martin: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzXsN8jgzF51mQS5gfo4H7QKNhDKDEyXZSGen83MYw9GyIMi+AdH1fuhnYBlN2fTlHjs88otZkBMhVzE5lbkutz07j+ZpF6AdUvxqesqkXa2hdXFBRRwnG7u0Pxbi7vhr7uUWMa1WzJYynwmYBLL0yNEK6dI1qJcpwaK6v8UOZymiSJh04Sqd1LfEKd7R3BdzRCqkeKab1351OmJSswN+HRsAsDbdOIDBXpUMomvYAxJud4Wv90NcXfYikI7lhaAILBPTSUQqgTFFHhjfw9pe6Uhxb5URVS5ENjYVDyD2Lo1daZwy+sSYvA1LKZLQVEBKyx1o6SLLsuYqOuOIxiy8UEQ9vLHBdYQ+Ca0m2TruPtxEIu67WQFMBjMXcja4p516UkiuFqr0sQftI0HvVIZHS95DTK2BygkOy9Aok/fQ4IBeraN9EjIRkAB5Hn0z8vxBQMf9ZKUisMbN8nk22YpGte1RD9BFS9Swm7IE1c55QD30S6tD5z0lMUcU+ol3rOIh/013hNj9ZLsYxOtGJtIX3Xc+tIbUgXKou1sjPGQx4M2t9RRZTJ8L4l2DYw4joNoFXGiwFW586DBMw6wb9YeikA+Nuy0RFY8ytgBD5Qdh7IbF7+aA8f0ZkGHkmf/VLM1UkO5XXh3bNlz03IPcav091mAAlu/OHCdOhN54V9vE1FQ== cardno:4268913' }, -} + [ + deployments.dockerComposeApp('promstack'), + deployments.dockerComposeApp('mailcow', 'mail.example.com'), + deployments.laravelApp('test', 'www.example.com'), + deployments.laravelApp('app', 'example.com'), + ] +) diff --git a/lib/deployments.libsonnet b/lib/deployments.libsonnet new file mode 100644 index 0000000..9d95925 --- /dev/null +++ b/lib/deployments.libsonnet @@ -0,0 +1,22 @@ +{ + Deployment:: { + roles: error "At least one role is required", + variables: error "Deployment variables missing" + }, + + dockerComposeApp: function (name, domain = null) self.Deployment { + roles: ['reverse-proxy', 'docker', 'docker-compose-app'], + variables: { + docker_compose_app: [ name ], + domains: [ domain ] + }, + }, + + laravelApp: function (name, domain) self.Deployment { + roles: ['reverse-proxy', 'laravel-app'], + variables: { + laravel_apps: [ { name: name, domain: domain } ] , + domains: [ domain ] + } + } +} diff --git a/lib/servers.libsonnet b/lib/servers.libsonnet new file mode 100644 index 0000000..a2d671e --- /dev/null +++ b/lib/servers.libsonnet @@ -0,0 +1,21 @@ +local defaults = import "../config/defaults.libsonnet"; +local utils = import "../lib/utils.libsonnet"; + +{ + hashIp: function (name) std.substr(std.md5(name), 0, 4) + ":" + std.substr(std.md5(name), 4, 4), + meta: function (name, instance, deployments) { + [name]: { + name: name, + networking: { + publicSubdomain: name + ".infra", + internalSubdomain: name + ".i.infra", + publicDomain: self.publicSubdomain + "." + defaults.infraDomain, + internalDomain: self.internalSubdomain + "." + defaults.infraDomain, + wireguardIp: defaults.ipSubnet + ":" + $.hashIp(name), + }, + instance: instance + { name: name }, + roles: std.uniq(std.sort(std.foldl(function (roles, deployment) roles + deployment.roles, deployments, []))), + deployment_vars: std.foldl(function (vars, deployment) utils.merge(vars, deployment.variables), deployments, {}) + } + } +} diff --git a/lib/terraform.libsonnet b/lib/terraform.libsonnet index 1d51d61..048605f 100644 --- a/lib/terraform.libsonnet +++ b/lib/terraform.libsonnet @@ -1,7 +1,6 @@ -local config = import "../config/config.libsonnet"; +local defaults = import "../config/defaults.libsonnet"; { - local terraform = self, local rname (server, suffix) = "host_" + server.name + "_" + suffix, HcloudInstance:: { @@ -13,8 +12,7 @@ local config = import "../config/config.libsonnet"; HcloudSSHKey:: { name: error "Key must have field: name", - public_key: error "Key must have field: public_key", - labels: { source: "terraform" } + public_key: error "Key must have field: public_key" }, HdnsRecord:: { @@ -22,14 +20,14 @@ local config = import "../config/config.libsonnet"; name: error "Record must have field: name", value: error "Record must have field: value", type: error "Record must have field: type", - ttl: config.defaultTTL + ttl: defaults.defaultTTL }, serverDnsRecords: function (s) { local attr (s, n) = "${hcloud_server." + s.name + "." + n + "}", - [rname(s, "A")]: terraform.HdnsRecord{ name: s.publicSubdomain, value: attr(s, "ipv4_address"), type: "A" }, - [rname(s, "AAAA")]: terraform.HdnsRecord{ name: s.publicSubdomain, value: attr(s, "ipv6_address"), type: "AAAA" }, - [rname(s, "VPN")]: terraform.HdnsRecord{ name: s.internalSubdomain, value: s.wireguardIp, type: "AAAA" }, + [rname(s, "A")]: $.HdnsRecord{ name: s.networking.publicSubdomain, value: attr(s, "ipv4_address"), type: "A" }, + [rname(s, "AAAA")]: $.HdnsRecord{ name: s.networking.publicSubdomain, value: attr(s, "ipv6_address"), type: "AAAA" }, + [rname(s, "VPN")]: $.HdnsRecord{ name: s.networking.internalSubdomain, value: s.networking.wireguardIp, type: "AAAA" }, } } diff --git a/lib/utils.libsonnet b/lib/utils.libsonnet new file mode 100644 index 0000000..caa3391 --- /dev/null +++ b/lib/utils.libsonnet @@ -0,0 +1,32 @@ +{ + # adopted from stdlib source to handle array merges + # https://github.com/google/jsonnet/blob/4e67da2c015bb316158d3e52a47376b38a29a4ef/stdlib/std.jsonnet#L1473 + merge (target, patch):: + if std.isObject(patch) then + local target_object = + if std.isObject(target) then target else {}; + + local target_fields = + if std.isObject(target_object) then std.objectFields(target_object) else []; + + local null_fields = [k for k in std.objectFields(patch) if patch[k] == null]; + local both_fields = std.setUnion(target_fields, std.objectFields(patch)); + + { + [k]: + if !std.objectHas(patch, k) then + target_object[k] + else if !std.objectHas(target_object, k) then + $.merge(null, patch[k]) + else + $.merge(target_object[k], patch[k]) + for k in std.setDiff(both_fields, null_fields) + } + else if std.isArray(patch) then + if std.isArray(target) && target != [null] then + target + patch + else + patch + else + patch +} diff --git a/services.jsonnet b/services.jsonnet index 6afdb55..7942a5a 100644 --- a/services.jsonnet +++ b/services.jsonnet @@ -1,68 +1,5 @@ -local terraform = import "lib/terraform.libsonnet"; -local config = import "config/config.libsonnet"; - { - "terraform/terraform.tf.json": std.manifestJson({ - terraform: { - required_providers: { - hcloud: { - source: "hetznercloud/hcloud", - version: "1.30.0" - }, - hetznerdns: { - source: "timohirt/hetznerdns", - version: "1.1.1" - } - } - }, - - provider: { - hcloud: { - token: config.hcloudToken - }, - hetznerdns: { - apitoken: config.hdnsToken - } - }, - - resource: { - hcloud_ssh_key: { - [k]: terraform.HcloudSSHKey { name: k, public_key: config.sshKeys[k] } - for k in std.objectFields(config.sshKeys) - }, - hcloud_server: { - [s]: config.servers[s].instance - for s in std.objectFields(config.servers) - }, - hetznerdns_zone: { - infra: { name: config.infraDomain, ttl: config.defaultZoneTTL }, - }, - hetznerdns_record: std.foldl(function (a, b) a + b, [ - terraform.serverDnsRecords(config.servers[s]) - for s in std.objectFields(config.servers) - ], {}) - } - }), - "ansible/inventory.yaml": std.manifestYamlDoc({ - all: { - hosts: { - [s]: config.servers[s] + { - ansible_host: config.servers[s].publicDomain, - ansible_user: "root" - } - for s in std.objectFields(config.servers) - } - } - }), - "ansible/site.yaml": std.manifestYamlDoc([ - { - name: "Test command", - hosts: "all", - tasks: [ - { - "ansible.builtin.command": "ls" - } - ] - } - ]) + "terraform/terraform.tf.json": std.manifestJson(import "services/terraform.libsonnet"), + "ansible/inventory.yaml": std.manifestYamlDoc(import "services/inventory.libsonnet"), + "ansible/site.yaml": std.manifestYamlDoc(import "services/playbook.libsonnet") } diff --git a/services/inventory.libsonnet b/services/inventory.libsonnet new file mode 100644 index 0000000..8d7aecb --- /dev/null +++ b/services/inventory.libsonnet @@ -0,0 +1,13 @@ +local servers = import "../config/servers.libsonnet"; + +{ + all: { + hosts: { + [s]: servers[s] + { + ansible_host: servers[s].networking.publicDomain, + ansible_user: "root" + } + for s in std.objectFields(servers) + } + } +} diff --git a/services/playbook.libsonnet b/services/playbook.libsonnet new file mode 100644 index 0000000..df338c4 --- /dev/null +++ b/services/playbook.libsonnet @@ -0,0 +1,11 @@ +[ + { + name: "Test command", + hosts: "all", + tasks: [ + { + "ansible.builtin.command": "ls" + } + ] + } +] diff --git a/services/terraform.libsonnet b/services/terraform.libsonnet new file mode 100644 index 0000000..cf85cfd --- /dev/null +++ b/services/terraform.libsonnet @@ -0,0 +1,85 @@ +local domains = import "../config/domains.libsonnet"; +local servers = import "../config/servers.libsonnet"; +local credentials = import "../credentials.libsonnet"; +local terraform = import "../lib/terraform.libsonnet"; +local defaults = import "../config/defaults.libsonnet"; + +local domainToName (domain) = std.strReplace(domain, '.', '_'); +local splitDomainName (domain, subdomaine = []) = + local found = std.find(domain, domains); + local split = std.split(domain, '.'); + if std.length(found) > 0 + then { subdomain: std.join('.', subdomaine), zone: domainToName(domains[found[0]]) } + else splitDomainName(std.join('.', split[1:]), subdomaine + [split[0]]); + +local domainEntries(domain, server) = + local split = splitDomainName(domain); + local record = terraform.HdnsRecord { zone_id: "${hetznerdns_zone." + split.zone + ".id}", name: "" }; + local hostAttr (attr) = "${hcloud_server." + server.name + "." + attr + "}"; + if split.subdomain == "" + then { + ["deployment_" + domainToName(domain) + "_A"]: record { value: hostAttr('ipv4_address'), type: "A" }, + ["deployment_" + domainToName(domain) + "_AAAA"]: record { value: hostAttr('ipv6_address'), type: "AAAA" } + } + else { ["deployment_" + domainToName(domain) + "_CNAME"]: record { name: split.subdomain, value: server.networking.publicDomain, type: 'CNAME' } }; + +{ + terraform: { + required_providers: { + hcloud: { + source: "hetznercloud/hcloud", + version: "1.30.0" + }, + hetznerdns: { + source: "timohirt/hetznerdns", + version: "1.1.1" + } + } + }, + + provider: { + hcloud: { + token: credentials.hcloudToken + }, + hetznerdns: { + apitoken: credentials.hdnsToken + } + }, + + resource: { + hcloud_ssh_key: { + [k]: terraform.HcloudSSHKey { name: k, public_key: defaults.defaultSshKeys[k] } + for k in std.objectFields(defaults.defaultSshKeys) + }, + + hcloud_server: { + [s]: servers[s].instance + for s in std.objectFields(servers) + }, + + hetznerdns_zone: { + infra: { name: defaults.infraDomain, ttl: defaults.defaultZoneTTL }, + } + { + [domainToName(domain)]: { name: domain, ttl: defaults.defaultZoneTTL } + for domain in domains + }, + + # Default records for every host (v4, v6, VPN) + local hostRecords = std.foldl(function (a, b) a + b, [ + terraform.serverDnsRecords(servers[s]) + for s in std.objectFields(servers) + ], {}), + + # DNS records for deployed apps + local appRecords = std.foldl( + function (records, server) + records + std.foldl( + function (entries, domain) entries + domainEntries(domain, servers[server]), + servers[server].deployment_vars.domains, {} + ), + std.objectFields(servers), {} + ), + + hetznerdns_record: hostRecords + appRecords + } +}