Infrastructure Layout
Pattern for organizing infrastructure-as-code using the .infra/ convention in repositories.
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
infrarepository) - 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
.tfstatefiles 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:planto 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
masterbranch
Related Patterns
- Taskfile as Contract — Exposing infrastructure operations
- AI Agent Entrypoint — Agent rules for infrastructure
- Repository Structure — Where
.infra/lives
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.