# The site on Railway: one project, with an environment each for prod and dev, # and one service that runs the repo's Dockerfile. server.py does the routing, # as it does locally; Railway holds the certificates. # # Terraform makes the project, the service and the domains, and puts their # records on Cloudflare. It never deploys: deploy.sh uploads the site with # Railway's CLI. State is in the tfstate bucket on Railway (infra/README.md). terraform { required_version = ">= 1.10" required_providers { railway = { source = "terraform-community-providers/railway", version = "~> 0.6.2" } cloudflare = { source = "cloudflare/cloudflare", version = "~> 5.25" } } # The tfstate project's bucket on Railway, which holds every project's state # under its own key. S3-compatible, not AWS: hence the skips. Its keys come # from TFSTATE_* in .env (compose.yaml passes them as AWS_*). backend "s3" { bucket = "tfstate-9p20m8qvmqf8gqk-z" key = "bible/terraform.tfstate" region = "auto" endpoints = { s3 = "https://t3.storageapi.dev" } use_lockfile = true skip_credentials_validation = true skip_region_validation = true skip_requesting_account_id = true skip_metadata_api_check = true } } # Reads RAILWAY_TOKEN (compose.yaml sets it from RAILWAY_API_TOKEN in .env): # a workspace token. provider "railway" {} # Reads CLOUDFLARE_API_TOKEN: DNS edit and zone read, on aerlinn.net only. provider "cloudflare" {} locals { zone = "aerlinn.net" # Railway Metal, Virginia. region = "us-east4-eqdc4a" } resource "railway_project" "bible" { name = "bible" description = "Offline Bible reader" default_environment = { name = "production" } } resource "railway_environment" "dev" { name = "dev" project_id = railway_project.bible.id } resource "railway_service" "site" { name = "site" project_id = railway_project.bible.id regions = [{ region = local.region num_replicas = 1 }] # Made after both environments, so it is in both. depends_on = [railway_environment.dev] } locals { environments = { production = { id = railway_project.bible.default_environment.id domain = "bible.${local.zone}" } dev = { id = railway_environment.dev.id domain = "bible-dev.${local.zone}" } } } resource "railway_custom_domain" "site" { for_each = local.environments domain = each.value.domain environment_id = each.value.id service_id = railway_service.site.id } data "cloudflare_zone" "site" { filter = { name = local.zone } } # DNS only, not proxied: Railway issues the certificate itself and has to see # the CNAME as it is. resource "cloudflare_dns_record" "site" { for_each = railway_custom_domain.site zone_id = data.cloudflare_zone.site.id name = each.value.domain type = "CNAME" content = each.value.dns_record_value ttl = 1 proxied = false comment = "terraform: railway bible ${each.key}" } # Railway's proof that the domain is ours. resource "cloudflare_dns_record" "verification" { for_each = railway_custom_domain.site zone_id = data.cloudflare_zone.site.id name = "${each.value.verification_host_label}.${each.value.zone}" type = "TXT" content = "\"${each.value.verification_record_value}\"" ttl = 1 comment = "terraform: railway bible ${each.key} verification" } output "project_id" { value = railway_project.bible.id } output "service" { value = railway_service.site.name } output "domains" { value = { for name, d in railway_custom_domain.site : name => { domain = d.domain host_label = d.host_label zone = d.zone target = d.dns_record_value verification_host = d.verification_host_label verification = d.verification_record_value } } }