Files
acdl/docs/environments/index.md
T
Jon Chery 07c0349131 feat(P24): platform Lambda + DynamoDB contract ingestion + cross-account IAM
Phase 24 — platform-lambda-and-contract-ingestion.

- core/lambda/contract_ingestor.py: AWS Lambda handler invoked via Function
  URL (IAM auth). Parses JSON body, validates required fields, writes the
  contract to DynamoDB table acdl-contracts (PK consumerRepo, SK
  contractId#submittedAt, status submitted, ISO-8601 submittedAt). report_error
  action is a stub returning "error_report_prepared"; GitHub issue creation is
  wired in Phase 25. Returns 400 on missing fields / unknown action, 500 on
  error. Table name + GitHub-token secret ID come from env (set by Terraform).
- core/lambda/__init__.py: empty package marker.
- terraform/platform/main.tf: DynamoDB acdl-contracts (PITR, SSE via CMK),
  KMS customer-managed key with alias/acdl-platform, Secrets Manager secret
  acdl/github-token, IAM execution role (DynamoDB write + Secrets Manager read +
  KMS decrypt + CloudWatch logs), Lambda acdl-contract-ingestor (Python 3.12,
  handler contract_ingestor.lambda_handler), Function URL with AWS_IAM auth.
  State key platform/terraform.tfstate (distinct from spike/microservice).
- terraform/platform/README.md: documents what it deploys, the state key, how
  to apply, and the cross-account invocation model.
- terraform/platform/consumer_invoke_policy.json: ABAC-scoped policy template
  applied to consumer deploy roles during onboarding; grants
  lambda:InvokeFunctionUrl conditioned on aws:PrincipalTag/acdl:owner ==
  consumerRepo.
- tests/test_contract_ingestor.py: 11 tests (moto-backed DynamoDB mock) covering
  submit_contract put_item shape, report_error stub, missing-field 400, unknown
  action 400, the lambda_handler wrapper with a Function-URL-style event, dict
  body, default action, and internal-error 500.
- docs/environments/index.md: new section documenting the cross-account
  contract-ingestion grant (one-way consumer→platform, D-051) and that
  onboarding now also grants the consumer deploy role InvokeFunctionUrl.
- scripts/run_ci.sh, pipelines/ci.yaml, .gitea/workflows/ci.yml,
  .github/workflows/ci.yml: add core/lambda/contract_ingestor.py to the lint
  py_compile list. The two workflow YAMLs remain byte-identical.

Verification: scripts/run_ci.sh passes all 3 stages (lint/test/check-only);
python3 -m pytest tests/ -v passes all 213 tests (11 new + 202 existing).

---ci---
project: acdl
phase: 24
milestone: v1.7
status: execute
---/ci---
2026-07-22 20:04:10 +00:00

104 lines
4.4 KiB
Markdown

# Environments
A consumer does **not** provide an AWS account, a VPC, a subnet, an S3 state
bucket, or a runner key. The platform manages environments.
## What an environment is
A named environment is a **platform-owned** bundle of:
- An AWS account (or a scoped partition of one).
- A network (VPC + subnets).
- A state backend (an S3 bucket + DynamoDB lock table for infrastructure
state).
- An IAM role surfaced to the consumer via attribute-based authorization
(ABAC), scoped to the consumer's repository identity and resource tags.
A consumer selects an environment **by name** in their contract:
```yaml
environment: dev
```
The platform resolves the name to the underlying account/network/state/role
at run time. The consumer never sees the raw credentials.
## First-run onboarding
When a consumer pipeline runs for the first time and **no environment is
defined** for the consumer's repo, the platform detects this and emits a
user-friendly onboarding prompt instead of failing opaquely. The prompt
tells the consumer:
1. That no environment is bound to their repo yet.
2. What the platform will provision on their behalf (account/network/state/
role).
3. The expected turnaround for the platform team to grant the environment.
4. How to request an environment (contact the platform team).
The pipeline then exits without attempting a deployment. Once the platform
team binds an environment to the repo, the next pipeline run proceeds
normally.
## Autonomy by environment
| Environment | Autonomy | Gate |
|-------------|----------|------|
| dev | Full autonomy | Confidence ≥ 0.50 |
| qa | Held for attestation | QA attestation + confidence ≥ 0.75 |
| prod | Held for attestation | SRE attestation + confidence ≥ 0.90 |
| dr | Held for attestation | SRE attestation + confidence ≥ 0.95 + dr-drill |
`dev` is the only autonomous environment. Higher environments require human
attestation (a platform-runner deployment approval) and a higher confidence
threshold. Staging does not exist.
## Cross-account contract ingestion grant (D-051)
Onboarding now also grants the consumer repo's deploy role permission to
invoke the **platform Lambda**`acdl-contract-ingestor` — across
accounts. The Lambda is invoked via a Function URL with IAM auth, so the
grant is an inline IAM policy applied to the consumer's deploy role. The
policy template lives at
[`terraform/platform/consumer_invoke_policy.json`](https://github.com/acdl/acdl/blob/main/terraform/platform/consumer_invoke_policy.json)
and is scoped via **ABAC**: the condition
`aws:PrincipalTag/acdl:owner == ${consumerRepo}` ensures a repo can only
invoke the Lambda when its principal tag matches its claimed identity.
The consumer's deploy workflow signs the Function URL request with
SigV4 using its deploy-role credentials; the platform Lambda validates
the signature and the ABAC condition before accepting the payload.
This is a **one-way** channel — the consumer pushes contracts *to* the
platform; the platform never reaches back into the consumer account. It
is used for two purposes:
1. **Contract ingestion** — the consumer submits its resolved deployment
contract (`action: "submit_contract"`) so the platform has a durable
record in the `acdl-contracts` DynamoDB table (PK `consumerRepo`, SK
`contractId#submittedAt`).
2. **Error reporting** (D-055) — the consumer reports a deployment error
(`action: "report_error"`) which the platform turns into a GitHub
issue on the platform repo (wired in Phase 25; the Lambda returns a
prepared-status stub until then).
The Lambda handler and the Terraform that deploys it live in
[`core/lambda/contract_ingestor.py`](https://github.com/acdl/acdl/blob/main/core/lambda/contract_ingestor.py)
and
[`terraform/platform/main.tf`](https://github.com/acdl/acdl/blob/main/terraform/platform/main.tf)
respectively.
## Onboarding scaffold (current state)
The platform repo ships a minimal onboarding scaffold:
- [`core/environments/`](https://github.com/acdl/acdl/blob/main/core/environments/)
— environment definitions (a sample `dev.json`).
- `core/environment_check.py` — checks whether an environment is defined for
a given contract's repo + environment name; prints the friendly onboarding
prompt when none is defined.
- `scripts/run_platform.sh` calls the check before contract validation.
The scaffold is minimal: the actual provisioning of a new environment is a
platform-team action today. Self-service environment provisioning is on the
[roadmap](../).