diff options
-rw-r--r-- | TODOs.org | 101 | ||||
-rw-r--r-- | default.nix | 16 | ||||
-rwxr-xr-x | scripts/ci/provision.sh | 3 | ||||
-rwxr-xr-x | scripts/ci/setup.sh | 1 | ||||
-rw-r--r-- | secrets/envrc.sh | bin | 1987 -> 2052 bytes | |||
-rw-r--r-- | vps.tf | 78 |
6 files changed, 135 insertions, 64 deletions
@@ -100,24 +100,33 @@ CLOSED: [2019-06-05 Wed 19:28] See if it is actually working as expected. ** DONE Use Digital Ocean's Volumes for persistent extended storage CLOSED: [2019-06-05 Wed 20:38] -** TODO Make VPS provisioning more robust +** DONE Make VPS provisioning more robust +CLOSED: [2019-06-10 Mon 09:01] *** DONE Use Ansible (or an equivalent tool) instead of custom Bash scripts CLOSED: [2019-06-05 Wed 16:41] They are now more fragile, ad-hoc and imperative than I would like. Today Terraform won't run the =deploy.sh= if no infrastructure changes are required. Split infrastructure provisioning from server configuration with somethong like Ansible or =nix copy closure= and add extra command in the pipeline run. -*** TODO Always perform a blue/green infrastructure deployment with Terraform +*** DONE Always perform a blue/green infrastructure deployment with Terraform +CLOSED: [2019-06-10 Mon 09:01] Recreate a new Droplet from scratch, even if no changes happened. This way every deployment tests the code path of creating everything from scratch again, from the DNS public IP all the way to restoring backups. -*** TODO Destroy and recreate the volume on deployment +*** DONE Destroy and recreate the volume on deployment +CLOSED: [2019-06-10 Mon 09:01] Restore from the latest backup with: #+BEGIN_SOURCE shell borg list --short --sort-by timestamp | tail -n 1 #+END_SOURCE -** WAITING Configure DNS from Terraform -*** TODO Test provisioning DNS entries with other DNS registrars -*** TODO Have dynamic Floating IP (a.k.a. =$PINNED_IP=) +** DONE Configure DNS from Terraform +Handling DNS with DigitalOcean did it. Namecheap and GoDaddy API are bad, and all I had to do manually was configure a [[https://www.digitalocean.com/community/tutorials/how-to-point-to-digitalocean-nameservers-from-common-domain-registrars][custom nameserver to point to DigitalOcean's nameserver]]. +CLOSED: [2019-06-09 Sun 22:52] +*** DONE Test provisioning DNS entries with other DNS registrars +CLOSED: [2019-06-09 Sun 22:52] +DNS registrar API are bad in general (from what I've seen). Using DigitalOcean's DNS was more straightforward. +*** DONE Have dynamic Floating IP (a.k.a. =$PINNED_IP=) +CLOSED: [2019-06-09 Sun 22:52] +Floating IP is dynamically attached to the DNS entry in DigitalOcean itself. ** TODO Create snapshots before destroying resources This way the previous good state can be reverted if the deployment fails or the backup can't be restored. @@ -136,14 +145,13 @@ Right now, secrets are scattered between the two repositories. By moving I can c CLOSED: [2019-06-05 Wed 19:48] ** TODO Store updated =.tfstate= even in case of deployment failure Right now the script fails on Terraform commands before reaching git commands. I should trap the error, store on git and only then fail. -** TODO Fix alias in =bash-profile.sh= +** DONE Fix alias in =bash-profile.sh= +CLOSED: [2019-06-10 Mon 09:01] ** TODO Email verbose (Ansible) log files in case of error builds.sr.ht only emails the link. Should it be extended to support encrypted log attachments? ** TODO Use environment variables for SSH key paths and volume mounts ** TODO Don't allow backups to fail -* Must -** Fully deployable from code -Use NixOps and Terraform to fully automate all of the configuration. +** TODO Don't hardcode =/root/= paths: use =~/= instead to allow for different users * Services ** DONE =$tld=: Static webhosting CLOSED: [2019-05-26 Sun 10:17] @@ -209,14 +217,6 @@ Test the Emacs Matrix client along with the server installation. ** WAITING =search.$tld=: Searx instance Would it be actually more private? * Questions -** TODO How to dynamically handle Floating IPs? -Right now the current Floating IP defined in =.envrc= was created manually in DigitalOcean's web UI and copied from it to the environment variable. - -If everything was teared down, I couldn't recreate everything from source, because the Floating IP would be different. - -The ultimate goal would be to upsert a Floating IP address? If no Floating IP address exists, create one. If one already exists (I don't how to get a reference to it), use it. - -In other words, I don't want any hardcoded IPs in the recipe. The IP address has to be fixed, and the same on the DNS registrar and DigitalOcean's Floating IP. ** TODO Critiques of Docker? What does NixOps, DisNix and Dysnomia are trying to accomplish that overlap with Docker? Use sqldiff for NixOps? @@ -229,6 +229,17 @@ Maybe rsync the contents of the Borg repository into S3. Should I restore backup Can it do key rotation? ** TODO Can the =setup.sh= and =provision.sh= scripts be run inside a chroot or a NixOS contained environment? Right now they can't simply be a derivation because =setup.sh= needs access to the disk and =provision.sh= needs to access the internet. +** DONE How to dynamically handle Floating IPs? +CLOSED: [2019-06-10 Mon 08:59] +Right now the current Floating IP defined in =.envrc= was created manually in DigitalOcean's web UI and copied from it to the environment variable. + +If everything was teared down, I couldn't recreate everything from source, because the Floating IP would be different. + +The ultimate goal would be to upsert a Floating IP address? If no Floating IP address exists, create one. If one already exists (I don't how to get a reference to it), use it. + +In other words, I don't want any hardcoded IPs in the recipe. The IP address has to be fixed, and the same on the DNS registrar and DigitalOcean's Floating IP. +*** Solution +I provisioned both the Floating IP and the DNS A record in the same recipe. Now everything is recreated from scratch every time. ** DONE Do I want or need Docker? Should I use it? CLOSED: [2019-05-25 Sat 18:1980] It was a better path than sticking with NixOps and nixcloud-webservices. It's more widespread and has more things done for it. @@ -241,7 +252,53 @@ This was I can compartimentalize the data storage to easily backup and duplicate * Nice to have ** =euandreh.org= as =$tld= ** Nix Terraform provisioning -** Upgrade =terraform-godaddy= to 0.12 to support looping over CNAME records +** WAITING Upgrade =terraform-godaddy= to 0.12 to support looping over CNAME records +When using =terraform-godaddy= this made sense: +#+BEGIN_SRC hcl +locals { + cname_subdomains = [ + "${var.wallabag_tld_prefix}", + "${var.nextcloud_tld_prefix}", + ] +} + +resource "godaddy_domain_record" "vps_tld" { + domain = "${var.tld}" + addresses = ["${var.floating_ip}"] + + dynamic "record" { + for_each = local.cname_subdomains + + content { + type = "CNAME" + name = tag + data = "${var.tld}" + } + } +} +#+END_SRC +However, when transitioning to DNS provisioning using DigitalOcean, there's a catch: the =digitalocean_record= resource in Terraform lives on the toplevel, not nested. I tried doing a similar thing, but [[https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each][Terraform 0.12 doesn't support =for_each= loops on =resources=]]: + +: During the development of Terraform 0.12 we've also laid the groundwork for supporting for_each directly inside a resource or data block as a more convenient way to create a resource instance for each element in a list or map. Unfortunately we will not be able to fully complete this feature for the Terraform 0.12 initial release, but we plan to include this in a subsequent release to make it easier to dynamically construct multiple resource instances of the same type based on elements of a given map. + +The equivalent code should look like: +#+BEGIN_SRC hcl +locals { + cname_subdomains = [ + "${var.wallabag_tld_prefix}", + "${var.nextcloud_tld_prefix}", + ] +} + +resource "digitalocean_record" "subdomains" { + for_each = local.cname_subdomains + + domain = "${digitalocean_domain.vps_tld.name}" + type = "CNAME" + name = each + value = "${digitalocean_domain.vps_tld.name}." +} +#+END_SRC ** Upgrade =docker-compose.yaml= file from version 2 to version 3 ** Full blue/green deployments without downtime Only when doing a voluntary restore from backup in a newly created volume. @@ -298,4 +355,10 @@ Instead, explicitly call =ansible-playbook= after =terraform apply= finished run This way we test the DNS A record -> Floating IP -> Droplet IP path. We can't do that inside Terraform declaration because the =local-exec= provisioning command runs before the =digitalocean_floating_ip_assignment= is created, and we can't create a cyclic dependency between the two resources. We could use the raw Droplet IP instead of the DNS A record, but I prefer calling it later in order to always test the full DNS resolution. +* COMMENT +** DONE Must +CLOSED: [2019-06-10 Mon 08:51] +*** DONE Fully deployable from code +CLOSED: [2019-06-10 Mon 08:51] +Use +NixOps+ Ansible and Terraform to fully automate all of the configuration. * Scrath diff --git a/default.nix b/default.nix index 35eead8..cc64016 100644 --- a/default.nix +++ b/default.nix @@ -1,16 +1,4 @@ -let - pkgs = import <nixpkgs> { }; - terraform-godaddy = pkgs.buildGoModule rec { - name = "terraform-godaddy-${version}"; - version = "1.6.4"; - src = pkgs.fetchFromGitHub { - owner = "n3integration"; - repo = "terraform-godaddy"; - rev = "v${version}"; - sha256 = "00blqsan74s53dk9ab4hxi1kzxi46k57dr65dmbiradfa3yz3852"; - }; - modSha256 = "0p81wqw2n8vraxk20xwg717582ijwq2k7v5j3n13y4cd5bxd8hhz"; - }; +let pkgs = import <nixpkgs> { }; in rec { utils = import ./utils.nix { pkgs = pkgs; @@ -18,7 +6,6 @@ in rec { baseName = "vps"; }; subtasks = rec { - terraformGodaddyBuild = terraform-godaddy; formatTerraform = utils.baseTask.overrideAttrs (baseAttrs: { name = "${baseAttrs.name}-format-terraform"; buildInputs = baseAttrs.buildInputs ++ [ pkgs.terraform ]; @@ -52,7 +39,6 @@ in rec { git-crypt gettext terraform-providers.digitalocean - terraform-godaddy terraform ansible ]; diff --git a/scripts/ci/provision.sh b/scripts/ci/provision.sh index 3b2d912..b1d23e5 100755 --- a/scripts/ci/provision.sh +++ b/scripts/ci/provision.sh @@ -28,7 +28,6 @@ ssh "$TLD" /home/vps/create-backup.sh || echo "FAILED TO CREATE BACKUP." echo "Done." echo "Initializing Terraform..." -ln -s "$(command -v terraform-godaddy)" terraform-provider-godaddy terraform --version terraform init echo "Done." @@ -66,6 +65,8 @@ popd echo "Done." echo "Running the Ansible playbook..." + +echo "${TLD},$(terraform output public_floating_ip) ssh-rsa $(awk '{print $2}' < ./secrets/ssh/vps-box-server.pub)" > ./generated-known-hosts.txt ansible-playbook provision.yaml echo "Done." diff --git a/scripts/ci/setup.sh b/scripts/ci/setup.sh index f134c6a..d9ac70c 100755 --- a/scripts/ci/setup.sh +++ b/scripts/ci/setup.sh @@ -33,7 +33,6 @@ export SSH_SERVER_PUBLIC_KEY # https://stackoverflow.com/questions/24963705/is-there-an-escape-character-for-envsubst export DOLLAR='$' -echo "${TLD},${PINNED_IP} ssh-rsa $(echo "${SSH_SERVER_PUBLIC_KEY}" | awk '{print $2}')" > ./generated-known-hosts.txt envsubst < ./ssh.env.conf >> ~/.ssh/config envsubst < ./hosts.env > ./hosts envsubst < ./docker-compose.env.yaml > ./docker-compose.yaml diff --git a/secrets/envrc.sh b/secrets/envrc.sh Binary files differindex 82f186a..94ec7e5 100644 --- a/secrets/envrc.sh +++ b/secrets/envrc.sh @@ -1,9 +1,22 @@ -variable "floating_ip" {} +variable "do_token" { + type = "string" + description = "DigitalOcean API token." +} + +variable "tld" { + type = "string" + description = "Root Top-Level Domain. Subdomains will be derived from it." +} -variable "do_token" {} -variable "tld" {} -variable "wallabag_tld" {} -variable "nextcloud_tld" {} +variable "wallabag_tld_prefix" { + type = "string" + description = "DNS prefix used for the Wallabag installation. Does not contain a dot at the end." +} + +variable "nextcloud_tld_prefix" { + type = "string" + description = "DNS prefix used for the Nextcloud installation. Does not contain a dot at the end." +} provider "digitalocean" { token = "${var.do_token}" @@ -42,11 +55,6 @@ resource "digitalocean_droplet" "vps" { } } -resource "digitalocean_floating_ip_assignment" "vps" { - ip_address = "${var.floating_ip}" - droplet_id = "${digitalocean_droplet.vps.id}" -} - resource "digitalocean_volume" "vps_persistent_volume" { region = "nyc3" name = "vps-persistent-volume" @@ -60,26 +68,40 @@ resource "digitalocean_volume_attachment" "foobar" { droplet_id = "${digitalocean_droplet.vps.id}" } -locals { - cname_subdomains = [ - "${var.wallabag_tld}", - "${var.nextcloud_tld}", - ] +resource "digitalocean_floating_ip" "vps_public_ip" { + region = "${digitalocean_droplet.vps.region}" } -resource "godaddy_domain_record" "vps_tld" { - domain = "${var.tld}" - addresses = ["${var.floating_ip}"] +resource "digitalocean_floating_ip_assignment" "vps_public_ip_assignment" { + ip_address = "${digitalocean_floating_ip.vps_public_ip.id}" + droplet_id = "${digitalocean_droplet.vps.id}" +} - record { - type = "CNAME" - name = "${var.tld}" - data = "${var.wallabag_tld}" - } +output "public_floating_ip" { + value = "${digitalocean_floating_ip.vps_public_ip.ip_address}" +} - record { - type = "CNAME" - name = "${var.tld}" - data = "${var.nextcloud_tld}" - } +resource "digitalocean_domain" "vps_tld" { + name = "${var.tld}" +} + +resource "digitalocean_record" "at_sign" { + domain = "${digitalocean_domain.vps_tld.name}" + type = "A" + name = "@" + value = "${digitalocean_floating_ip.vps_public_ip.ip_address}" +} + +resource "digitalocean_record" "wallabag" { + domain = "${digitalocean_domain.vps_tld.name}" + type = "CNAME" + name = "${var.wallabag_tld_prefix}" + value = "${digitalocean_domain.vps_tld.name}." +} + +resource "digitalocean_record" "nextcloud" { + domain = "${digitalocean_domain.vps_tld.name}" + type = "CNAME" + name = "${var.nextcloud_tld_prefix}" + value = "${digitalocean_domain.vps_tld.name}." } |