Infra
This commit is contained in:
1 parent
ea1afaf833
commit
e0db317146
12 files changed
+444
-56
No files matched your search
@@ -0,0 +1,75 @@
|
||||
# Infrastructure
|
||||
|
||||
The site on Railway: one project, `bible`, with a `production` and a `dev`
|
||||
environment, and one service, `site`, that runs the repo's `Dockerfile` in
|
||||
Virginia (`us-east4-eqdc4a`). Railway holds the certificates; `server.py` does
|
||||
the routing there as it does locally. DNS stays on Cloudflare.
|
||||
|
||||
| env | Railway environment | URL |
|
||||
|------|---------------------|-------------------------------|
|
||||
| prod | `production` | https://bible-2.aerlinn.net |
|
||||
| dev | `dev` | https://bible-dev.aerlinn.net |
|
||||
|
||||
```
|
||||
railway/ project, environments, service, custom domains, and their
|
||||
CNAME + TXT records on Cloudflare
|
||||
railway-cli/ Railway's CLI as a pinned image (theirs is x86-only)
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
Nothing to install but Docker. Terraform and Railway's CLI are services in
|
||||
`compose.yaml` (profile `tools`), reading tokens from `.env`. From the repo
|
||||
root:
|
||||
|
||||
```
|
||||
docker compose run --rm terraform -chdir=infra/railway plan
|
||||
docker compose run --rm railway status
|
||||
```
|
||||
|
||||
Commit `.terraform.lock.hcl`; it pins the provider builds.
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. **Tokens.** A Railway workspace token (account settings -> Tokens, with the
|
||||
workspace selected; a project token cannot make projects) and the
|
||||
Cloudflare token. `cp .env.example .env` and fill both in.
|
||||
|
||||
2. **Project and domains.**
|
||||
```
|
||||
docker compose run --rm terraform -chdir=infra/railway init
|
||||
docker compose run --rm terraform -chdir=infra/railway apply
|
||||
```
|
||||
|
||||
3. **Deploy**, below. Railway issues each certificate once it sees the CNAME;
|
||||
the first HTTPS requests can fail for a minute or two until then.
|
||||
|
||||
## Deploying
|
||||
|
||||
```
|
||||
python3 utils/compile_bible.py # only when source/ changes
|
||||
./deploy.sh dev
|
||||
./deploy.sh prod
|
||||
```
|
||||
|
||||
`deploy.sh` copies what the Dockerfile needs into `.build/` and uploads that
|
||||
with `railway up`; Railway builds it and switches over when the new container
|
||||
is running. Not the repo itself: the CLI skips gitignored paths, which
|
||||
includes `bible/`, and telling it not to would send `.env` and `source/` too.
|
||||
The build logs stream to the terminal, and the dashboard has them as well.
|
||||
|
||||
Terraform never deploys, and the provider cannot set anything Railway calls
|
||||
config-as-code (healthchecks, start commands); Railway's defaults do.
|
||||
|
||||
## State
|
||||
|
||||
Local: `infra/railway/terraform.tfstate`, gitignored. Back it up. If it is
|
||||
lost, nothing on Railway goes away; `terraform import` each resource back
|
||||
(IDs from the dashboard URLs; the Cloudflare records as `<zone_id>/<id>`).
|
||||
|
||||
## Cost
|
||||
|
||||
Hobby plan: $5 a month, which includes $5 of usage. The service idles at a
|
||||
few tens of MB and next to no CPU in each environment ($10 per GB-month of
|
||||
memory, $20 per vCPU-month), so both fit in the included usage. Egress is
|
||||
$0.05/GB, about $0.0001 per new reader (the whole library, ~2.2 MB).
|
||||
@@ -0,0 +1,17 @@
|
||||
# Railway's CLI, pinned. Their own image is x86-only; the release binaries are
|
||||
# static, so this just fetches the one for the platform being built.
|
||||
FROM alpine:3.22
|
||||
|
||||
ARG VERSION=5.57.12
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN case "$TARGETARCH" in \
|
||||
arm64) triple=aarch64-unknown-linux-musl ;; \
|
||||
amd64) triple=x86_64-unknown-linux-musl ;; \
|
||||
*) echo "unsupported: $TARGETARCH" >&2; exit 1 ;; \
|
||||
esac \
|
||||
&& wget -qO- "https://github.com/railwayapp/cli/releases/download/v$VERSION/railway-v$VERSION-$triple.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin railway \
|
||||
&& railway --version
|
||||
|
||||
ENTRYPOINT ["railway"]
|
||||
Generated
+41
@@ -0,0 +1,41 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/cloudflare/cloudflare" {
|
||||
version = "5.25.0"
|
||||
constraints = "~> 5.25"
|
||||
hashes = [
|
||||
"h1:uOE/ctTNSIjD6mEvrKz81eLB7hVfAAa8TsMbDhJlHts=",
|
||||
"zh:403d477c0e4c05a9f9886ef7e750dcc3488be7cd2210207e98b4a731bc499159",
|
||||
"zh:4a2a4659c3273ddb1437a34865eac21823a117557b050786ba510ac5f34047cf",
|
||||
"zh:6e4a2f6ed105b5353c7b56962d00f923873dd68af6509b9b9f595057edba1915",
|
||||
"zh:aadd2051c1eb01e7b518d4808de7f5192b2ecbc841d70924785ee3385a889b9e",
|
||||
"zh:ab0322bcb0d4e465f8603b39f7e743c836b22ae775c03ed677ad999a6a1b8bf4",
|
||||
"zh:c47e38911c92e0b2ffe7be1d36d96b79dd2af79bc61745d6afe5a730cae9f287",
|
||||
"zh:df3f13ae57104ce2f7619cc6c050ab90a5a3299470cdbd2ab8dd3809b84edcb8",
|
||||
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
||||
"zh:fb7de9d22b02367036c146bd683c8968ec4bbb56aca3185fa84146d118a2bf96",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/terraform-community-providers/railway" {
|
||||
version = "0.6.2"
|
||||
constraints = "~> 0.6.2"
|
||||
hashes = [
|
||||
"h1:4Az1FULORPb4P5ZtZ6D/tR6PcqE1AY+hCJW+sJuXjxs=",
|
||||
"zh:2165a426b2e346c2debb05517e3993f87253ddcdf145d94401ea2ab9178d35ab",
|
||||
"zh:2f3f0f3423564c88ada05238f136034fc35634fc7978efc503b984ba35e83724",
|
||||
"zh:3bb0618cddccca0d0c690044f7338e4cd103b45114ffbcca8769e864a1b2554b",
|
||||
"zh:50fd5dc5cf749d8ecd10e5e4ee5cef04b541beb522aab6d4e1845ee812e0c088",
|
||||
"zh:6a52b2c60121a695151d6a91e61c3fecabc0a40bf75d6b530d828eef0dc0619d",
|
||||
"zh:7356f98cef110a5ca1a261fddde4b53c74b6a22848bdca642e2094f7708b3626",
|
||||
"zh:7b47792310e502d0a0b4427133b161114dce2323bc61bf40dc6c4d67858b7629",
|
||||
"zh:7e8740d239d76a869e0d8eb5dc6c1ec62459603ea6a057333006f8de1c12e2f0",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:99d7a179fe16211beb790e19a8f65d570a23ed4f6b752b5bd6d073195fc4cf9d",
|
||||
"zh:9f31f26d0d93067fca796a718a9b39ebfb1fce4d24e382c215f33e0dde7b3420",
|
||||
"zh:adb29c332488b29cc483f8cac00a0cc54cacd4cfe5bbd818a55a1c1b4394ebcd",
|
||||
"zh:d16148e673900550273cbbb12f7dbd7f96ec0cfaa863ec93ec5b108258415637",
|
||||
"zh:da40431bcd65e85bec7f301611cad5b4b772dae250e9622556072b6a3a7503b6",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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 local (terraform.tfstate here, gitignored).
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.10"
|
||||
required_providers {
|
||||
railway = { source = "terraform-community-providers/railway", version = "~> 0.6.2" }
|
||||
cloudflare = { source = "cloudflare/cloudflare", version = "~> 5.25" }
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user