From e27919fe1800a11c693a08adefc1854d16c46c3e Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 29 Jul 2026 21:07:33 +0900 Subject: [PATCH 1/2] Add vault.rubyci.org Fastly service for chkbuild logs rubyci.org currently links to build logs on the public rubyci S3 bucket directly, so every fetch including crawler traffic hits S3 request and egress billing. Fronting the bucket with Fastly caches the immutable timestamped logs for a year, keeps overwritten keys like cur/ on a short TTL, and gives us edge logging and UA control. DNS records are staged as a TODO because certificate issuance needs the ACME challenge from terraform output first. Co-Authored-By: Claude Fable 5 --- cdn/README.md | 2 ++ cdn/vault_rubyci.tf | 74 ++++++++++++++++++++++++++++++++++++++++ cdn/vcl/vault_rubyci.vcl | 56 ++++++++++++++++++++++++++++++ dns/dnsconfig.js | 4 +++ 4 files changed, 136 insertions(+) create mode 100644 cdn/vault_rubyci.tf create mode 100644 cdn/vcl/vault_rubyci.vcl diff --git a/cdn/README.md b/cdn/README.md index 8d8ea8c..64ae9cb 100644 --- a/cdn/README.md +++ b/cdn/README.md @@ -4,6 +4,8 @@ Terraform configuration for the Fastly services of ruby-lang.org. Each service l The `ftp` service has been dormant since 2018 and is kept with `activate = false`. `ftp.ruby-lang.org` is actually served by the `cache` service. +The `vault.rubyci.org` service fronts the public `rubyci` S3 bucket (chkbuild logs). Bootstrap order: `terraform apply`, add the ACME challenge CNAME from `terraform output vault_rubyci_managed_dns_challenges` to `dns/dnsconfig.js` and push it, wait for certificate issuance, then add the `vault` CNAME (see the TODO in `dnsconfig.js`). Switching rubyci.org log links over to vault.rubyci.org happens in the ruby/rubyci repository. + ## Usage Credentials come from `~/.config/fastly/token.sh`, which must export `FASTLY_API_KEY` (global scope token), `TF_VAR_datadog_token`, `TF_VAR_logging_s3_access_key`, and `TF_VAR_logging_s3_secret_key`. The state backend also needs AWS credentials that can read and write `s3://ruby-lang-terraform-state`, supplied through any standard AWS mechanism such as `AWS_PROFILE` or `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`. diff --git a/cdn/vault_rubyci.tf b/cdn/vault_rubyci.tf new file mode 100644 index 0000000..82ae72f --- /dev/null +++ b/cdn/vault_rubyci.tf @@ -0,0 +1,74 @@ +# Serves chkbuild logs from the public rubyci S3 bucket (ap-northeast-1) +# so that rubyci.org can link to vault.rubyci.org instead of S3 directly. +resource "fastly_service_vcl" "vault_rubyci" { + activate = true + stage = false + default_ttl = 300 + http3 = true + name = "vault.rubyci.org" + stale_if_error = true + stale_if_error_ttl = 43200 + + backend { + address = "rubyci.s3.ap-northeast-1.amazonaws.com" + auto_loadbalance = false + between_bytes_timeout = 10000 + connect_timeout = 1000 + error_threshold = 0 + first_byte_timeout = 15000 + keepalive_time = 0 + max_conn = 200 + max_lifetime = 0 + max_use = 0 + name = "rubyci S3 bucket" + override_host = "rubyci.s3.ap-northeast-1.amazonaws.com" + port = 443 + prefer_ipv6 = false + shield = "tyo-tokyo-jp" + ssl_cert_hostname = "rubyci.s3.ap-northeast-1.amazonaws.com" + ssl_check_cert = true + ssl_sni_hostname = "rubyci.s3.ap-northeast-1.amazonaws.com" + use_ssl = true + weight = 100 + } + + domain { + name = "vault.rubyci.org" + } + + logging_datadog { + format = file("${path.module}/logging/datadog_format.json") + format_version = 2 + name = "Datadog" + processing_region = "none" + region = "AP1" + token = var.datadog_token + } + + request_setting { + bypass_busy_wait = false + force_miss = false + force_ssl = true + max_stale_age = 0 + name = "Force TLS" + timer_support = false + xff = "append" + } + + vcl { + content = file("${path.module}/vcl/vault_rubyci.vcl") + main = true + name = "default" + } +} + +resource "fastly_tls_subscription" "vault_rubyci" { + domains = [one([for d in fastly_service_vcl.vault_rubyci.domain : d.name])] + certificate_authority = "certainly" +} + +# CNAME records to add in dns/dnsconfig.js: the ACME challenge for issuance, +# then the domain itself pointing at the Fastly TLS endpoint. +output "vault_rubyci_managed_dns_challenges" { + value = fastly_tls_subscription.vault_rubyci.managed_dns_challenges +} diff --git a/cdn/vcl/vault_rubyci.vcl b/cdn/vcl/vault_rubyci.vcl new file mode 100644 index 0000000..cd06b61 --- /dev/null +++ b/cdn/vcl/vault_rubyci.vcl @@ -0,0 +1,56 @@ +sub vcl_recv { +#FASTLY recv + + if (req.request == "FASTLYPURGE") { + set req.http.Fastly-Purge-Requires-Auth = "1"; + } + + if (req.request != "HEAD" && req.request != "GET" && req.request != "FASTLYPURGE") { + return(pass); + } + + return(lookup); +} + +sub vcl_fetch { +#FASTLY fetch + + set beresp.stale_while_revalidate = 60s; + + if (req.url ~ "/log/\d{8}T\d{6}Z\.") { + # Timestamped chkbuild logs are immutable + set beresp.ttl = 31536000s; + } else { + # cur/, summary pages and other keys are overwritten by chkbuild + set beresp.ttl = 300s; + } + + return(deliver); +} + +sub vcl_hit { +#FASTLY hit + + if (!obj.cacheable) { + return(pass); + } + return(deliver); +} + +sub vcl_miss { +#FASTLY miss + return(fetch); +} + +sub vcl_deliver { +#FASTLY deliver + return(deliver); +} + +sub vcl_error { +#FASTLY error +} + +sub vcl_pass { +#FASTLY pass +} diff --git a/dns/dnsconfig.js b/dns/dnsconfig.js index 583c8ba..f26f939 100644 --- a/dns/dnsconfig.js +++ b/dns/dnsconfig.js @@ -33,5 +33,9 @@ D("rubyci.org", REG_CHANGEME, // Cloudflare flattens this apex CNAME; dnscontrol expresses it as ALIAS. ALIAS("@", "rubyci.org.herokudns.com."), CNAME("www", "www.rubyci.org.herokudns.com."), + // TODO(vault): after `terraform apply` in cdn/, add the ACME challenge + // CNAME from `terraform output vault_rubyci_managed_dns_challenges`, + // wait for the certificate, then uncomment: + // CNAME("vault", "j.sni.global.fastly.net."), ); From 75ae1ba992b103b967526d156f1d9e7492e342d8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 30 Jul 2026 07:27:02 +0900 Subject: [PATCH 2/2] Name the chkbuild log service logs.rubyci.org and finish bootstrap vault implied secret storage; these are public read-only build logs, so logs says what it serves. The TLS subscription needs an explicit configuration_id because the account has no default configuration. Picking "HTTP/3 & TLS v1.3" keeps the negotiated protocols in line with http3 on the service, and it is what decides the m.sni.global.fastly.net CNAME target. Co-Authored-By: Claude Fable 5 --- cdn/README.md | 6 +++++- cdn/{vault_rubyci.tf => logs_rubyci.tf} | 21 +++++++++++-------- cdn/vcl/{vault_rubyci.vcl => logs_rubyci.vcl} | 0 dns/dnsconfig.js | 9 ++++---- 4 files changed, 22 insertions(+), 14 deletions(-) rename cdn/{vault_rubyci.tf => logs_rubyci.tf} (72%) rename cdn/vcl/{vault_rubyci.vcl => logs_rubyci.vcl} (100%) diff --git a/cdn/README.md b/cdn/README.md index 64ae9cb..26748e1 100644 --- a/cdn/README.md +++ b/cdn/README.md @@ -4,7 +4,11 @@ Terraform configuration for the Fastly services of ruby-lang.org. Each service l The `ftp` service has been dormant since 2018 and is kept with `activate = false`. `ftp.ruby-lang.org` is actually served by the `cache` service. -The `vault.rubyci.org` service fronts the public `rubyci` S3 bucket (chkbuild logs). Bootstrap order: `terraform apply`, add the ACME challenge CNAME from `terraform output vault_rubyci_managed_dns_challenges` to `dns/dnsconfig.js` and push it, wait for certificate issuance, then add the `vault` CNAME (see the TODO in `dnsconfig.js`). Switching rubyci.org log links over to vault.rubyci.org happens in the ruby/rubyci repository. +The `logs.rubyci.org` service fronts the public `rubyci` S3 bucket (chkbuild logs). Switching rubyci.org log links over to it happens in the ruby/rubyci repository. + +Bootstrapping a domain on a new TLS subscription takes three steps, because the ACME challenge has to resolve before Fastly will issue: `terraform apply`, then add the challenge CNAME from `terraform output logs_rubyci_managed_dns_challenges` to `dns/dnsconfig.js` and push it, then add the domain CNAME once the subscription reaches `issued`. The challenge record stays in `dnsconfig.js` afterwards, since renewal reuses it. + +Two things about the Fastly API token. It needs the TLS management permission, which is only settable when the automation token is created; without it `fastly_tls_subscription` fails with a bare `403 - Forbidden`. And this account has no default TLS configuration, so `configuration_id` is required, which also decides the CNAME target the domain points at. ## Usage diff --git a/cdn/vault_rubyci.tf b/cdn/logs_rubyci.tf similarity index 72% rename from cdn/vault_rubyci.tf rename to cdn/logs_rubyci.tf index 82ae72f..eb2bf7e 100644 --- a/cdn/vault_rubyci.tf +++ b/cdn/logs_rubyci.tf @@ -1,11 +1,11 @@ # Serves chkbuild logs from the public rubyci S3 bucket (ap-northeast-1) -# so that rubyci.org can link to vault.rubyci.org instead of S3 directly. -resource "fastly_service_vcl" "vault_rubyci" { +# so that rubyci.org can link to logs.rubyci.org instead of S3 directly. +resource "fastly_service_vcl" "logs_rubyci" { activate = true stage = false default_ttl = 300 http3 = true - name = "vault.rubyci.org" + name = "logs.rubyci.org" stale_if_error = true stale_if_error_ttl = 43200 @@ -33,7 +33,7 @@ resource "fastly_service_vcl" "vault_rubyci" { } domain { - name = "vault.rubyci.org" + name = "logs.rubyci.org" } logging_datadog { @@ -56,19 +56,22 @@ resource "fastly_service_vcl" "vault_rubyci" { } vcl { - content = file("${path.module}/vcl/vault_rubyci.vcl") + content = file("${path.module}/vcl/logs_rubyci.vcl") main = true name = "default" } } -resource "fastly_tls_subscription" "vault_rubyci" { - domains = [one([for d in fastly_service_vcl.vault_rubyci.domain : d.name])] +resource "fastly_tls_subscription" "logs_rubyci" { + domains = [one([for d in fastly_service_vcl.logs_rubyci.domain : d.name])] certificate_authority = "certainly" + # "HTTP/3 & TLS v1.3". The account has no default TLS configuration, and the + # older v1.2 ones used by ruby-lang.org do not advertise http/3. + configuration_id = "cyAx0f8WAbapyNAT4TVOgw" } # CNAME records to add in dns/dnsconfig.js: the ACME challenge for issuance, # then the domain itself pointing at the Fastly TLS endpoint. -output "vault_rubyci_managed_dns_challenges" { - value = fastly_tls_subscription.vault_rubyci.managed_dns_challenges +output "logs_rubyci_managed_dns_challenges" { + value = fastly_tls_subscription.logs_rubyci.managed_dns_challenges } diff --git a/cdn/vcl/vault_rubyci.vcl b/cdn/vcl/logs_rubyci.vcl similarity index 100% rename from cdn/vcl/vault_rubyci.vcl rename to cdn/vcl/logs_rubyci.vcl diff --git a/dns/dnsconfig.js b/dns/dnsconfig.js index f26f939..ed38ce7 100644 --- a/dns/dnsconfig.js +++ b/dns/dnsconfig.js @@ -33,9 +33,10 @@ D("rubyci.org", REG_CHANGEME, // Cloudflare flattens this apex CNAME; dnscontrol expresses it as ALIAS. ALIAS("@", "rubyci.org.herokudns.com."), CNAME("www", "www.rubyci.org.herokudns.com."), - // TODO(vault): after `terraform apply` in cdn/, add the ACME challenge - // CNAME from `terraform output vault_rubyci_managed_dns_challenges`, - // wait for the certificate, then uncomment: - // CNAME("vault", "j.sni.global.fastly.net."), + // chkbuild logs, served by the logs.rubyci.org Fastly service in cdn/. + // The _acme-challenge record renews the Certainly certificate; removing it + // breaks renewal. + CNAME("logs", "m.sni.global.fastly.net."), + CNAME("_acme-challenge.logs", "d867b4erl69vx2y93y.fastly-validations.com."), );