Pinterest: Resource Provisioner Pipeline — a Centralized Terraform Engine for Least-Privilege AWS at Scale
Problem + constraints
Pinterest's AWS footprint spans hundreds of Terraform workspaces controlling tens of thousands of resources — IAM policies, VPCs, load balancers, S3 buckets, Kubernetes clusters. The conventional way teams get there is each repo wiring its own GitHub Actions workflow to run terraform plan/apply against AWS, usually via a broadly-scoped CI credential.
That pattern creates two compounding problems at scale. First, blast radius: a CI runner (or its shell, or a leaked token) that can apply Terraform for one team's infra is often, in practice, far more privileged than that team's actual resources require — because scoping IAM tightly per-workspace by hand, hundreds of times over, doesn't happen consistently. Second, fix fan-out: if a systemic weakness is found — a vulnerable CI runner base image, a missing static-analysis check — fixing it means touching every one of those hundreds of independently-configured pipelines, not one place. Neither problem is really a Terraform problem; it's what happens when the same security-critical logic (auth, review gating, scoping) is duplicated per-repo instead of centralized.
Solution
Pinterest built the Resource Provisioner Pipeline (RPP) — a purpose-built Terraform execution engine that every workspace routes through, rather than each team owning its own apply pipeline:
- Workspace-path-role mapping as source of truth. A central config maps each Terraform workspace path to the specific AWS IAM role it's allowed to assume — down-scoped to only what that workspace's resources need. RPP validates the backend and this mapping before any plan/apply runs, so a workspace can't silently drift into requesting broader access than it's declared.
- OIDC role-chaining instead of static CI credentials. Runs authenticate via OIDC and chain into the down-scoped role for that specific workspace, rather than every pipeline sharing one broad, long-lived credential.
- Dual control, split across plan and apply. A human code-review approval on the PR is one control; triggering the actual
applyrequires a separate, explicit PR comment. Planning and applying are kept as distinct, auditable actions rather than one PR-merge automatically applying infrastructure changes. - Centralized composite GitHub Actions. PR-triggered checks — static analysis via custom Semgrep rules, AI-assisted scanning, and optional LocalStack-based dry runs against mocked AWS behavior — live in shared composite actions, not duplicated per-repo.
The payoff of centralizing is the same payoff as any shared-core pattern: a systemic issue (a weak CI runner shell, a missing check) gets fixed in RPP once, and every workspace that routes through it inherits the fix — instead of a fan-out across hundreds of independently-drifted pipelines.
What to steal
- Route security-critical CI/CD logic through one engine, not N copies of a workflow file. The instant a pipeline pattern (auth, approval gating, scoping) is copy-pasted into every repo that needs it, it starts drifting, and a fix becomes an audit-and-patch-everywhere project instead of a one-line change.
- Make the scope-mapping itself a validated, central source of truth, not tribal knowledge encoded per-workspace. Pinterest's workspace-path-role config is the thing that makes least privilege enforceable at hundreds-of-workspaces scale — without it, "least privilege" is just a policy on paper that depends on every team getting IAM right by hand.
- Split plan and apply into separately-gated steps. A single "merge PR → auto-apply" pipeline collapses review-of-intent and authorization-to-execute into one action; requiring a distinct trigger for apply (on top of code review) is a cheap way to keep a compromised or mis-clicked merge from directly mutating production infrastructure.
- Push cheap, deterministic checks (static analysis, mocked dry-runs) before the expensive/risky one (a real
apply). LocalStack dry-runs catch a class of mistakes without ever touching a real AWS account, the same "fail fast and cheap before you fail expensive" instinct that shows up in test pyramids generally.
Principal Engineer Lens
This is the IaC-pipeline version of a pattern that shows up constantly at Principal scope: centralize the redundant, security-critical client instead of asking every team to reimplement it correctly. The naive alternative — "every team owns its own Terraform pipeline, scoped and reviewed however they see fit" — looks like autonomy, but it's actually hundreds of independent opportunities for privilege creep, and a systemic fix requires touching all of them. Pinterest's answer converts "get IAM scoping right" from a per-team discipline problem into a property the platform enforces centrally (the workspace-path-role mapping), which is a much more durable way to get consistent least privilege than a policy doc and hope. In an architecture review, the sharp question for any multi-team CI/CD-to-cloud pipeline is: "if we found a critical flaw in this pipeline tomorrow, how many places would we have to fix it?" — if the honest answer is "every repo," that's the signal to centralize, same as Pinterest did here.
Reel Script
Setup: Pinterest runs hundreds of Terraform workspaces touching tens of thousands of AWS resources. If every team wires its own GitHub Actions pipeline to apply infra changes, you get inconsistent scoping and a security fix that has to be copy-pasted into every repo.
Concept walkthrough: Walk through RPP as the single execution engine every workspace routes through: a central workspace-to-IAM-role mapping enforces down-scoped, least-privilege access; OIDC role-chaining replaces static shared credentials; dual control splits code review (on the PR) from triggering the apply (a separate comment); and centralized composite actions run static analysis, AI-assisted scanning, and LocalStack dry-runs before anything touches a real account.
Real example / case study tie-in: Trace what happens when a weak CI runner shell is found. In a per-repo pipeline world, that's an audit-and-patch project across hundreds of repos. Routed through RPP, it's a fix in one engine that every workspace inherits on its next run.
Tradeoffs & alternatives: Contrast with letting every team own its pipeline (more autonomy, but privilege creep and fix fan-out) and with a full mono-repo Terraform migration (would also centralize control, but is a much bigger structural change than Pinterest needed — RPP gets the centralization benefit without forcing every team's Terraform into one repo).
Principal Engineer takeaway: When the same security-critical logic would otherwise be duplicated across many teams' pipelines, ask whether it can be pulled into one enforced, centrally-owned engine — the fix-once property is usually worth more than the flexibility lost.