Skip to content

Move docs-dev to S3 backends as the docs.ruby-lang.org migration canary - #72

Open
znz wants to merge 7 commits into
ruby:masterfrom
znz:docs-dev-s3-canary
Open

Move docs-dev to S3 backends as the docs.ruby-lang.org migration canary#72
znz wants to merge 7 commits into
ruby:masterfrom
znz:docs-dev-s3-canary

Conversation

@znz

@znz znz commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Canary for moving docs.ruby-lang.org off the docs-origin server onto S3 +
Fastly, on the docs-dev service (production docs.tf is untouched).

A targeted plan has been run and comes out exactly as intended —
Plan: 6 to add, 1 to change, 0 to destroy (the five bucket resources and
the dictionary items created, docs_dev updated in place, nothing destroyed).
Please apply with the real datadog_token: the local plan passed
-var datadog_token=dummy, which is why it shows the logging_datadog block
replaced; applying that literally would break the logging endpoint.

  • cdn/s3.tf: new public-read docs.r-l.o bucket (us-east-1 like
    ftp.r-l.o — everything is fetched through Fastly, so the cheapest tier wins
    over client proximity). Prefix layout and writers are documented in the
    file; write access is meant to be split per pipeline with IAM prefix
    conditions rather than per-bucket.
  • cdn/docs_dev.tf: three backends — the docs bucket (shielded iad-va-us,
    default_host/domain arrangement from cache.tf), the rubyci bucket for
    /capi/en/master/ (step 1 keeps Doxygen where it is; step 2 later moves it
    into the docs bucket), and docs-origin as the fallback for unflagged paths
    so prefixes can be moved one at a time. Routing is request_conditions on a
    header flag set before #FASTLY recv (assigning req.backend in VCL would
    bypass shielding, per the cache.tf comment). Adds the docs_versions
    dictionary (latest=4.0, master=4.1).
  • cdn/vcl/docs_dev.vcl: replaces the behavior-neutral boilerplate with
    the actual origin logic:
    • the five nginx redirect rules, unchanged semantics and order
    • 302 (temporary) for the unreleased version: /en/4.1/* and /ja/4.1/*
      go to the master alias until the release; keyed off the dictionary, so
      a release only updates dictionary values
    • query-string stripping (S3 would interpret them as API parameters)
    • trailing-slash redirect for extensionless directory-looking URLs, and
      index.html appended internally for trailing-slash URLs
    • /ja/latest//ja/master/ resolved internally via the dictionary (the
      bucket holds no symlink objects; client URLs stay on the alias)
    • Markdown content negotiation on /ja/ pages: Accept: text/markdown on
      an .html URL internally fetches the .md twin, with a restart fallback
      to .html where no twin exists (generated index pages, frozen RD
      versions); Accept is normalized to two values to keep Vary cacheable
    • Surrogate-Key / Cache-Control emitted at the edge with the existing
      purge-key scheme (alias and real version share one object, so its key
      carries both names)
    • S3 AccessDenied (public-read grants GetObject only) mapped to 404 with a
      short negative-cache TTL; x-amz-* headers stripped; 50x synthetic page

Testing (once content is synced to the bucket)

Against https://docs-rlo-dev.global.ssl.fastly.net/ (no DNS/TLS setup needed,
per the existing comment in docs_dev.tf). Minimum sync for testing:
rurema/generated-documents html/ja/4.0 → ja/4.0/, docs repo public/ →
root, one en version → en/<v>/.

  • / /ja/ /ja/latest/ /ja/latest/doc/index.html 200
  • /ja/latest/class/Array.html == /ja/4.0/class/Array.html
  • /ja/latest/class/Array.mdtext/markdown; charset=utf-8
  • curl -H 'Accept: text/markdown' /ja/latest/class/Array.html → the .md
    twin; same on /ja/latest/class/index.html → falls back to HTML
  • /ja/latest → 301 /ja/latest/
  • /en/trunk/x → 301, /en/3.4/doc/y → 301, /ja/2.8.0/z → 301 /ja/3.0/z
  • /ja/search/query:%E9%85%8D%E5%88%97/ → 301 /ja/search/?q=%E9%85%8D%E5%88%97
  • /en/4.1/anything → 302 /en/master/anything, /ja/4.1/x → 302 /ja/master/x
  • /capi/en/master/ serves Doxygen, /capi/en/master/sitemap.xml 200
  • a missing path is 404 (not 403); Fastly-Debug: 1 shows the Surrogate-Key
    and purging ja/4.0 / ja/latest works

Follow-ups (separate PRs)

  • GitHub Actions upload credentials: OIDC roles (id-token: write +
    aws-actions/configure-aws-credentials) with per-pipeline IAM prefix
    conditions, replacing the long-lived secrets currently used for rubyci
  • Deploy pipelines: gd → ja/, ruby/actions docs.yml → en/<v>/, docs repo
    public/ → root; one-off sync for frozen en versions and old ja versions
  • Adopting the proven shape in docs.tf, then retiring docs-origin

🤖 Generated with Claude Code

znz and others added 7 commits August 6, 2026 18:49
Add a public-read docs.r-l.o bucket (us-east-1, the cheapest tier; every
request reaches it through Fastly so the client distance does not matter)
and route docs-dev to it, with the doxygen pages still proxied from the
rubyci bucket and docs-origin left as the fallback for unflagged paths.

The custom VCL takes over everything the nginx origin does today: the
five redirect rules, per-version Surrogate-Key and Cache-Control (the
purge key scheme is unchanged), directory index.html, text/markdown for
the .md twins, and the 50x page. On top of that it resolves /ja/latest
and /ja/master through an edge dictionary instead of filesystem
symlinks, temporarily redirects the unreleased version (en/4.1 and
ja/4.1 to the master alias, 302, keyed off the same dictionary so a
release only updates dictionary values), and serves Markdown content
negotiation on /ja/ pages (Accept: text/markdown fetches the .md twin,
falling back to .html where no twin exists).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
default_host rewrites Host to the docs bucket endpoint for every
backend without an override, and S3 routes virtual-hosted requests by
Host, so /capi/en/master/ fetches were answered by the docs bucket and
came back 404. Use the regional endpoint like logs_rubyci.tf while
here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shield POP runs this VCL again on the edge's rewritten URL, and a
restart re-enters recv the same way. /ja/master resolved to /ja/<devel>
then matched the unreleased-version rule there, producing a 302 to
itself that the edge cached. The markdown fallback restart is likewise
kept at the edge so the fallback .html is never cached under a .md key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The no-dot-in-last-segment heuristic treated /ja/4.0 and /en/3.4 as
pages with an extension, so they fetched a nonexistent key and came
back 404 instead of redirecting like /ja/latest does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
X-Orig-Url and friends are only trusted because later passes set them
themselves; on the first pass at the edge they can only be a client
spoof. A forged X-Orig-Url poisons the Surrogate-Key of the cached
object, making it unpurgeable by the real keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A new bucket starts with all four blocks enabled, so PutBucketPolicy on
the public-read policy came back 403. The legacy buckets predate the
feature and stay unmanaged; ACLs remain blocked here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fastly.ff_visits_this_service is not a Fastly VCL variable and the
service rejected the upload. Fastly-FF is what the logging conditions
already use; since a client can forge it, vcl_fetch now derives the
Surrogate-Key and Vary decisions from the rewritten req.url instead of
the X-Orig-Url request header, which is dropped entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hsbt

hsbt commented Aug 19, 2026

Copy link
Copy Markdown
Member

@znz I added additional fixes and applied this branch to production. Can you confirm them?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants