We’ve been running a multi-cloud (AWS and Azure) setup for about a year now, and after a few iterations, I think we’ve landed on a clean, maintainable monorepo structure using Terraform. The goal was to keep things DRY, enforce consistency, and still give teams the autonomy they need for their specific cloud resources.
I’ll call our layout “OpenClaw” (named after our internal project). The core idea is separating **environment definitions** from **resource modules**, with a clear inheritance chain for configuration. Here’s the high-level directory tree:
```
openclaw-infra/
├── terraform/
│ ├── modules/ # Reusable resource modules (network, compute, db)
│ │ ├── aws/
│ │ ├── azure/
│ │ └── _shared/ # Cloud-agnostic logic (naming, tagging)
│ ├── envs/ # Environment definitions (live state)
│ │ ├── aws/
│ │ │ ├── prod/
│ │ │ ├── staging/
│ │ │ └── _region/ # Region-specific base config
│ │ └── azure/
│ │ ├── prod/
│ │ └── nonprod/
│ └── stacks/ # Composed, cross-cloud deployments
└── cicd/ # Pipeline definitions (GitLab CI in our case)
```
The key is how the `envs/` directory works. Each environment (e.g., `aws/prod`) doesn’t define resources directly. Instead, it calls modules from `modules/` and passes in variables that are specific to that environment and cloud. We use a three-tier variable inheritance:
1. **Base defaults** in `modules/_shared/variables.tf`
2. **Cloud-specific overrides** in `envs/aws/_region/variables.tf`
3. **Environment final values** in `envs/aws/prod/terraform.tfvars`
This means adding a new Azure region, for example, only requires defining the differences from the Azure base.
Here’s a snippet from an `envs/aws/prod/main.tf` showing how a VPC module is invoked:
```hcl
module "prod_vpc" {
source = "../../../modules/aws/network/vpc"
cidr_block = var.vpc_cidr
environment = "prod"
az_count = 2
tags = local.global_tags
}
```
The `stacks/` directory is for when we need to orchestrate something that spans both clouds, like a global DNS entry or a security baseline. Those deployments have their own state files and reference outputs from the environment states via `terraform_remote_state` data sources.
So far, this has helped us a lot with:
* **State isolation:** Each environment and stack has its own state file, limiting blast radius.
* **Consistent tagging & naming:** Enforced through the `_shared` modules.
* **Clear learning path:** New engineers can understand the layout in an afternoon.
I’m curious how others are structuring their multi-cloud repos. What’s working for you, and what would you do differently with this layout?