Files
acdl/terraform/platform/README.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

4.0 KiB

ACDL Platform Infrastructure (D-051)

Terraform configuration for the platform-side infrastructure that ingests consumer deployment contracts and (Phase 25) reports errors as GitHub issues.

This stack is separate from terraform/spike/ (the consumer stack spike) and terraform/microservice/ (the demo microservice). It manages resources that live in the platform AWS account and serve all consumers — the contract ingestion pipeline and the secrets it needs.

What it deploys

Resource Name Purpose
aws_dynamodb_table acdl-contracts Stores submitted consumer contracts. PK consumerRepo, SK contractId#submittedAt. SSE via CMK, PITR enabled.
aws_kms_key + aws_kms_alias alias/acdl-platform Customer-managed key — encrypts DynamoDB SSE, Secrets Manager, and SSM. Key rotation enabled.
aws_secretsmanager_secret acdl/github-token GitHub PAT used by the Lambda to create issues on the platform repo (D-055, wired in Phase 25).
aws_iam_role + aws_iam_role_policy acdl-contract-ingestor-role Execution role for the Lambda — DynamoDB write, Secrets Manager read, KMS decrypt, CloudWatch logs.
aws_lambda_function acdl-contract-ingestor Python 3.12 Lambda. Handler contract_ingestor.lambda_handler. Source: core/lambda/contract_ingestor.py, packaged as contract_ingestor.zip.
aws_lambda_function_url Function URL with AWS_IAM authorization. Consumers invoke it via SigV4-signed requests.

State

Key Value
Backend S3
Bucket acdl-tfstate-581513795199-us-east-1
State key platform/terraform.tfstate
Region us-east-1

The state key is distinct from spike/terraform.tfstate and microservice/terraform.tfstate — the three stacks are independent.

Apply

# Package the Lambda source first (from the repo root):
cd core/lambda
zip contract_ingestor.zip contract_ingestor.py
cd ../../terraform/platform

terraform init
terraform plan
terraform apply

The Lambda's filename points at contract_ingestor.zip in the working directory (terraform/platform/); either place the zip there or adjust the path. source_code_hash = filebase64sha256("contract_ingestor.zip") forces a redeploy whenever the package changes.

Cross-account invocation model

The Lambda is invoked cross-account by consumer pipelines. The flow:

  1. Onboarding. When a consumer repo is onboarded, the platform team applies consumer_invoke_policy.json to the consumer's deploy role. The policy grants lambda:InvokeFunctionUrl on the Lambda ARN, scoped via ABAC — the condition aws:PrincipalTag/acdl:owner == ${consumerRepo} ensures a repo can only invoke when it is the owner it claims to be.
  2. Runtime. The consumer's deploy workflow (running in the consumer AWS account under the consumer's deploy role) signs the Function URL request with SigV4 using its deploy-role credentials. The IAM auth on the Function URL validates the signature and the ABAC condition.
  3. Lambda. The Lambda parses the JSON body, validates the fields, and writes the contract to acdl-contracts.

This is a one-way channel (D-051): the consumer pushes contracts to the platform; the platform never reaches back into the consumer account. Error reporting (D-055, action: "report_error") flows over the same channel and is implemented in Phase 25 (GitHub issue creation on the platform repo).