Skip to content

landing-zone

Cloud substrate

The slow-moving AWS layer — VPC, base IAM, KMS, DNS, cluster vending — as OpenTofu components driven by Terragrunt, plus the generic module that provisions a tenant's datastores from its declaration.

github.com/nanohype/landing-zone ·AGENTS.md


What follows is that repo's AGENTS.md — the entry point an agent reads before working on it. It is rendered from the repo itself at build time rather than copied, so it says what the repo says today. Links in it point back at the repo they were written for.

landing-zone — agent entry point

You’re an AI client (or the author of one) about to provision cloud substrate. This file gets you running in five minutes. For the wider picture — how this repo fits into the rest of the nanohype stack — read the Platform Reference.

What this repo gives you

OpenTofu + Terragrunt monorepo for the AWS substrate every nanohype-stack app lands on:

  • components/aws/ — VPC (network/shared-network/egress-network), base IAM, KMS keys, EKS cluster, cluster bootstrap, observability, secrets, governance, agent-iam. Shared multi-tenant components (var.tenants): druid, pipeline, governance, and tenant-substrate — the generic per-tenant datastore substrate that provisions a tenant’s declared stores from its Platform CR.

Plus:

  • modules/ — reusable building blocks components compose: workload-identity (the EKS Pod Identity role factory) and eks-vpc-endpoints (the private endpoint set create-mode network and shared-network build).
  • live/ — per-environment terragrunt configurations. Path is live/aws/<account>/<region>/<env>/<component>/terragrunt.hcl.

Run it

The build interface is Taskfile.yaml (Task), not raw tofu/terragrunt. Prerequisites: OpenTofu >= 1.11.0, Terragrunt, TFLint with the AWS plugin, Python 3, and the AWS CLI for anything that touches an account.

Terminal window
task check # everything CI runs: fmt, validate, lint, tofu test, and every gate
task fmt:check # formatting only
task validate # tofu init -backend=false + validate, every root
task lint # tflint at --minimum-failure-severity=notice
task test # the tofu test suites
task gates # the semantic gates under scripts/, each its own CI job
task plan ACCOUNT=workload-development REGION=us-east-1 ENVIRONMENT=development COMPONENT=network

task check needs no AWS credentials — every suite runs against mocked providers and every gate reads the tree. task plan and task apply need credentials for the target account.

Six user-invocable skills live in .claude/skills/: add-component, add-tenant, plan, drift, validate, destroy. Each names the steps and the gates its change has to satisfy.

Contract surface

Every component:

  • Has its own versions.tf declaring terraform >= 1.11.0 + AWS provider ~> 6.0.
  • Has its own variables.tf + outputs.tf + per-resource files.
  • Reads from upstream component outputs via terragrunt dependency blocks declared in live/_envcommon/aws/<component>.hcl.
  • Tags every resource with Environment, ManagedBy, Project, CostCenter, BusinessUnit, DataClassification, Compliance, Repository (default tags emitted by live/root.hcl).
  • Uses EKS Pod Identity via the shared modules/aws/workload-identity module. Trust policies target pods.eks.amazonaws.com (not an OIDC provider), and each role is bound to a specific ServiceAccount in a specific namespace through an EKS Pod Identity association.

A tenant’s stateful substrate is a declaration, not a per-app component. Each store is declared in Platform.spec.datastores (relational/keyValue/objectStore/queue/cache/stream) and the generic tenant-substrate component provisions it; the eks-agent-platform operator generates the scoped datastore-access IAM policy on the tenant role and binds the tenant-runtime ServiceAccount to it via a Pod Identity association. Non-datastore capabilities (SES, EventBridge Scheduler) are declared in Platform.spec.identity.capabilities and the operator generates their grants (minting a scheduler-invoke role for the latter). Bedrock model access comes from the agent-iam tenant baseline, clamped by the operator to Platform.spec.identity.allowedModels. Adding a tenant is a declaration in the app’s own repo — landing-zone hand-writes no per-app substrate.

Add a new component

  1. Create components/aws/<name>/ with versions.tf, variables.tf, main.tf, outputs.tf, plus per-resource files (rds.tf, s3.tf, etc.).
  2. Add live/_envcommon/aws/<name>.hcl declaring dependencies on upstream components (typically network, cluster, cluster-bootstrap).
  3. Add live/aws/<account>/<region>/<env>/<name>/terragrunt.hcl per environment you want to provision (workload-development, workload-staging, workload-production).
  4. Add the component to the ## Layer Breakdown table in docs/architecture.md and to the layer list in CLAUDE.md. scripts/check-architecture-components.sh gates both: docs/architecture.md in both directions (a documented component that is gone, and a component no table names), and CLAUDE.md in the omission direction only — it is prose rather than a table, so demanding a parseable shape would fix its formatting in place.
  5. Declare the teardown posture: a component holding an aws_s3_bucket, aws_rds_cluster, aws_dynamodb_table or aws_secretsmanager_secret either wires a force_destroy_buckets lever through every gate attribute those types need, or is named in the EXEMPT table in scripts/check-teardown-gates.py with the reason.
  6. Write components/aws/<name>/tests/<name>.tftest.hcl. Every other root carries one; they run at command = plan against a mock_provider, so they need no credentials.
  7. Run task check.
  8. CI auto-discovers the new component via git ls-files — no workflow edit needed (Validate (aws/<name>) job materializes on the next PR).

Add a tenant’s substrate

A tenant’s databases, buckets, queues, caches, and streams are not a landing-zone component — they are declared in the app’s Platform.spec.datastores and provisioned by the generic tenant-substrate component from that declaration (its var.tenants map is rendered from the Platform CRs, not hand-authored). SES and EventBridge Scheduler ride Platform.spec.identity.capabilities, generated by the operator. So to add or change a tenant’s substrate you edit the Platform CR in the app’s repo — there is no per-app component to author here.

What still lands in landing-zone:

  1. Cloud-substrate gaps the datastore vocabulary does not cover — a shared VPC, a base IAM grant, a KMS key, a new managed service — as a new general component (not a per-app one).
  2. If a tenant needs a substrate service the tenant permissions boundary doesn’t cover yet, extend agent-iam’s tenant boundary — the boundary caps every tenant role, so a grant outside it is silently clipped.

Conventions

  • All TF formatted with tofu fmt -recursive. CI’s Format Check job blocks on drift.
  • tflint runs repo-wide via the .tflint-aws.hcl config — a hard gate at --minimum-failure-severity=notice, so undocumented or unused declarations and missing version constraints fail the build. The uniform envcommon interface inputs carry inline # tflint-ignore rationale; nothing else may.
  • Security scan via checkov — a hard gate (soft_fail: false); accepted posture trade-offs are enumerated in .checkov.yaml, one line of rationale each.
  • Backend: per-component S3 state with native lockfile locking, generated by live/root.hcl.
  • Input precedence: an ambient TF_VAR_* beats a leaf’s inputs value — the opposite of the natural assumption, and terragrunt’s documented order. Two consequences: anything injecting TF_VAR_* silently overrides a deliberate per-environment value, so inject only when the value differs from the default; and no TF_VAR can rescue an unapplied dependency, because a dependency is resolved at config-parse time before tofu exists — which is why a live/aws/workload-*/ leaf may not depend on another account’s state (gated by scripts/check-account-local-deps.py). Full statement in docs/inputs.md.
  • The github-oidc deploy-role trust scopes to repo:nanohype/landing-zone with environment-gated and tag-push subject claims (allowed_subject_claims = ["environment:*", "ref:refs/tags/*"]). A bare :* — which would also trust fork PRs and arbitrary branches — is deliberately excluded; widen allowed_subject_claims explicitly if the CI model needs another context.

Pointers

  • README.md — full repo overview
  • docs/ — architecture, OIDC setup, drift management
  • CLAUDE.md — Claude Code session instructions
  • Platform Reference — the stack-wide view
  • eks-agent-platform/AGENTS.md — the operator that provisions per-tenant IAM + Pod Identity and generates the datastore-access + capability-access grants from the Platform CR