aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-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";
+ };
+ };
+}