Organizational

Infrastructure Layout

Pattern for organizing infrastructure-as-code using the .infra/ convention in repositories.

Production

Core Principle

Infrastructure-as-code lives in .infra/ within each repository.

Each repository that provisions cloud resources MUST place Terraform configurations in a .infra/ directory at the repository root.

The .infra/ Convention

Structure

my-service/
├─ .infra/
│  ├─ main.tf
│  ├─ variables.tf
│  ├─ outputs.tf
│  ├─ backend.tf
│  └─ modules/
│     ├─ database/
│     └─ cache/
├─ src/
├─ Taskfile.yaml
├─ AGENTS.md
└─ README.md

What Lives in .infra/

  • Terraform configurations for this repository's infrastructure
  • Terraform modules specific to this repository
  • Environment-specific variable files (.tfvars)
  • Infrastructure documentation specific to this repository

What Does NOT Live in .infra/

  • Shared Terraform modules (live in engineering-patterns or a dedicated modules repository)
  • Platform-wide infrastructure (lives in the central infra repository)
  • Application code or configuration
  • Deployment scripts (those belong in Taskfile or CI/CD)

The Central infra Repository Exception

The central infra repository is the ONLY exception to the .infra/ rule.

The infra repository:

  • Provisions foundational platform infrastructure
  • Does NOT follow the .infra/ convention
  • Organizes Terraform at the repository root or in top-level directories

Why This Exception Exists

The infra repository IS infrastructure. It doesn't contain application code that needs infrastructure; it defines the platform that other repositories build upon.

Example central infra repository structure:

infra/
├─ bootstrap/           # Backend initialization
│  ├─ init.sh          # Creates S3 bucket and DynamoDB table
│  └─ README.md
├─ global/             # Global Terraform configuration
│  ├─ main.tf
│  ├─ backend.tf
│  ├─ variables.tf
│  ├─ outputs.tf
│  ├─ route53/         # DNS management
│  ├─ iam/             # Identity and access management
│  ├─ cost-alerts/     # Budget monitoring
│  └─ amplify/         # Amplify applications (optional)
├─ utils/              # Reusable resources
│  ├─ policies/        # IAM policy documents
│  ├─ scripts/         # Utility scripts
│  └─ templates/       # Terraform templates
├─ Taskfile.yaml
├─ AGENTS.md
└─ README.md

Repository Categories

Application Repositories

Repositories that contain application code and need infrastructure:

  • Structure: Code in src/, infrastructure in .infra/
  • Purpose: Provision resources specific to this application
  • Examples: API services, web frontends, background workers

Infrastructure Repository

The central repository that provisions foundational platform infrastructure:

  • Structure: Terraform at root or top-level directories (bootstrap, global, utils)
  • Purpose: Bootstrap and manage the platform
  • Examples: DNS zones, IAM roles, cost alerts, Terraform backend

Library Repositories

Repositories that contain shared code but no infrastructure:

  • Structure: No .infra/ directory
  • Purpose: Provide reusable code to other repositories
  • Examples: Shared libraries, SDKs, utilities

Operating Infrastructure via Taskfile

Infrastructure operations MUST be exposed through Taskfile tasks:

For application repositories:

# Taskfile.yaml
infra:plan:
  desc: Show Terraform plan for infrastructure changes
  dir: .infra
  cmds:
    - terraform init
    - terraform plan

infra:apply:
  desc: Apply infrastructure changes
  dir: .infra
  preconditions:
    - sh: "[ -f .infra/terraform.tfvars ]"
      msg: "Missing terraform.tfvars. Copy from terraform.tfvars.example"
  cmds:
    - terraform init
    - terraform apply

infra:destroy:
  desc: Destroy infrastructure (use with caution)
  dir: .infra
  preconditions:
    - sh: "[ '{{.CONFIRM}}' = 'yes' ]"
      msg: "Must set CONFIRM=yes to destroy infrastructure"
  cmds:
    - terraform destroy

Usage:

task infra:plan
task infra:apply
CONFIRM=yes task infra:destroy

State Management

Terraform state MUST be stored remotely:

  • Use S3 + DynamoDB for state storage and locking
  • Never commit .tfstate files to version control
  • Document state backend configuration in .infra/backend.tf

Application repository backend configuration:

# .infra/backend.tf
terraform {
  backend "s3" {
    bucket         = "ontopix-tfstate"
    key            = "services/my-service/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "ontopix-tflocks"
  }
}

Central infra repository backend configuration:

# global/backend.tf
terraform {
  backend "s3" {
    bucket         = "ontopix-tfstate"
    key            = "global/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "ontopix-tflocks"
  }
}

Environment Separation

Infrastructure configurations SHOULD support multiple environments:

Option 1: Workspaces (Simple)

terraform workspace select dev
terraform apply

terraform workspace select prod
terraform apply

Option 2: Variable Files (Explicit)

.infra/
├─ main.tf
├─ variables.tf
├─ dev.tfvars
├─ staging.tfvars
└─ prod.tfvars
terraform apply -var-file=dev.tfvars
terraform apply -var-file=prod.tfvars

Option 3: Separate Directories (Isolated)

.infra/
├─ dev/
│  ├─ main.tf
│  └─ variables.tf
├─ staging/
│  ├─ main.tf
│  └─ variables.tf
└─ prod/
   ├─ main.tf
   └─ variables.tf

Choose based on:

  • Workspaces: Shared configuration, simple separation
  • Variable files: Shared configuration, explicit environment differences
  • Separate directories: Complete isolation, maximum safety for production

AI Agent Rules

Agents MUST request human approval before:

  • Running task infra:apply
  • Running task infra:destroy
  • Modifying Terraform configurations
  • Changing infrastructure state

Agents MAY:

  • Run task infra:plan to show proposed changes
  • Read Terraform configurations
  • Suggest infrastructure improvements

Agents MUST NOT:

  • Apply infrastructure changes without explicit human approval
  • Destroy infrastructure without explicit human approval
  • Modify production infrastructure without human oversight
  • Apply production infrastructure from non-master branches

Documentation in AGENTS.md

Repositories with infrastructure MUST document in AGENTS.md:

  • What infrastructure this repository provisions
  • How to view infrastructure plans (task infra:plan)
  • When infrastructure changes require human approval (always)
  • What infrastructure dependencies exist

Example for application repository:

## Infrastructure

This repository provisions:
- RDS PostgreSQL database
- ElastiCache Redis cluster
- S3 bucket for file storage
- IAM roles for service access

**View infrastructure plan:**

task infra:plan


**Apply infrastructure changes (requires human approval):**

task infra:apply


**⚠️ Agents: ALWAYS request human approval before running `infra:apply` or `infra:destroy`.**

Example for central infra repository:

## Infrastructure

This is the **central infrastructure repository** for Ontopix. It provisions foundational platform infrastructure:

- Route53 DNS management
- IAM roles and policies
- Cost alerts and budget monitoring
- Terraform state backend (S3 + DynamoDB)

**View infrastructure plan:**

task infra:plan


**Apply infrastructure changes (requires human approval):**

task infra:apply


**Initialize backend:**

task bootstrap:init


**⚠️ Agents: ALWAYS request human approval before running `infra:apply` or `infra:destroy`.**

This repository does NOT use the `.infra/` convention because it IS the infrastructure repository.

Module Reusability

Shared Terraform modules SHOULD live in:

  • The engineering-patterns repository (for Ontopix-standard patterns)
  • A dedicated Terraform modules repository (for complex shared modules)
  • The Terraform Registry (for open-source public modules)

Shared modules SHOULD NOT live in .infra/ of individual repositories.

Example module reference:

# .infra/main.tf
module "database" {
  source = "github.com/ontopix/engineering-patterns//terraform/modules/postgres?ref=v1.2.0"

  name        = "my-service-db"
  environment = var.environment
}

Security Considerations

Secrets Management

  • NEVER commit secrets to .infra/ configurations
  • Use AWS Secrets Manager, Parameter Store, or similar
  • Reference secrets in Terraform, don't define them

Least Privilege

  • Infrastructure MUST use least-privilege IAM policies
  • Service accounts MUST have minimal required permissions
  • Production infrastructure MUST require additional approval gates

Audit Trail

  • All infrastructure changes MUST be tracked in version control
  • Infrastructure changes SHOULD go through pull requests
  • Production changes MUST be reviewed by multiple engineers
  • Production changes MUST only be applied from the master branch

Rationale

The .infra/ convention exists because:

  • Infrastructure should live close to the code it supports
  • Each repository should own its infrastructure
  • Consistent location reduces cognitive load
  • Agents and humans know where to find infrastructure code

The central infra repository exception exists because:

  • The platform itself needs a home
  • Foundational infrastructure shouldn't be scattered
  • Bootstrapping requires a central coordination point

See decisions/adr-0004-infra-layout.md for the full decision context.