We didn't have any automation for creation of VMs to server DNS servers and the process consisted of many manual error-prone steps. In this commit, we introduce SetupDnsServerVM prog, which makes it straightforward to create fully-functional DNS servers from scratch (which could be useful for development environments for now and prod in the future) as well as adding VMs to existing DNS servers. While working on this, I noticed that we don't have a config variable for the project holding the DNS service resources we use in prod, so I created that. You also need to populate that in the dev env. DnsServer class itself doesn't hold that value (but DnsZone does) so that addition could be another beginner-friendly task. Usage is like this: ```ruby ds = DnsServer.first || DnsServer.create_with_id(name: "elephant") dz1 = DnsZone.create_with_id( project_id: Config.dns_service_project_id, name: "erentest1.ubicloud.com", last_purged_at: Time.now ) dz2 = DnsZone.create_with_id( project_id: Config.dns_service_project_id, name: "erentest2.ubicloud.com", last_purged_at: Time.now ) Strand.create(prog: "DnsZone::DnsZoneNexus", label: "wait") { _1.id = dz1.id } Strand.create(prog: "DnsZone::DnsZoneNexus", label: "wait") { _1.id = dz2.id } dz1.add_dns_server DnsServer.first dz2.add_dns_server DnsServer.first ["hebele", "hubele", "asdasd-asdasd-asd", "123123"].each do |r| dz1.insert_record(record_name: "#{r}.#{dz1.name}", type: "A", ttl: 10, data: "0.1.1.0") dz2.insert_record(record_name: "#{r}.#{dz2.name}", type: "A", ttl: 10, data: "0.1.1.0") end st = Prog::DnsZone::SetupDnsServerVm.assemble(DnsServer.first) ``` Note that concurrency between the VM creation, zone operations and records are not handled in the best way. So it's advised to double-check the results after the VM is created in prod environment. We have a ton more to do for a proper DNS service lifecycle. A few ideas: - Health check for DNS servers and VMs - Check-ups for consistency among the VMs of a DNS server - Ability to add/remove DNS zones - More validation about multiple DNS servers - DnsServer belongs to a project
145 lines
3.9 KiB
Ruby
145 lines
3.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Prog::DnsZone::SetupDnsServerVm < Prog::Base
|
|
subject_is :vm, :sshable
|
|
|
|
def self.assemble(dns_server_id, name: nil, vm_size: "standard-2", storage_size_gib: 30, location: "hetzner-fsn1")
|
|
unless (dns_server = DnsServer[dns_server_id])
|
|
fail "No existing Dns Server"
|
|
end
|
|
|
|
unless Project[Config.dns_service_project_id]
|
|
fail "No existing Project"
|
|
end
|
|
|
|
name ||= "#{dns_server.ubid}-#{SecureRandom.alphanumeric(8).downcase}"
|
|
|
|
DB.transaction do
|
|
vm_st = Prog::Vm::Nexus.assemble_with_sshable(
|
|
"ubi",
|
|
Config.dns_service_project_id,
|
|
location: location,
|
|
name: name,
|
|
size: vm_size,
|
|
storage_volumes: [
|
|
{encrypted: true, size_gib: storage_size_gib}
|
|
],
|
|
boot_image: "ubuntu-jammy",
|
|
enable_ip4: true
|
|
)
|
|
|
|
Strand.create_with_id(prog: "DnsZone::SetupDnsServerVm", label: "start", stack: [{subject_id: vm_st.id, dns_server_id: dns_server_id}])
|
|
end
|
|
end
|
|
|
|
def ds
|
|
@ds ||= DnsServer[frame["dns_server_id"]]
|
|
end
|
|
|
|
label def start
|
|
nap 5 unless vm.strand.label == "wait"
|
|
register_deadline(nil, 15 * 60)
|
|
hop_prepare
|
|
end
|
|
|
|
label def prepare
|
|
sshable.cmd <<~SH
|
|
sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
|
|
sudo sed -i ':a;N;$!ba;s/127.0.0.1 localhost\\n\\n#/127.0.0.1 localhost\\n127.0.0.1 #{vm.inhost_name}\\n\\n#/' /etc/hosts
|
|
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
|
|
sudo apt-get update
|
|
sudo apt-get -y install apt-transport-https ca-certificates wget
|
|
sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg
|
|
echo "deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/knot-dns jammy main" | sudo tee /etc/apt/sources.list.d/cznic-labs-knot-dns.list
|
|
sudo apt-get update
|
|
sudo systemctl reboot
|
|
SH
|
|
|
|
hop_setup_knot
|
|
end
|
|
|
|
label def setup_knot
|
|
nap 5 unless sshable.available?
|
|
|
|
sshable.cmd <<~SH
|
|
sudo apt-get -y install knot
|
|
echo "KNOTD_ARGS="-C /var/lib/knot/confdb"" | sudo tee -a /etc/default/knot
|
|
SH
|
|
|
|
knot_config = <<-CONF
|
|
server:
|
|
rundir: "/run/knot"
|
|
user: "knot:knot"
|
|
listen: [ "0.0.0.0@53", "::@53" ]
|
|
|
|
log:
|
|
- target: "syslog"
|
|
any: "info"
|
|
|
|
database:
|
|
storage: "/var/lib/knot"
|
|
|
|
acl:
|
|
- id: "allow_dynamic_updates"
|
|
address: "127.0.0.1/32"
|
|
action: "update"
|
|
|
|
template:
|
|
- id: "default"
|
|
storage: "/var/lib/knot"
|
|
file: "%s.zone"
|
|
acl: "allow_dynamic_updates"
|
|
zonefile-sync: "60"
|
|
zonefile-load: "difference"
|
|
journal-content: "all"
|
|
|
|
|
|
zone:
|
|
#{ds.dns_zones.map { |dz| "- domain: \"#{dz.name}.\"" }.join("\n ")}
|
|
CONF
|
|
|
|
sshable.cmd("sudo tee /etc/knot/knot.conf > /dev/null", stdin: knot_config)
|
|
|
|
hop_sync_zones
|
|
end
|
|
|
|
label def sync_zones
|
|
nap 5 if ds.dns_zones.any?(&:refresh_dns_servers_set?)
|
|
|
|
ds.dns_zones.each do |dz|
|
|
zone_config = <<-CONF
|
|
#{dz.name}. 3600 SOA ns.#{dz.name}. #{dz.name}. 37 86400 7200 1209600 3600
|
|
#{dz.name}. 3600 NS #{ds.name}.
|
|
CONF
|
|
sshable.cmd("sudo -u knot tee /var/lib/knot/#{dz.name}.zone > /dev/null", stdin: zone_config)
|
|
end
|
|
|
|
sshable.cmd "sudo systemctl restart knot"
|
|
|
|
ds.dns_zones.each(&:purge_obsolete_records)
|
|
|
|
commands = ds.dns_zones.flat_map do |dz|
|
|
["zone-abort #{dz.name}", "zone-begin #{dz.name}"] +
|
|
dz.records.map do |r|
|
|
"zone-set #{dz.name} #{r.name} #{r.ttl} #{r.type} #{r.data}"
|
|
end + ["zone-commit #{dz.name}", "zone-flush #{dz.name}"]
|
|
end
|
|
|
|
# Put records
|
|
sshable.cmd("sudo -u knot knotc", stdin: commands.join("\n"))
|
|
|
|
hop_validate
|
|
end
|
|
|
|
label def validate
|
|
outputs = (ds.vms + [vm]).map do |vm|
|
|
vm.sshable.cmd("sudo -u knot knotc", stdin: "zone-read --").split("\n").to_set
|
|
end
|
|
|
|
hop_sync_zones unless outputs.uniq.count == 1
|
|
|
|
ds.add_vm vm unless ds.vms.map(&:id).include? vm.id
|
|
pop "created VM for DnsServer"
|
|
end
|
|
end
|