Files
bible/infra/railway/main.tf
T
2026-09-19 13:17:36 -04:00

143 lines
3.9 KiB
Terraform

# 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-2.${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
}
}
}