If you manage infrastructure across two or more clouds, you already know the hard problem isn't writing resources — it's keeping them consistent, secure, and auditable as teams grow.
This isn't a Terraform tutorial. It's a guide to production patterns for teams operating multi-cloud infrastructure (AWS + OCI as the reference case), distilled from real projects where a single mistake —corrupted state, exposed secrets, orphaned resources— costs real downtime or unexpected bills.
1. State Management: The Multi-Cloud Achilles Heel
With a single provider, state is just a remote file. In multi-cloud, it's your business map. If it gets corrupted or desynchronized, you lose visibility of what's deployed and where.
Centralized backend with per-environment isolation
Use a single backend (S3 with DynamoDB for locking, or an OCI bucket with OCI Object Storage locking) but with keys scoped by environment and provider:
| Environment | Provider | State Key |
|---|---|---|
| dev | AWS | env:/dev/aws/terraform.tfstate |
| dev | OCI | env:/dev/oci/terraform.tfstate |
| prod | AWS | env:/prod/aws/terraform.tfstate |
| prod | OCI | env:/prod/oci/terraform.tfstate |
This lets you apply changes to one environment without locking others, and keeps clear separation between providers. Use OpenTofu over Terraform if you prefer MPL licensing and want to avoid unilateral license changes.
2. Layered Modular Design
A single directory with 2000 resources is unsustainable. Divide by layer, not by provider:
infrastructure/
└── modules/
│ └── networking/
│ └── compute/
│ └── database/
│ └── storage/
│ └── monitoring/
└── environments/
│ └── dev/
│ │ └── aws/
│ │ └── oci/
│ └── staging/
│ │ └── aws/
│ │ └── oci/
│ └── prod/
│ └── aws/
│ └── oci/
└── terragrunt.hcl Terragrunt as orchestrator
Terragrunt (or Terramate) solves the repetition problem across environments. Each environment inherits base configuration and only overrides what varies:
# environments/prod/terragrunt.hcl
terraform {
source = "../../modules//networking"
}
inputs = {
environment = "prod"
vpc_cidr = "10.0.0.0/16"
region = "us-east-1"
} The same module is reused across dev, staging, and prod with different inputs. This drastically reduces code duplication.
3. Secrets Management: Out of State, Out of Code
Terraform state stores all resource values, including secrets if you pass them as plain variables. An exposed S3 bucket with state leaks database credentials, API keys, and tokens.
Production rules:
- Never rely on
sensitive = trueas protection — it only hides from output, not from state. - Use
vault(HashiCorp Vault, AWS Secrets Manager, OCI Vault) as backend for sensitive variables. - With Terragrunt: use
dependencyblocks to source outputs from other modules without hardcoding. - With OpenTofu:
tofu init -backend-config=backend.hclwith backend config never in the repo.
# Example with AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/database/credentials"
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}
resource "aws_db_instance" "main" {
username = local.db_creds.username
password = local.db_creds.password
} 4. CI/CD: Pipeline as the Single Source of Truth
If someone can run terraform apply from their terminal, they will eventually break something. The CI/CD pipeline must be the only deployment path.
| Phase | What happens | Typical duration |
|---|---|---|
| Plan | tofu plan -out=tfplan + comment on PR | 30-60s |
| Review | Human reviews diff in PR. Approve = trigger apply | variable |
| Apply | Merge to main → GitHub Actions runs auto apply | 2-5 min |
| Drift | Daily cron detects untracked manual changes | scheduled |
Multi-provider GitHub Actions
name: "IaC - Apply"
on:
push:
branches: [main]
paths: ["environments/prod/**"]
jobs:
terraform:
strategy:
matrix:
provider: [aws, oci]
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: tofu init -backend-config=backend-$}{{ matrix.provider }}.hcl
- run: tofu apply -auto-approve tfplan 5. Cost Control from IaC
The cheapest time to prevent unnecessary spending is before the resource is created. Integrate cost policies directly into your IaC pipeline:
- Sentinel / OPA policies: Block deploys that create instances without cost tags, or exceed size thresholds.
- Infracost: Runs on every PR and estimates the cost of the proposed change. Teams see the financial impact before approving.
- Mandatory tagging: The base module for every resource must enforce tags like
Environment,CostCenter,Owner.
# OPA policy: block instances larger than t3.large in dev
deny[msg] {
input.resource_changes[_].type == "aws_instance"
instance_type := input.resource_changes[_].change.after.instance_type
instance_type in ["t3.xlarge", "t3.2xlarge", "m5.xlarge"]
msg := sprintf("Instance %v not allowed in dev", [instance_type])
} 6. Drift Detection and Remediation
In multi-cloud, someone will always touch something from the web console. Drift is inevitable; automated detection is not optional.
Recommendation: a daily pipeline that runs tofu plan on every environment and notifies if it detects untracked changes. If drift exceeds a threshold, a human reviews; if minor, auto-remediation with tofu apply restores desired state.
Summary: Multi-Cloud IaC Checklist
- ☑ Remote state with locking and per-environment/provider keys
- ☑ Modular design: reusable modules, environments as instances
- ☑ Secrets outside state (Vault / Secrets Manager)
- ☑ Single CI/CD pipeline for all deploys
- ☑ Cost estimation on every PR (Infracost)
- ☑ OPA/Sentinel policies for governance
- ☑ Daily drift detection
- ☑ OpenTofu > Terraform to avoid license lock-in
- ☑ No local terraform apply — CI/CD only
Infrastructure as Code is the foundation of any multi-cloud operation that doesn't want to become a headache. Implementing these patterns from day one —or fixing them gradually— separates teams that control their infrastructure from those controlled by it.