Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ What we steer by. Items link their tracking issue; see [GitHub Issues](https://g

- [#1052](https://github.com/asanexample/platform/issues/1052) **Platform FinOps practice (epic) — adopt the FinOps Framework ([ADR-092](docs/adrs/092-platform-finops-practice.md)).** The operating model (Inform → Optimize → Operate) for the platform's **own** cost, above the per-team guardrails (ADR-091, shipped). Free OSS/native only (no spend budget); Savings Plans + paid tiers documented-but-deferred. Workstreams — **shipped/closed:** AWS Budgets + Cost Anomaly Detection ([#1054](https://github.com/asanexample/platform/issues/1054)), Infracost shift-left cost-in-PR ([#1056](https://github.com/asanexample/platform/issues/1056)), true-spend CUR→Athena ([#668](https://github.com/asanexample/platform/issues/668)). **Still open:** platform-shared cost bucket ([#1053](https://github.com/asanexample/platform/issues/1053)), Compute Optimizer rightsizing ([#1055](https://github.com/asanexample/platform/issues/1055)), kube-green off-hours idle spike ([#1057](https://github.com/asanexample/platform/issues/1057)), Cloud Custodian account janitor spike ([#1058](https://github.com/asanexample/platform/issues/1058)), FinOps cadence + forecasting ([#1059](https://github.com/asanexample/platform/issues/1059)), Slack cost-alert delivery ([#1063](https://github.com/asanexample/platform/issues/1063)).

#### Supply Chain & Artifacts

- [#1249](https://github.com/asanexample/platform/issues/1249) **Epic: package registry — AWS CodeArtifact (+ ECR pull-through cache) ([ADR-098](docs/adrs/098-package-registry-codeartifact.md)).** The language-package sibling to ECR — private npm/PyPI/Maven/NuGet with upstream-proxy caching of public repos — plus a public-Docker mirror via ECR's own pull-through cache. Managed AWS, reusing the Pod-Identity/GitHub-OIDC auth model (no self-hosted registry: Nexus CE is capped + SSO/HA-gated, Harbor is OCI-only). Workstreams: `codeartifact` module ([#1250](https://github.com/asanexample/platform/issues/1250)), live unit + per-Product derivation ([#1251](https://github.com/asanexample/platform/issues/1251)), ECR pull-through cache ([#1252](https://github.com/asanexample/platform/issues/1252)), consumer/CI auth wiring ([#1253](https://github.com/asanexample/platform/issues/1253)), client bootstrap ([#1254](https://github.com/asanexample/platform/issues/1254)). Deferred: Go modules (unsupported by CodeArtifact).

#### Reliability & Tech Debt

- [#769](https://github.com/asanexample/platform/issues/769) **Epic: tech-debt paydown — 2026 H2 inventory.** Deep 10-pass audit (~150 `TD-NNN` findings; the dated audit snapshots are retired — see git history). Tier 1 (security/correctness, spot-verified) — **all four done/closed:** enforce cosign on the hub ([#770](https://github.com/asanexample/platform/issues/770) ✅), private-only EKS default ([#771](https://github.com/asanexample/platform/issues/771) ✅), fail-closed gate scripts ([#772](https://github.com/asanexample/platform/issues/772) ✅), DeveloperAccess regression ([#647](https://github.com/asanexample/platform/issues/647) — closed as superseded by OIDC-native cluster auth [#364](https://github.com/asanexample/platform/issues/364); capability still unbuilt). Tier 2 (systemic) — **done/closed:** provider-constraint standardization ([#773](https://github.com/asanexample/platform/issues/773), residual stragglers → [#980](https://github.com/asanexample/platform/issues/980)), `.tool-versions` SSOT ([#774](https://github.com/asanexample/platform/issues/774)), Azure-carcass removal ([#775](https://github.com/asanexample/platform/issues/775)), Action SHA-pinning ([#776](https://github.com/asanexample/platform/issues/776)). **Deferred:** Dependabot coverage ([#777](https://github.com/asanexample/platform/issues/777)), tests-into-PR-gates ([#778](https://github.com/asanexample/platform/issues/778)). Security/IAM findings fold into the existing tracker [#654](https://github.com/asanexample/platform/issues/654). The second-pass live-state audit (epic [#809](https://github.com/asanexample/platform/issues/809), TD2-NN) is burning down — CNPG backups (#1119) done, preprod Kyverno HA (#812) + right-sizing (#818) outstanding.
Expand Down
147 changes: 147 additions & 0 deletions docs/adrs/098-package-registry-codeartifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# ADR-098: Package Registry — AWS CodeArtifact (with ECR Pull-Through Cache for the Docker Mirror)

**Date:** 2026-07-08

**Status:** Proposed

## Context

The platform has a first-class **container-image** registry — centralized ECR in the platform account,
cross-account pull, per-Product repos, immutable tags, lifecycle rules, cosign-verified (ADR-028, ADR-050,
ADR-014). It has **no equivalent for language packages.** As soon as tenant apps grow past a single service
they need somewhere to publish and consume private **npm / PyPI / Maven / NuGet** packages, and — just as
importantly — a **proxy/cache of public upstreams** (npmjs, PyPI, Maven Central, …) so builds don't depend
on third-party registry availability, rate limits, or a supply-chain incident on the public internet. ECR
handles OCI artifacts (images, Helm charts, signatures, SBOMs); it does nothing for these package formats.

A related, narrower gap is a **pull-through cache for public Docker images** (`docker.io`, `ghcr.io`,
`quay.io`, `registry.k8s.io`) so cluster and CI pulls of base/tooling images are mirrored into our own
account rather than hitting Docker Hub rate limits on every pull.

Two forces frame the decision:

1. **This is a reference platform whose mission is to demonstrate scale and governance.** A capped or
bolt-on registry undercuts that.
2. **The platform's identity model is IAM- and Keycloak-native.** Runtime AWS access is EKS Pod Identity
(ADR-047/041); CI access is GitHub OIDC federation (ADR-036); human/UI SSO is Keycloak (ADR-053). Any
registry that requires its own local user directory and hand-minted tokens sits *outside* this model.

### Alternatives considered

**1. Self-hosted Sonatype Nexus Repository Community Edition.** The obvious "universal, open-source,
self-hosted" candidate — one tool covering Docker + npm + PyPI + Maven + NuGet + more, with proxy/hosted/group
repos. **Rejected.** The 2025-rebranded Community Edition is materially limited in exactly the dimensions this
platform cares about:

- **Hard usage caps** — 40,000 total components *or* 100,000 requests/day; exceeding either **blocks new
components** until the deployment drops back under. These caps were *halved* in 3.87.0 (from 100k/200k), so
the trend is downward. A scale-demonstrating platform gated behind a shrinking free tier is the wrong bet.
- **No SSO / SAML** (Pro-only). This collides with our access-as-code model (ADR-053). `oauth2-proxy` can't
rescue it the way it fronts the Rollouts UI, because package clients (`npm`, `pip`, `docker`) authenticate
to the registry **API** with tokens/basic-auth, not a browser OIDC flow — so CE means local Nexus users +
hand-minted tokens off to the side of Keycloak/IAM.
- **No HA / single node / no zero-downtime upgrades** (Pro-only) — a single-instance SPOF in the
build-and-deploy critical path, against ADR-085. We have already been burned by fragile single-instance
auth DBs (Backstage/Keycloak SSO wedges).
- **Cloud blob-store support appears Pro-gated** ("Full Support for Azure, GCP, and AWS" is Pro), which would
likely force local/EBS storage rather than S3.

**2. Self-hosted Harbor.** CNCF-graduated, genuinely high quality. **Rejected as a fit, not on merit:** Harbor
is an **OCI** registry (images, Helm, OCI artifacts, proxy cache, Trivy, cosign). It does *not* serve
npm/PyPI/Maven/etc., so it would largely duplicate ECR (which is more integrated here) while leaving the
actual gap — language packages — unfilled.

**3. JFrog Artifactory OSS.** The OSS tier is far more limited than Nexus CE (effectively Maven/Gradle/Ivy +
generic; no Docker, npm, or PyPI without a paid tier). Not viable.

**4. GitHub Packages.** We already live on GitHub. **Rejected:** it's SaaS-tied, its auth is GitHub-token- /
PAT-based rather than IAM/Pod-Identity, and it pulls package hosting off the platform we're demonstrating. It
doesn't extend the ECR/Pod-Identity pattern we already operate.

**5. AWS CodeArtifact + ECR pull-through cache (chosen).** The managed AWS package registry is the packages
sibling to ECR: same account topology, same IAM/OIDC auth model, no caps, HA and S3-backed by AWS, KMS
encryption, and native **upstream external connections** that proxy+cache public repos. The Docker-mirror gap
is closed by ECR's own **pull-through cache** rules — no new tool. The single real gap is **Go modules**
(unsupported by CodeArtifact), handled as a caveat below.

## Decision

Adopt **AWS CodeArtifact** as the platform's package registry, and enable **ECR pull-through cache** for the
public-Docker mirror. Treat both as managed extensions of the existing ECR/Pod-Identity/OIDC pattern rather
than new self-hosted services.

**D1 — Centralized CodeArtifact domain in the platform account, cross-account read.** A single CodeArtifact
**domain** in the platform account (`<PLATFORM_ACCOUNT_ID>`) is the dedupe + encryption boundary (one KMS
key, one asset store). Repositories live within it. A **domain permissions policy** grants read to the
preprod and prod account principals, mirroring ECR's centralized-registry-with-cross-account-pull design
(ADR-028). CI publishes once; environments read the same package versions. Managed by a new
`infra/modules/aws/codeartifact/` module + a live unit alongside the `ecr` unit.

**D2 — Docker pull-through cache is ECR-native, not a new registry.** Create ECR **pull-through cache rules**
for `docker.io`, `ghcr.io`, `quay.io`, and `registry.k8s.io`; upstream credentials (Docker Hub, to avoid
anonymous rate limits) live in Secrets Manager. Public base/tooling images are then lazily mirrored into our
account on first pull and served with the same IAM auth as our own images. This closes the "pull-through
docker cache" need without adopting Harbor or a registry mirror.

**D3 — Auth reuses the existing model; no local users, no SSO problem.** Runtime consumers authenticate via
**EKS Pod Identity** (ADR-047/041) to call `codeartifact:GetAuthorizationToken` and read; **CI** publishes via
**GitHub OIDC** (ADR-036) assuming a scoped role. There is no CodeArtifact user directory and no Keycloak
integration to build — the SSO gap that sinks self-hosted CE simply does not exist, because access *is* IAM.

**D4 — Per-Team/Product scoping + upstream connections, derived from the Product registry.** Repositories are
scoped per Team/Product (matching ECR's `team-<team>/<product>-<svc>` intent), with **upstream external
connections** to the public sources each format needs (`public:npmjs`, `public:pypi`,
`public:maven-central`, `public:nuget-org`, …) so first requests cache through and later requests are served
locally. Repository/derivation config follows registries-as-single-source (ADR-069) rather than a hand-kept
map, consistent with how `ecr`/`policy`/`github-oidc` already derive per-Product.

**D5 — Go modules are a known gap, handled out-of-band.** CodeArtifact does **not** serve Go modules. Go
dependency caching is deferred to a follow-up — either an **Athens** GOPROXY (self-hosted, but a stateless
cache, not an identity-bearing registry) or accepting direct `GOPROXY` (which we already do for `crank`; see
the Crossplane-CLI-in-CI note). This ADR does not commit to Go module hosting.

## Consequences

### Positive

- **Fills the actual gap** (language packages) without duplicating ECR, and closes the Docker-mirror need with
a native ECR feature — one coherent AWS-native registry story (images *and* packages) under one auth model.
- **No caps, HA, S3-backed, KMS-encrypted** by AWS — no single-instance SPOF in the build/deploy path, in
keeping with ADR-085.
- **Zero new identity surface.** Pod Identity + GitHub OIDC already exist; there is no local user store, no
SSO wiring, and no long-lived credentials — consistent with the `restrict-iam-users` SCP.
- **Supply-chain posture improves** — builds resolve public dependencies through cached upstreams we control,
reducing exposure to public-registry outages, rate limits, and tampering; a natural home for future
package-level scanning/provenance.
- **On-brand showcase.** The demonstration is the *governed, registry-derived, Pod-Identity-scoped* package
access pattern — not operating a JVM monolith.

### Negative

- **AWS coupling.** Packages join images in being AWS-managed; there is no portable self-hosted artifact
plane. Accepted — it is the same trade already made for ECR and consistent with the AWS-only posture.
- **Format coverage is finite.** CodeArtifact covers npm/PyPI/Maven/NuGet/generic and several others, but the
exact matrix must be confirmed against teams' real languages before rollout — and **Go is not covered**
(D5).
- **Client configuration is per-tool.** Each package manager needs endpoint + token wiring (the token is
short-lived, ≤12h), so tenant CI and dev tooling need a small, documented bootstrap (a scaffolder concern).

### Risks

- **Token/permission sprawl.** Per-Product read/publish scoping via IAM must be derived correctly from the
registry, or a product could read/publish outside its scope. Mitigated by deriving from the Product registry
(ADR-069) and reviewing the domain/repository permission policies like we review ECR's.
- **Cost of cached upstreams.** Proxying large public ecosystems into CodeArtifact incurs storage + request
cost; unbounded caching could surprise. Mitigated by scoping upstream connections to formats actually used
and folding CodeArtifact into the FinOps guardrails (ADR-091/092).
- **New-account onboarding drift.** Like ECR's `pull_account_ids`, the domain permissions policy must be
updated when a new account is added — add it to the new-account onboarding checklist.

## Related

- [ADR-028: ECR Cross-Account Container Registry](028-ecr-cross-account-container-registry.md) — the OCI sibling; same centralized-with-cross-account-read topology.
- [ADR-036: GitHub Actions OIDC Federation for CI/CD](036-github-actions-oidc-federation.md) — CI publish auth.
- [ADR-047: EKS Pod Identity as the Standard for Pod AWS Identity](047-pod-identity-as-aws-identity-standard.md), [ADR-041: EKS Pod Identity for Tenant Workloads](041-pod-identity-for-tenant-workloads.md) — runtime read auth.
- [ADR-053: Identity & Cross-System Authorization Strategy](053-identity-and-cross-system-authorization-strategy.md) — why a local-user/SSO-gated registry is off-model.
- [ADR-069: Delivery Source-of-Truth — Product Registry + Environment Claims](069-delivery-source-of-truth-product-environment.md) — per-Product derivation.
- [ADR-050: Shared `build-sign` Reusable Workflow](050-shared-build-sign-reusable-workflow.md), [ADR-085: Workload Availability & Graceful-Disruption Defaults](085-workload-availability-graceful-disruption-defaults.md) — supply-chain and availability posture referenced above.
1 change: 1 addition & 0 deletions docs/adrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ rewriting it. This index is the canonical list; keep it in sync when adding an A
| [ADR-071: Image-Digest Promotion via the Control Plane (Protected-Main Delivery)](071-digest-promotion-via-control-plane.md) | Accepted |
| [ADR-072: App-Repo Naming & Team Ownership](072-app-repo-naming-and-team-ownership.md) | Accepted |
| [ADR-095: Dynamic Application Security Testing (DAST)](095-dast-dynamic-application-security-testing.md) | Proposed |
| [ADR-098: Package Registry — AWS CodeArtifact (+ ECR Pull-Through Cache)](098-package-registry-codeartifact.md) | Proposed |

## Developer Portal & Experience

Expand Down