aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-08-09 12:41:52 -0700
committerFranck Cuny <franck@fcuny.net>2025-08-09 12:41:52 -0700
commit97a7718d1070b02068b7a9ccd76ab5a7d5703031 (patch)
tree95c23d793e377ef7142cc88bec83025f53cc55dd
parentchange emacs theme and simplify whitespace configuration (diff)
downloadinfra-97a7718d1070b02068b7a9ccd76ab5a7d5703031.tar.gz
write the terraform configuration in HCL
This will become more tricky over time. Using HCL is not as elegant and forces me to learn yet another DSL, but this will be easier to debug if I'm stuck with something I don't know how to do.
-rw-r--r--infra/tf/.envrc.local.template4
-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/backups.nix42
-rw-r--r--infra/tf/cloudflare-dns/main.tf24
-rw-r--r--infra/tf/cloudflare-dns/records.tf196
-rw-r--r--infra/tf/cloudflare-dns/terraform.tfvars3
-rw-r--r--infra/tf/cloudflare-dns/variables.tf11
-rw-r--r--infra/tf/dns.nix138
-rw-r--r--infra/tf/flake-module.nix17
13 files changed, 423 insertions, 180 deletions
diff --git a/infra/tf/.envrc.local.template b/infra/tf/.envrc.local.template
new file mode 100644
index 0000000..82ebf5f
--- /dev/null
+++ b/infra/tf/.envrc.local.template
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+# stored in 1password
+export CLOUDFLARE_API_TOKEN=...
diff --git a/infra/tf/.gitignore b/infra/tf/.gitignore
new file mode 100644
index 0000000..cff6f1b
--- /dev/null
+++ b/infra/tf/.gitignore
@@ -0,0 +1 @@
+/.envrc.local
diff --git a/infra/tf/backups-bucket/main.tf b/infra/tf/backups-bucket/main.tf
new file mode 100644
index 0000000..a86e582
--- /dev/null
+++ b/infra/tf/backups-bucket/main.tf
@@ -0,0 +1,43 @@
+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
new file mode 100644
index 0000000..3ca60c5
--- /dev/null
+++ b/infra/tf/backups-bucket/outputs.tf
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..1d4f2c4
--- /dev/null
+++ b/infra/tf/backups-bucket/terraform.tfvars
@@ -0,0 +1,27 @@
+# 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
new file mode 100644
index 0000000..ffdc1a6
--- /dev/null
+++ b/infra/tf/backups-bucket/variables.tf
@@ -0,0 +1,73 @@
+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/backups.nix b/infra/tf/backups.nix
deleted file mode 100644
index e76ed2e..0000000
--- a/infra/tf/backups.nix
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- pkgs,
-}:
-pkgs.writeTextFile {
- name = "backups.tf.json";
- text = builtins.toJSON ([
- {
- terraform = {
- backend = {
- gcs = {
- bucket = "fcuny-infra-tofu-state";
- prefix = "backups";
- };
- };
- };
- }
- {
- provider = {
- google = [
- {
- project = "fcuny-infra";
- region = "us-west1";
- }
- ];
- };
- }
- {
- resource = {
- google_storage_bucket = {
- "backups" = {
- name = "fcuny-infra-backups";
- location = "us-west1";
- uniform_bucket_level_access = true;
- force_destroy = true;
- public_access_prevention = "enforced";
- storage_class = "NEARLINE";
- };
- };
- };
- }
- ]);
-}
diff --git a/infra/tf/cloudflare-dns/main.tf b/infra/tf/cloudflare-dns/main.tf
new file mode 100644
index 0000000..30442e6
--- /dev/null
+++ b/infra/tf/cloudflare-dns/main.tf
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000..b1543d1
--- /dev/null
+++ b/infra/tf/cloudflare-dns/records.tf
@@ -0,0 +1,196 @@
+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_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
new file mode 100644
index 0000000..5a88de4
--- /dev/null
+++ b/infra/tf/cloudflare-dns/terraform.tfvars
@@ -0,0 +1,3 @@
+# 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
new file mode 100644
index 0000000..24a4a35
--- /dev/null
+++ b/infra/tf/cloudflare-dns/variables.tf
@@ -0,0 +1,11 @@
+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/dns.nix b/infra/tf/dns.nix
deleted file mode 100644
index df0ed65..0000000
--- a/infra/tf/dns.nix
+++ /dev/null
@@ -1,138 +0,0 @@
-{
- pkgs,
-}:
-let
- zoneId = "6878e48b5cb81c7d789040632153719d";
- zoneName = "fcuny.net";
-
- # Helper function to create DNS records with common fields
- mkRecord =
- type: name: content: extra:
- {
- inherit name type;
- zone_id = zoneId;
- ttl = 1;
- proxied = false;
- content = content;
- }
- // extra;
-
- # Helper for A records (typically proxied)
- mkARecord = name: ip: mkRecord "A" name ip { proxied = true; };
-
- # Helper for CNAME records
- mkCNAME = name: target: mkRecord "CNAME" name target { };
-
- # Helper for MX records
- mkMXRecord =
- priority: target:
- mkRecord "MX" zoneName target {
- inherit priority;
- };
-
- # Helper for SRV records with data block
- mkSRVRecord = name: port: target: weight: priority: {
- inherit name;
- type = "SRV";
- zone_id = zoneId;
- ttl = 1;
- proxied = false;
- priority = priority;
- data = {
- inherit
- port
- target
- weight
- priority
- ;
- };
- };
-
- # Helper for TXT records
- mkTXTRecord = name: content: mkRecord "TXT" name content { };
-
-in
-pkgs.writeTextFile {
- name = "cloudflare-dns.tf.json";
- text = builtins.toJSON ([
- {
- terraform = {
- required_providers = {
- cloudflare = {
- source = "cloudflare/cloudflare";
- version = "~> 4.0";
- };
- };
- backend = {
- gcs = {
- bucket = "fcuny-infra-tofu-state";
- prefix = "cloudflare-dns";
- };
- };
- };
- }
- {
- provider = {
- cloudflare = [ { } ];
- };
- }
- {
- # Use data source for existing zone instead of managing it
- data = {
- cloudflare_zone = {
- "main" = {
- name = zoneName;
- };
- };
- };
- }
- {
- resource = {
- cloudflare_record = {
- # A records for root domain
- "cname_root_0" = mkARecord zoneName "185.199.108.153";
- "cname_root_1" = mkARecord zoneName "185.199.110.153";
- "cname_root_2" = mkARecord zoneName "185.199.109.153";
- "cname_root_3" = mkARecord zoneName "185.199.111.153";
-
- # DKIM CNAME records
- "cname_dkim_0" = mkCNAME "fm1._domainkey" "fm1.fcuny.net.dkim.fmhosted.com" // {
- ttl = 60;
- };
- "cname_dkim_1" = mkCNAME "fm2._domainkey" "fm2.fcuny.net.dkim.fmhosted.com" // {
- ttl = 60;
- };
- "cname_dkim_2" = mkCNAME "fm3._domainkey" "fm3.fcuny.net.dkim.fmhosted.com" // {
- ttl = 60;
- };
-
- # Git subdomain via Cloudflare tunnel
- "cname_git" = mkCNAME "git" "b5d5071d-3c09-4379-9d6c-0684c478f151.cfargotunnel.com" // {
- proxied = true;
- };
-
- # MX records
- "mx_0" = mkMXRecord 10 "in1-smtp.messagingengine.com";
- "mx_1" = mkMXRecord 20 "in2-smtp.messagingengine.com";
-
- # SPF TXT record
- "txt_spf" = mkTXTRecord zoneName "v=spf1 include:spf.messagingengine.com ?all";
- };
- };
- }
- {
- resource = {
- cloudflare_record = {
- # SRV records for email services
- "srv_caldavs" = mkSRVRecord "_caldavs._tcp" 443 "caldav.fastmail.com" 1 0;
- "srv_caldav" = mkSRVRecord "_caldav._tcp" 0 "." 0 0;
- "srv_carddavs" = mkSRVRecord "_carddavs._tcp" 443 "carddav.fastmail.com" 1 0;
- "srv_carddav" = mkSRVRecord "_carddav._tcp" 0 "." 0 0;
- "srv_imaps" = mkSRVRecord "_imaps._tcp" 993 "imap.fastmail.com" 1 0;
- "srv_imap" = mkSRVRecord "_imap._tcp" 0 "." 0 0;
- "srv_smtp" = mkSRVRecord "_submission._tcp" 587 "smtp.fastmail.com" 1 0;
- };
- };
- }
- ]);
-}
diff --git a/infra/tf/flake-module.nix b/infra/tf/flake-module.nix
new file mode 100644
index 0000000..a386177
--- /dev/null
+++ b/infra/tf/flake-module.nix
@@ -0,0 +1,17 @@
+{
+ perSystem =
+ { pkgs, ... }:
+ {
+ devShells.terraform = pkgs.mkShellNoCC {
+ packages = with pkgs; [
+ google-cloud-sdk
+ (pkgs.opentofu.withPlugins (p: [
+ p.google
+ p.cloudflare
+ p.external
+ p.null
+ ]))
+ ];
+ };
+ };
+}