Refactor and add deployment support

This commit is contained in:
Martin 2021-10-12 00:13:21 +02:00
parent ae2e359225
commit 6cc0596b72
Signed by: mawalu
GPG Key ID: BF556F989760A7C8
13 changed files with 218 additions and 106 deletions

View File

@ -1,5 +0,0 @@
local credentials = import "../credentials.libsonnet";
local servers = import "servers.libsonnet";
local networking = import "networking.libsonnet";
credentials + servers + networking

10
config/defaults.libsonnet Normal file
View File

@ -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'
},
}

3
config/domains.libsonnet Normal file
View File

@ -0,0 +1,3 @@
[
'example.com'
]

View File

@ -1,7 +0,0 @@
{
infraDomain: "m5w.de",
# needs to be /96
ipSubnet: "fdc2:d459:3f8a:84a3:coffe:coffe",
defaultTTL: 3600,
defaultZoneTTL: 86400,
}

View File

@ -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'),
]
)

22
lib/deployments.libsonnet Normal file
View File

@ -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 ]
}
}
}

21
lib/servers.libsonnet Normal file
View File

@ -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, {})
}
}
}

View File

@ -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" },
}
}

32
lib/utils.libsonnet Normal file
View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
}
}
}

View File

@ -0,0 +1,11 @@
[
{
name: "Test command",
hosts: "all",
tasks: [
{
"ansible.builtin.command": "ls"
}
]
}
]

View File

@ -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
}
}