aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-08-31 13:33:54 -0700
committerFranck Cuny <franck@fcuny.net>2025-08-31 13:33:54 -0700
commit145e1dab68caf3f57c53820c6359bef83a5ce52a (patch)
tree592546ad50121b32f386f532e3be8f75cb521d54
parentadd terranix (diff)
downloadinfra-145e1dab68caf3f57c53820c6359bef83a5ce52a.tar.gz
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.
-rw-r--r--.envrc2
-rw-r--r--.envrc.local.template (renamed from infra/tf/.envrc.local.template)0
-rw-r--r--.gitignore6
-rw-r--r--docs/dns.org3
-rw-r--r--flake.nix2
-rw-r--r--flake/overlays.nix23
-rw-r--r--flake/terraform.nix40
-rw-r--r--infra/tf/.envrc12
-rw-r--r--infra/tf/.gitignore1
-rw-r--r--infra/tf/backups-bucket/main.tf43
-rw-r--r--infra/tf/backups-bucket/outputs.tf24
-rw-r--r--infra/tf/backups-bucket/terraform.tfvars27
-rw-r--r--infra/tf/backups-bucket/variables.tf73
-rw-r--r--infra/tf/cloudflare-dns/main.tf24
-rw-r--r--infra/tf/cloudflare-dns/records.tf223
-rw-r--r--infra/tf/cloudflare-dns/terraform.tfvars3
-rw-r--r--infra/tf/cloudflare-dns/variables.tf11
-rwxr-xr-xinfra/tf/do-vm/extra-files-script.sh8
-rw-r--r--infra/tf/do-vm/main.tf82
-rw-r--r--infra/tf/do-vm/outputs.tf9
-rw-r--r--infra/tf/do-vm/terraform.tfvars2
-rw-r--r--infra/tf/flake-module.nix30
-rw-r--r--infra/tf/scripts.nix58
-rw-r--r--scripts/common.nix25
-rw-r--r--terraform/admin/backups.nix28
-rw-r--r--terraform/admin/base.nix30
-rw-r--r--terraform/admin/default.nix9
-rw-r--r--terraform/admin/dns.nix117
-rw-r--r--terraform/admin/droplet-proxy.nix89
-rw-r--r--terraform/admin/variables.nix29
30 files changed, 397 insertions, 636 deletions
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/infra/tf/.envrc.local.template b/.envrc.local.template
index 82ebf5f..82ebf5f 100644
--- a/infra/tf/.envrc.local.template
+++ b/.envrc.local.template
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/.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";
+ };
+ };
+}