From 145e1dab68caf3f57c53820c6359bef83a5ce52a Mon Sep 17 00:00:00 2001 From: Franck Cuny Date: Sun, 31 Aug 2025 13:33:54 -0700 Subject: manage terraform configuration with terranix All the terraform configuration is managed within one state instead of having multiple state for each components. This might not be the best practice but it simplifies things for me. Now, all I need to do is to run `nix run .#tf -- plan` and I can see what will be changed for all the resources that I care about. --- .envrc | 2 + .envrc.local.template | 4 + .gitignore | 6 +- docs/dns.org | 3 +- flake.nix | 2 +- flake/overlays.nix | 23 +++- flake/terraform.nix | 40 ++++++ infra/tf/.envrc | 12 -- infra/tf/.envrc.local.template | 4 - infra/tf/.gitignore | 1 - infra/tf/backups-bucket/main.tf | 43 ------ infra/tf/backups-bucket/outputs.tf | 24 ---- infra/tf/backups-bucket/terraform.tfvars | 27 ---- infra/tf/backups-bucket/variables.tf | 73 ---------- infra/tf/cloudflare-dns/main.tf | 24 ---- infra/tf/cloudflare-dns/records.tf | 223 ------------------------------- infra/tf/cloudflare-dns/terraform.tfvars | 3 - infra/tf/cloudflare-dns/variables.tf | 11 -- infra/tf/do-vm/extra-files-script.sh | 8 -- infra/tf/do-vm/main.tf | 82 ------------ infra/tf/do-vm/outputs.tf | 9 -- infra/tf/do-vm/terraform.tfvars | 2 - infra/tf/flake-module.nix | 30 ----- infra/tf/scripts.nix | 58 -------- scripts/common.nix | 25 ++++ terraform/admin/backups.nix | 28 ++++ terraform/admin/base.nix | 30 +++++ terraform/admin/default.nix | 9 ++ terraform/admin/dns.nix | 117 ++++++++++++++++ terraform/admin/droplet-proxy.nix | 89 ++++++++++++ terraform/admin/variables.nix | 29 ++++ 31 files changed, 401 insertions(+), 640 deletions(-) create mode 100644 .envrc.local.template create mode 100644 flake/terraform.nix delete mode 100644 infra/tf/.envrc delete mode 100644 infra/tf/.envrc.local.template delete mode 100644 infra/tf/.gitignore delete mode 100644 infra/tf/backups-bucket/main.tf delete mode 100644 infra/tf/backups-bucket/outputs.tf delete mode 100644 infra/tf/backups-bucket/terraform.tfvars delete mode 100644 infra/tf/backups-bucket/variables.tf delete mode 100644 infra/tf/cloudflare-dns/main.tf delete mode 100644 infra/tf/cloudflare-dns/records.tf delete mode 100644 infra/tf/cloudflare-dns/terraform.tfvars delete mode 100644 infra/tf/cloudflare-dns/variables.tf delete mode 100755 infra/tf/do-vm/extra-files-script.sh delete mode 100644 infra/tf/do-vm/main.tf delete mode 100644 infra/tf/do-vm/outputs.tf delete mode 100644 infra/tf/do-vm/terraform.tfvars delete mode 100644 infra/tf/flake-module.nix delete mode 100644 infra/tf/scripts.nix create mode 100644 terraform/admin/backups.nix create mode 100644 terraform/admin/base.nix create mode 100644 terraform/admin/default.nix create mode 100644 terraform/admin/dns.nix create mode 100644 terraform/admin/droplet-proxy.nix create mode 100644 terraform/admin/variables.nix diff --git a/.envrc b/.envrc index 91d631a..6e7c094 100644 --- a/.envrc +++ b/.envrc @@ -3,3 +3,5 @@ watch_file nix/ watch_file flake/*.nix watch_file scripts/*.nix + +source_env_if_exists .envrc.local diff --git a/.envrc.local.template b/.envrc.local.template new file mode 100644 index 0000000..82ebf5f --- /dev/null +++ b/.envrc.local.template @@ -0,0 +1,4 @@ +#!/bin/sh + +# stored in 1password +export CLOUDFLARE_API_TOKEN=... diff --git a/.gitignore b/.gitignore index 30ae628..30f8639 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,7 @@ bin/ /.direnv/ .aider* .env - -.terraform* +config.tf.json +.terraform +.terraform.lock.hcl +/.envrc.local diff --git a/docs/dns.org b/docs/dns.org index 53a0873..1972f5f 100644 --- a/docs/dns.org +++ b/docs/dns.org @@ -2,10 +2,9 @@ ** fcuny.net To access the console: https://dash.cloudflare.com/2c659eeaf2ae9a0206c589c706b3748e/fcuny.net -The records are managed with =tofu=, and I use =nix= to manage the records. To apply the changes, first run =tofu-plan= to see the changes, then =tofu-apply= if they look correct. +The records are managed with =tofu=, and I use =nix= to manage the records. To apply the changes, first run =nix run .#tf -- plan= to see the changes, then =nix run .#tf -- apply= if they look correct. To renew API token: https://dash.cloudflare.com/profile/api-tokens The token should: - be able to edit zone DNS - limited to the domain =fcuny.net= -- have a reasonable TTL (~60 days) diff --git a/flake.nix b/flake.nix index cf2675a..4686208 100644 --- a/flake.nix +++ b/flake.nix @@ -73,9 +73,9 @@ ./flake/devshells.nix ./flake/formatter.nix ./flake/hosts.nix + ./flake/terraform.nix ./flake/overlays.nix ./flake/packages.nix - ./infra/tf/flake-module.nix ]; }; } diff --git a/flake/overlays.nix b/flake/overlays.nix index 1eecfcf..2f9100d 100644 --- a/flake/overlays.nix +++ b/flake/overlays.nix @@ -1,4 +1,9 @@ -{ inputs, self, ... }: +{ + inputs, + self, + config, + ... +}: { flake.overlays.default = _final: prev: { @@ -8,6 +13,17 @@ perSystem = { system, ... }: + let + mkTerraformCfg = + modules: + inputs.terranix.lib.terranixConfiguration { + inherit system; + extraArgs = { + inherit (config.flake) nixosConfigurations; + }; + inherit modules; + }; + in { _module.args.pkgs = import inputs.nixpkgs { inherit system; @@ -18,6 +34,11 @@ inputs.nur.overlays.default inputs.my-go-tools.overlays.default self.overlays.default + (_self: _super: { + adminTerraformCfg = mkTerraformCfg [ + "${self}/terraform/admin" + ]; + }) ]; }; }; diff --git a/flake/terraform.nix b/flake/terraform.nix new file mode 100644 index 0000000..23cc6d3 --- /dev/null +++ b/flake/terraform.nix @@ -0,0 +1,40 @@ +{ lib, ... }: +{ + perSystem = + { pkgs, ... }: + let + mkTfWrapper = + { + tfPlugins, + cfg, + }: + let + pkg = pkgs.opentofu.withPlugins tfPlugins; + in + { + type = "app"; + program = toString ( + pkgs.writers.writeBash "tf" '' + set -xeuo pipefail + ln -snf ${cfg} config.tf.json + exec ${lib.getExe pkg} "$@" + '' + ); + }; + in + { + apps = { + tf = mkTfWrapper { + cfg = pkgs.adminTerraformCfg; + tfPlugins = p: [ + p.cloudflare + p.digitalocean + p.external + p.google + p.null + p.random + ]; + }; + }; + }; +} diff --git a/infra/tf/.envrc b/infra/tf/.envrc deleted file mode 100644 index dd49577..0000000 --- a/infra/tf/.envrc +++ /dev/null @@ -1,12 +0,0 @@ -# shellcheck shell=bash -use flake .#terraform - -watch_file flake-module.nix -watch_file scripts.nix - -source_env_if_exists .envrc.local - -if ! use flake . --impure -then - echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 -fi diff --git a/infra/tf/.envrc.local.template b/infra/tf/.envrc.local.template deleted file mode 100644 index 82ebf5f..0000000 --- a/infra/tf/.envrc.local.template +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -# stored in 1password -export CLOUDFLARE_API_TOKEN=... diff --git a/infra/tf/.gitignore b/infra/tf/.gitignore deleted file mode 100644 index cff6f1b..0000000 --- a/infra/tf/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.envrc.local diff --git a/infra/tf/backups-bucket/main.tf b/infra/tf/backups-bucket/main.tf deleted file mode 100644 index a86e582..0000000 --- a/infra/tf/backups-bucket/main.tf +++ /dev/null @@ -1,43 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - google = { - source = "hashicorp/google" - version = ">= 5.0" - } - } - - backend "gcs" { - bucket = "fcuny-infra-tofu-state" - prefix = "backups" - } -} - -provider "google" { - project = var.project_id - region = var.region -} - -resource "google_storage_bucket" "backups" { - name = var.bucket_name - location = var.location - uniform_bucket_level_access = var.uniform_bucket_level_access - force_destroy = var.force_destroy - public_access_prevention = var.public_access_prevention - storage_class = var.storage_class - - # Optional: Add lifecycle rules for cost optimization - dynamic "lifecycle_rule" { - for_each = var.lifecycle_rules - content { - condition { - age = lifecycle_rule.value.age - } - action { - type = lifecycle_rule.value.action - storage_class = lifecycle_rule.value.storage_class - } - } - } -} diff --git a/infra/tf/backups-bucket/outputs.tf b/infra/tf/backups-bucket/outputs.tf deleted file mode 100644 index 3ca60c5..0000000 --- a/infra/tf/backups-bucket/outputs.tf +++ /dev/null @@ -1,24 +0,0 @@ -output "bucket_name" { - description = "Name of the created backup bucket" - value = google_storage_bucket.backups.name -} - -output "bucket_url" { - description = "URL of the backup bucket" - value = google_storage_bucket.backups.url -} - -output "bucket_self_link" { - description = "Self-link of the backup bucket" - value = google_storage_bucket.backups.self_link -} - -output "bucket_location" { - description = "Location of the backup bucket" - value = google_storage_bucket.backups.location -} - -output "bucket_storage_class" { - description = "Storage class of the backup bucket" - value = google_storage_bucket.backups.storage_class -} diff --git a/infra/tf/backups-bucket/terraform.tfvars b/infra/tf/backups-bucket/terraform.tfvars deleted file mode 100644 index 1d4f2c4..0000000 --- a/infra/tf/backups-bucket/terraform.tfvars +++ /dev/null @@ -1,27 +0,0 @@ -# GCP Configuration -project_id = "fcuny-infra" -region = "us-west1" - -# Bucket Configuration -bucket_name = "fcuny-infra-backups" -location = "us-west1" - -# Bucket Settings -uniform_bucket_level_access = true -force_destroy = true -public_access_prevention = "enforced" -storage_class = "NEARLINE" - -# Lifecycle Rules (optional customization) -lifecycle_rules = [ - { - age = 365 # After 1 year - action = "SetStorageClass" - storage_class = "COLDLINE" - }, - { - age = 730 # After 2 years - action = "SetStorageClass" - storage_class = "ARCHIVE" - } -] diff --git a/infra/tf/backups-bucket/variables.tf b/infra/tf/backups-bucket/variables.tf deleted file mode 100644 index ffdc1a6..0000000 --- a/infra/tf/backups-bucket/variables.tf +++ /dev/null @@ -1,73 +0,0 @@ -variable "project_id" { - description = "GCP Project ID" - type = string - default = "fcuny-infra" -} - -variable "region" { - description = "GCP Region" - type = string - default = "us-west1" -} - -variable "bucket_name" { - description = "Name of the backup storage bucket" - type = string - default = "fcuny-infra-backups" -} - -variable "location" { - description = "Location for the storage bucket" - type = string - default = "us-west1" -} - -variable "uniform_bucket_level_access" { - description = "Enable uniform bucket-level access" - type = bool - default = true -} - -variable "force_destroy" { - description = "Allow destruction of bucket even if it contains objects" - type = bool - default = true -} - -variable "public_access_prevention" { - description = "Public access prevention setting" - type = string - default = "enforced" - - validation { - condition = contains(["enforced", "inherited"], var.public_access_prevention) - error_message = "Public access prevention must be either 'enforced' or 'inherited'." - } -} - -variable "storage_class" { - description = "Storage class for the bucket" - type = string - default = "NEARLINE" - - validation { - condition = contains(["STANDARD", "NEARLINE", "COLDLINE", "ARCHIVE"], var.storage_class) - error_message = "Storage class must be one of: STANDARD, NEARLINE, COLDLINE, ARCHIVE." - } -} - -variable "lifecycle_rules" { - description = "List of lifecycle rules for the bucket" - type = list(object({ - age = number - action = string - storage_class = string - })) - default = [ - { - age = 365 - action = "SetStorageClass" - storage_class = "COLDLINE" - } - ] -} diff --git a/infra/tf/cloudflare-dns/main.tf b/infra/tf/cloudflare-dns/main.tf deleted file mode 100644 index 30442e6..0000000 --- a/infra/tf/cloudflare-dns/main.tf +++ /dev/null @@ -1,24 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - cloudflare = { - source = "cloudflare/cloudflare" - version = "~> 5" - } - } - - backend "gcs" { - bucket = "fcuny-infra-tofu-state" - prefix = "cloudflare-dns" - } -} - -provider "cloudflare" { - # API token will be provided via CLOUDFLARE_API_TOKEN environment variable -} - -# Use data source for existing zone instead of managing it -data "cloudflare_zone" "main" { - zone_id = var.zone_id -} diff --git a/infra/tf/cloudflare-dns/records.tf b/infra/tf/cloudflare-dns/records.tf deleted file mode 100644 index a282e2f..0000000 --- a/infra/tf/cloudflare-dns/records.tf +++ /dev/null @@ -1,223 +0,0 @@ -resource "cloudflare_dns_record" "cname_root_0" { - content = "185.199.108.153" - name = "fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_root_1" { - content = "185.199.110.153" - name = "fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_root_2" { - content = "185.199.109.153" - name = "fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_root_3" { - content = "185.199.111.153" - name = "fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_code" { - content = "165.232.158.110" - name = "code.fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_go" { - content = "165.232.158.110" - name = "go.fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_id" { - content = "165.232.158.110" - name = "id.fcuny.net" - proxied = false - ttl = 1 - type = "A" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_dkim_0" { - content = "fm1.fcuny.net.dkim.fmhosted.com" - name = "fm1._domainkey" - proxied = false - ttl = 60 - type = "CNAME" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_dkim_1" { - content = "fm2.fcuny.net.dkim.fmhosted.com" - name = "fm2._domainkey" - proxied = false - ttl = 60 - type = "CNAME" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "cname_dkim_2" { - content = "fm3.fcuny.net.dkim.fmhosted.com" - name = "fm3._domainkey" - proxied = false - ttl = 60 - type = "CNAME" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "mx_0" { - content = "in1-smtp.messagingengine.com" - name = "fcuny.net" - priority = 10 - proxied = false - ttl = 1 - type = "MX" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "mx_1" { - content = "in2-smtp.messagingengine.com" - name = "fcuny.net" - priority = 20 - proxied = false - ttl = 1 - type = "MX" - zone_id = var.zone_id -} - -resource "cloudflare_dns_record" "srv_caldavs" { - name = "_caldavs._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 443 - priority = 0 - target = "caldav.fastmail.com" - weight = 1 - } -} - -resource "cloudflare_dns_record" "srv_caldav" { - name = "_caldav._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 0 - priority = 0 - target = "." - weight = 0 - } -} - -resource "cloudflare_dns_record" "srv_carddavs" { - name = "_carddavs._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 443 - priority = 0 - target = "carddav.fastmail.com" - weight = 1 - } -} - -resource "cloudflare_dns_record" "srv_carddav" { - name = "_carddav._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 0 - priority = 0 - target = "." - weight = 0 - } -} - -resource "cloudflare_dns_record" "srv_imaps" { - name = "_imaps._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 993 - priority = 0 - target = "imap.fastmail.com" - weight = 1 - } -} - -resource "cloudflare_dns_record" "srv_imap" { - name = "_imap._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 0 - priority = 0 - target = "." - weight = 0 - } -} - -resource "cloudflare_dns_record" "srv_smtp" { - name = "_submission._tcp" - priority = 0 - proxied = false - ttl = 1 - type = "SRV" - zone_id = var.zone_id - data = { - port = 587 - priority = 0 - target = "smtp.fastmail.com" - weight = 1 - } -} - -resource "cloudflare_dns_record" "txt_spf" { - content = "\"v=spf1 include:spf.messagingengine.com ?all\"" - name = "fcuny.net" - proxied = false - ttl = 1 - type = "TXT" - zone_id = var.zone_id -} diff --git a/infra/tf/cloudflare-dns/terraform.tfvars b/infra/tf/cloudflare-dns/terraform.tfvars deleted file mode 100644 index 5a88de4..0000000 --- a/infra/tf/cloudflare-dns/terraform.tfvars +++ /dev/null @@ -1,3 +0,0 @@ -# Zone Configuration -zone_id = "6878e48b5cb81c7d789040632153719d" -state_bucket = "fcuny-infra-tofu-state" diff --git a/infra/tf/cloudflare-dns/variables.tf b/infra/tf/cloudflare-dns/variables.tf deleted file mode 100644 index 24a4a35..0000000 --- a/infra/tf/cloudflare-dns/variables.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "zone_id" { - description = "Cloudflare zone ID" - type = string - default = "6878e48b5cb81c7d789040632153719d" -} - -variable "state_bucket" { - description = "GCS bucket for Terraform state" - type = string - default = "fcuny-infra-tofu-state" -} diff --git a/infra/tf/do-vm/extra-files-script.sh b/infra/tf/do-vm/extra-files-script.sh deleted file mode 100755 index fa47762..0000000 --- a/infra/tf/do-vm/extra-files-script.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -mkdir -p etc/ssh/ - -if [ -n "$DO_SSH_HOSTKEY" ]; then - echo "$DO_SSH_HOSTKEY" | base64 -d > etc/ssh/ssh_host_ed25519_key - chmod 0600 etc/ssh/ssh_host_ed25519_key -fi diff --git a/infra/tf/do-vm/main.tf b/infra/tf/do-vm/main.tf deleted file mode 100644 index 4cff3e9..0000000 --- a/infra/tf/do-vm/main.tf +++ /dev/null @@ -1,82 +0,0 @@ -terraform { - required_providers { - digitalocean = { - source = "digitalocean/digitalocean" - version = "~> 2.0" - } - random = { - source = "hashicorp/random" - version = "~> 3.1" - } - } - - backend "gcs" { - bucket = "fcuny-infra-tofu-state" - prefix = "do-vm" - } -} - -provider "digitalocean" { - # Token will be read from DIGITALOCEAN_TOKEN environment variable -} - -variable "ssh_public_key" { - description = "SSH public key content" - type = string - default = "" -} - -variable "region" { - description = "DigitalOcean region" - type = string - default = "" -} - -# Random string for unique naming -resource "random_string" "host" { - length = 6 - special = false - upper = false -} - -# Locals -locals { - server_size = "s-2vcpu-2gb" - labels = { - environment = "nixos" - managed_by = "terraform" - } -} - -resource "digitalocean_ssh_key" "default" { - name = "nixos-anywhere-${random_string.host.result}" - public_key = var.ssh_public_key -} - -resource "digitalocean_droplet" "nixos" { - name = "nixos-${random_string.host.result}" - image = "ubuntu-24-04-x64" # Just to get the server started - size = local.server_size - region = var.region - ssh_keys = [digitalocean_ssh_key.default.id] - tags = ["nixos", "infrastructure"] -} - -module "nixos-system-build" { - source = "github.com/nix-community/nixos-anywhere//terraform/nix-build" - attribute = ".#nixosConfigurations.digitalocean.config.system.build.toplevel" -} - -module "nixos-disko" { - source = "github.com/nix-community/nixos-anywhere//terraform/nix-build" - attribute = ".#nixosConfigurations.digitalocean.config.system.build.diskoScript" -} - -module "nixos-install" { - source = "github.com/nix-community/nixos-anywhere//terraform/install" - nixos_system = module.nixos-system-build.result.out - nixos_partitioner = module.nixos-disko.result.out - target_host = digitalocean_droplet.nixos.ipv4_address - build_on_remote = true - extra_files_script = "./extra-files-script.sh" -} diff --git a/infra/tf/do-vm/outputs.tf b/infra/tf/do-vm/outputs.tf deleted file mode 100644 index 2b9f5c2..0000000 --- a/infra/tf/do-vm/outputs.tf +++ /dev/null @@ -1,9 +0,0 @@ -output "nixos_ip" { - description = "IP address of the NixOS server" - value = digitalocean_droplet.nixos.ipv4_address -} - -output "droplet_id" { - description = "DigitalOcean droplet ID" - value = digitalocean_droplet.nixos.id -} diff --git a/infra/tf/do-vm/terraform.tfvars b/infra/tf/do-vm/terraform.tfvars deleted file mode 100644 index 99af205..0000000 --- a/infra/tf/do-vm/terraform.tfvars +++ /dev/null @@ -1,2 +0,0 @@ -ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINBkozy+X96u5ciX766bJ/AyQ3xm1tXZTIr5+4PVFZFi" -region = "sfo3" diff --git a/infra/tf/flake-module.nix b/infra/tf/flake-module.nix deleted file mode 100644 index f5a01dd..0000000 --- a/infra/tf/flake-module.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ - perSystem = - { pkgs, ... }: - let - scripts = import ./scripts.nix { - inherit pkgs; - }; - in - { - devShells.terraform = pkgs.mkShellNoCC { - packages = - with pkgs; - [ - google-cloud-sdk - age - (pkgs.opentofu.withPlugins (p: [ - p.cloudflare - p.digitalocean - p.external - p.google - p.null - p.random - ])) - ] - ++ [ - scripts - ]; - }; - }; -} diff --git a/infra/tf/scripts.nix b/infra/tf/scripts.nix deleted file mode 100644 index 8e20d30..0000000 --- a/infra/tf/scripts.nix +++ /dev/null @@ -1,58 +0,0 @@ -{ pkgs }: -let - tofuSetup = '' - tofu_setup() { - # Ensure bucket exists - ${pkgs.google-cloud-sdk}/bin/gcloud storage buckets describe \ - gs://fcuny-infra-tofu-state \ - --project=fcuny-infra \ - --quiet || \ - - ${pkgs.google-cloud-sdk}/bin/gcloud storage buckets create \ - gs://fcuny-infra-tofu-state \ - --project=fcuny-infra \ - --uniform-bucket-level-access \ - --public-access-prevention \ - --location=us-west1 \ - --default-storage-class=STANDARD \ - --quiet - } - ''; -in -[ - (pkgs.writeShellScriptBin "gcloud-auth" '' - set -xeuo pipefail - ${pkgs.google-cloud-sdk}/bin/gcloud auth print-identity-token > /dev/null 2>&1 || \ - ${pkgs.google-cloud-sdk}/bin/gcloud auth login --quiet - ${pkgs.google-cloud-sdk}/bin/gcloud auth application-default print-access-token > /dev/null 2>&1 || \ - ${pkgs.google-cloud-sdk}/bin/gcloud auth application-default login --quiet - '') - - (pkgs.writeShellScriptBin "tf-plan-dns" '' - set -xeuo pipefail - ${tofuSetup} - tofu_setup - ${pkgs.opentofu}/bin/tofu -chdir="cloudflare-dns" plan - '') - - (pkgs.writeShellScriptBin "tf-apply-dns" '' - set -xeuo pipefail - ${tofuSetup} - tofu_setup - ${pkgs.opentofu}/bin/tofu -chdir="cloudflare-dns" apply -auto-approve - '') - - (pkgs.writeShellScriptBin "tf-plan-backups" '' - set -xeuo pipefail - ${tofuSetup} - tofu_setup - ${pkgs.opentofu}/bin/tofu -chdir="backups-bucket" plan - '') - - (pkgs.writeShellScriptBin "tf-plan-do" '' - set -xeuo pipefail - ${tofuSetup} - tofu_setup - ${pkgs.opentofu}/bin/tofu -chdir="do-vm" plan - '') -] diff --git a/scripts/common.nix b/scripts/common.nix index 931480c..b8ab82e 100644 --- a/scripts/common.nix +++ b/scripts/common.nix @@ -1,4 +1,29 @@ { pkgs }: [ (pkgs.writeScriptBin "update-deps" "nix flake update --commit-lock-file") + + (pkgs.writeShellScriptBin "gcloud-auth" '' + set -xeuo pipefail + ${pkgs.google-cloud-sdk}/bin/gcloud auth print-identity-token > /dev/null 2>&1 || \ + ${pkgs.google-cloud-sdk}/bin/gcloud auth login --quiet + ${pkgs.google-cloud-sdk}/bin/gcloud auth application-default print-access-token > /dev/null 2>&1 || \ + ${pkgs.google-cloud-sdk}/bin/gcloud auth application-default login --quiet + '') + + (pkgs.writeShellScriptBin "tf-state-setup" '' + set -xeuo pipefail + ${pkgs.google-cloud-sdk}/bin/gcloud storage buckets describe \ + gs://fcuny-infra-tofu-state \ + --project=fcuny-infra \ + --quiet || \ + + ${pkgs.google-cloud-sdk}/bin/gcloud storage buckets create \ + gs://fcuny-infra-tofu-state \ + --project=fcuny-infra \ + --uniform-bucket-level-access \ + --public-access-prevention \ + --location=us-west1 \ + --default-storage-class=STANDARD \ + --quiet + '') ] diff --git a/terraform/admin/backups.nix b/terraform/admin/backups.nix new file mode 100644 index 0000000..ae021e5 --- /dev/null +++ b/terraform/admin/backups.nix @@ -0,0 +1,28 @@ +{ lib, ... }: +{ + resource.google_storage_bucket.backups = { + name = "fcuny-infra-backups"; + storage_class = "NEARLINE"; + force_destroy = true; + uniform_bucket_level_access = true; + public_access_prevention = "enforced"; + location = lib.tfRef "var.gcp_region"; + + lifecycle_rule = [ + { + condition.age = 365; # After 1 year + action = { + type = "SetStorageClass"; + storage_class = "COLDLINE"; + }; + } + { + condition.age = 730; # After 2 years + action = { + type = "SetStorageClass"; + storage_class = "ARCHIVE"; + }; + } + ]; + }; +} diff --git a/terraform/admin/base.nix b/terraform/admin/base.nix new file mode 100644 index 0000000..7221742 --- /dev/null +++ b/terraform/admin/base.nix @@ -0,0 +1,30 @@ +{ lib, ... }: +{ + provider.google = { + region = lib.tfRef "var.gcp_region"; + project = lib.tfRef "var.gcp_project"; + }; + + terraform = { + backend.gcs = { + bucket = "fcuny-infra-tofu-state"; + prefix = "admin"; + }; + required_providers = { + google = { + source = "hashicorp/google"; + }; + cloudflare = { + source = "cloudflare/cloudflare"; + }; + digitalocean = { + source = "digitalocean/digitalocean"; + version = "~> 2.0"; + }; + random = { + source = "hashicorp/random"; + version = "~> 3.1"; + }; + }; + }; +} diff --git a/terraform/admin/default.nix b/terraform/admin/default.nix new file mode 100644 index 0000000..0cbbe12 --- /dev/null +++ b/terraform/admin/default.nix @@ -0,0 +1,9 @@ +{ + imports = [ + ./backups.nix + ./base.nix + ./dns.nix + ./droplet-proxy.nix + ./variables.nix + ]; +} diff --git a/terraform/admin/dns.nix b/terraform/admin/dns.nix new file mode 100644 index 0000000..eeddfd5 --- /dev/null +++ b/terraform/admin/dns.nix @@ -0,0 +1,117 @@ +{ lib, ... }: +let + zoneId = lib.tfRef "var.cloudflare_zone_id"; + primaryIPv4 = "165.232.158.110"; + domain = "fcuny.net"; + + # GitHub Pages IP addresses for root domain + githubPagesIPs = [ + "185.199.108.153" + "185.199.110.153" + "185.199.109.153" + "185.199.111.153" + ]; + + mkARecord = name: content: ttl: { + inherit name content ttl; + type = "A"; + proxied = false; + zone_id = zoneId; + }; + + mkCNAMERecord = name: content: ttl: { + inherit name content ttl; + type = "CNAME"; + proxied = false; + zone_id = zoneId; + }; + + mkMXRecord = name: content: priority: { + inherit name content priority; + type = "MX"; + proxied = false; + ttl = 1; + zone_id = zoneId; + }; + + mkSRVRecord = name: port: priority: target: weight: { + inherit name priority; + type = "SRV"; + proxied = false; + ttl = 1; + zone_id = zoneId; + data = { + inherit + port + priority + target + weight + ; + }; + }; + + mkTXTRecord = name: content: { + inherit name content; + type = "TXT"; + proxied = false; + ttl = 1; + zone_id = zoneId; + }; + + mkMultipleARecords = + baseName: ips: + lib.listToAttrs ( + lib.imap0 (i: ip: { + name = "${baseName}_${toString i}"; + value = mkARecord domain ip 1; + }) ips + ); + + dkimRecords = lib.listToAttrs ( + lib.imap1 + (i: _: { + name = "cname_dkim_${toString (i - 1)}"; + value = mkCNAMERecord "fm${toString i}._domainkey" "fm${toString i}.${domain}.dkim.fmhosted.com" 60; + }) + [ + 1 + 2 + 3 + ] + ); + + subdomainARecords = { + cname_code = mkARecord "code.${domain}" primaryIPv4 1; + cname_go = mkARecord "go.${domain}" primaryIPv4 1; + cname_id = mkARecord "id.${domain}" primaryIPv4 1; + }; + + mxRecords = { + mx_0 = mkMXRecord domain "in1-smtp.messagingengine.com" 10; + mx_1 = mkMXRecord domain "in2-smtp.messagingengine.com" 20; + }; + + srvRecords = { + srv_caldavs = mkSRVRecord "_caldavs._tcp" 443 0 "caldav.fastmail.com" 1; + srv_caldav = mkSRVRecord "_caldav._tcp" 0 0 "." 0; + srv_carddavs = mkSRVRecord "_carddavs._tcp" 443 0 "carddav.fastmail.com" 1; + srv_carddav = mkSRVRecord "_carddav._tcp" 0 0 "." 0; + srv_imaps = mkSRVRecord "_imaps._tcp" 993 0 "imap.fastmail.com" 1; + srv_imap = mkSRVRecord "_imap._tcp" 0 0 "." 0; + srv_smtp = mkSRVRecord "_submission._tcp" 587 0 "smtp.fastmail.com" 1; + }; + + txtRecords = { + txt_spf = mkTXTRecord domain "\"v=spf1 include:spf.messagingengine.com ?all\""; + }; + +in +{ + resource.cloudflare_dns_record = + (mkMultipleARecords "cname_root" githubPagesIPs) + // subdomainARecords + // dkimRecords + // mxRecords + // srvRecords + // txtRecords; +} diff --git a/terraform/admin/droplet-proxy.nix b/terraform/admin/droplet-proxy.nix new file mode 100644 index 0000000..51ad138 --- /dev/null +++ b/terraform/admin/droplet-proxy.nix @@ -0,0 +1,89 @@ +{ lib, pkgs, ... }: +let + serverSize = "s-2vcpu-2gb"; + + extraFilesScript = pkgs.writeShellScript "extra-files-script" '' + #!/usr/bin/env bash + set -euo pipefail + + mkdir -p etc/ssh/ + + if [ -n "''${DO_SSH_HOSTKEY:-}" ]; then + echo "Setting up SSH host key from environment" + echo "$DO_SSH_HOSTKEY" | base64 -d > etc/ssh/ssh_host_ed25519_key + chmod 0600 etc/ssh/ssh_host_ed25519_key + else + echo "Warning: DO_SSH_HOSTKEY environment variable not set" + fi + ''; + +in +{ + provider.digitalocean = { + # Token will be read from DIGITALOCEAN_TOKEN environment variable + }; + + resource = { + # Random string for unique naming + random_string.host = { + length = 6; + special = false; + upper = false; + }; + + digitalocean_ssh_key.default = { + name = "nixos-anywhere-\${random_string.host.result}"; + public_key = lib.tfRef "var.digitalocean_public_key"; + }; + + digitalocean_droplet.nixos = { + name = "nixos-\${random_string.host.result}"; + image = "ubuntu-24-04-x64"; # Bootstrap image + size = serverSize; + region = lib.tfRef "var.digitalocean_region"; + ssh_keys = [ "\${digitalocean_ssh_key.default.id}" ]; + tags = [ + "nixos" + "infrastructure" + ]; + }; + }; + + module = { + nixos-system-build = { + source = "github.com/nix-community/nixos-anywhere//terraform/nix-build"; + attribute = ".#nixosConfigurations.do-rproxy.config.system.build.toplevel"; + }; + + nixos-disko = { + source = "github.com/nix-community/nixos-anywhere//terraform/nix-build"; + attribute = ".#nixosConfigurations.do-rproxy.config.system.build.diskoScript"; + }; + + nixos-install = { + source = "github.com/nix-community/nixos-anywhere//terraform/install"; + nixos_system = "\${module.nixos-system-build.result.out}"; + nixos_partitioner = "\${module.nixos-disko.result.out}"; + target_host = "\${digitalocean_droplet.nixos.ipv4_address}"; + build_on_remote = true; + extra_files_script = toString extraFilesScript; + }; + }; + + output = { + server_ip = { + description = "IP address of the NixOS server"; + value = "\${digitalocean_droplet.nixos.ipv4_address}"; + }; + + ssh_command = { + description = "SSH command to connect to the server"; + value = "ssh root@\${digitalocean_droplet.nixos.ipv4_address}"; + }; + + server_name = { + description = "Name of the created server"; + value = "\${digitalocean_droplet.nixos.name}"; + }; + }; +} diff --git a/terraform/admin/variables.nix b/terraform/admin/variables.nix new file mode 100644 index 0000000..0c795dd --- /dev/null +++ b/terraform/admin/variables.nix @@ -0,0 +1,29 @@ +{ + variable = { + gcp_region = { + description = "GCP region"; + type = "string"; + default = "us-west1"; + }; + gcp_project = { + description = "GCP project"; + type = "string"; + default = "fcuny-infra"; + }; + cloudflare_zone_id = { + description = "cloudflare zone ID"; + type = "string"; + default = "6878e48b5cb81c7d789040632153719d"; + }; + digitalocean_region = { + description = "DigitalOcean region"; + type = "string"; + default = "SFO3"; + }; + digitalocean_public_key = { + description = "SSH public key"; + type = "string"; + default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINBkozy+X96u5ciX766bJ/AyQ3xm1tXZTIr5+4PVFZFi"; + }; + }; +} -- cgit v1.2.3