Stop Reinventing the Wheel: A Production-Ready Multi-Cloud Terraform Starter Kit
Starting multi-cloud IaC from scratch means week one is S3/GCS/Blob backends, DynamoDB locking, and KMS key setup. Week two is IAM policy JSON. Week three the security team asks about IMDSv2. Week four prod and dev have drifted because someone applied from their laptop.
You haven't written a single piece of application infrastructure yet.
The CloudCheers Multi-Cloud IaC Starter Kit skips all of that — production-hardened modules for AWS, GCP, and Azure, environment progression, and a full CI/CD pipeline, ready to fork.
The core design constraint: production defaults, relaxed per environment — not the other way around. Missing deletion_protection = true in dev gets copy-pasted into prod. Static access keys in GitHub Secrets rotate once and then get leaked. The kit makes the safe path the default path.
Repository Structure
multi-cloud-iac-starter/
├── modules/
│ ├── aws/
│ │ ├── vpc/ # VPC, subnets, NAT gateways, VPC flow logs
│ │ ├── eks/ # EKS cluster, managed node groups, IRSA bootstrap
│ │ ├── rds/ # Aurora PostgreSQL + KMS + no public endpoint
│ │ └── s3/ # Bucket + server-side encryption + block public access
│ ├── gcp/
│ │ ├── vpc/ # VPC, private service connect, Cloud NAT
│ │ ├── gke/ # GKE Autopilot + Workload Identity bindings
│ │ ├── cloud-sql/ # Cloud SQL + CMEK + private IP only
│ │ └── gcs/ # GCS bucket + CMEK + uniform bucket-level access
│ └── azure/
│ ├── vnet/ # VNet, NSGs, private endpoints
│ ├── aks/ # AKS + Azure AD workload identity federation
│ ├── postgresql/ # Flexible Server + BYOK + no public access
│ └── blob-storage/ # Storage account + CMK + private endpoint
├── environments/
│ ├── dev/ # aws / gcp / azure — each with its own backend
│ ├── staging/
│ └── prod/
├── global/
│ ├── dns/ # Route53 hosted zone + ACM wildcard cert
│ └── iam/ # GitHub Actions OIDC providers + least-privilege policies
└── .github/workflows/
├── terraform-plan.yml # Validate + security gates + cost diff on PR
└── terraform-apply.yml # Apply on merge, environment approvals for staging/prod
Each module is a self-contained unit with its own variables.tf, outputs.tf, and defaults. Environment directories call modules with environment-specific overrides. No environment branching logic lives inside modules — that pattern causes drift and untestable code paths.
Security Defaults
KMS encryption everywhere. S3, RDS, EBS, GCS, Cloud SQL, Azure PostgreSQL — all use customer-managed keys with 90-day rotation. You can bring your own key ARN. You cannot opt out. Checkov gates CKV_AWS_7 and CKV_GCP_42 fail the PR if you try.
No public endpoints. RDS, Cloud SQL, and Azure Flexible Server default to publicly_accessible = false. EKS, GKE, and AKS API servers default to private endpoint with an authorized CIDR allowlist. Setting enable_public_endpoint = true in dev is allowed but surfaces a Checkov warning in the PR comment — visible and documented, not silent.
OIDC keyless CI/CD. No static credentials in GitHub Secrets. The global/iam/ module provisions OIDC providers in each cloud that federate trust to GitHub Actions via repository/branch subject claims:
# global/iam/github-oidc.tf
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = { "token.actions.githubusercontent.com:sub" : "repo:${var.github_org}/${var.github_repo}:*" }
StringEquals = { "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com" }
}
}]
})
}
GCP uses Workload Identity Federation with the same pattern. Azure uses federated credentials on the App Registration. Workflows call aws-actions/configure-aws-credentials@v4 with role-to-assume — no aws-access-key-id anywhere.
IRSA / Workload Identity. EKS bootstraps IAM Roles for Service Accounts via the cluster OIDC issuer. GKE enables Workload Identity on node pools with the iam.workloadIdentityUser binding. Pods get a scoped role — not a node-level instance profile inherited by everything running on the host.
IMDSv2 enforced. All EC2 instances and EKS node groups default to http_tokens = "required". Mandatory, not optional.
CI/CD Pipeline
terraform-plan.yml runs on every PR that touches any .tf file. All jobs set soft_fail: false — failures block the merge rather than posting advisory warnings that get ignored:
jobs:
validate:
steps:
- run: terraform fmt -check -recursive
- run: terraform init -backend=false && terraform validate
security-scan:
needs: validate
steps:
- uses: aquasecurity/tfsec-action@v1
with: { soft_fail: false }
- uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: terraform
soft_fail: false
skip_check: "CKV_AWS_144" # cross-region replication — opt-in per workload
cost-diff:
needs: validate
steps:
- run: |
infracost diff --path environments/${{ env.TARGET_ENV }}/aws \
--format json --out-file /tmp/infracost.json
infracost comment github --path /tmp/infracost.json \
--repo $GITHUB_REPOSITORY \
--pull-request ${{ github.event.pull_request.number }}
Every PR gets an Infracost comment showing the monthly cost delta before anything merges. The comment breaks down cost by resource, so a reviewer can immediately see if a node group resize carries a $2,000/month impact rather than discovering it on the bill.
terraform-apply.yml auto-applies to dev on merge to main. Staging and prod require a named GitHub environment approval gate — no one runs terraform apply locally against prod. The only exception is the one-time bootstrap of OIDC providers and remote state backends, which requires a local credential.
Environment Progression
| Setting | dev | staging | prod |
|---|---|---|---|
| EKS node type | t3.medium |
m6i.large |
m6i.xlarge |
| Node group min/max | 1 / 3 | 2 / 6 | 3 / 20 |
| RDS instance class | db.t4g.medium |
db.r7g.large |
db.r7g.2xlarge |
| Multi-AZ RDS | false |
true |
true |
| Deletion protection | false |
true |
true |
| Public endpoint | allowed | blocked | blocked |
| Backup retention | 3 days | 14 days | 30 days |
| Apply | auto | manual approval | manual approval |
Dev is intentionally destroyable. Staging and prod carry prevent_destroy = true in the lifecycle block on top of cloud-native deletion protection — removing it requires a reviewed PR merge, not a CLI flag.
Quick Start
The kit ships with a .env.example covering every variable needed across all three clouds. Copy it, fill in account IDs and regions, then source it before running any Terraform commands.
# 1. Copy and fill in your environment config
cp .env.example .env
vim .env
source .env
# 2. Bootstrap OIDC providers and remote state — one time per org
cd global/iam
terraform init && terraform apply \
-var="github_org=your-org" \
-var="github_repo=your-repo"
# 3. Deploy dev (per-cloud or all at once with Terragrunt)
cd environments/dev/aws && terraform init && terraform apply
cd environments/dev/gcp && terraform init && terraform apply
cd environments/dev/azure && terraform init && terraform apply
# Or deploy all three clouds simultaneously
cd environments/dev && terragrunt run-all apply
The global bootstrap is the only step that uses a local credential. It provisions the OIDC federation and the remote state backends (S3 + DynamoDB for AWS, GCS for GCP, Azure Storage for Azure) so there is no chicken-and-egg problem with remote state — subsequent runs authenticate entirely through GitHub Actions OIDC.
Who It's For
Startups past MVP who need a reviewable, auditable baseline before the team grows. Compliance-bound teams targeting SOC 2 or HIPAA — every enforced control maps to a specific CC or §164 requirement, and the Checkov gate output is itself audit evidence. Platform teams standardizing IaC across product squads — modules are versioned via Git tags, platform teams own modules, product teams own environment compositions, CODEOWNERS enforces the boundary.
Need it pre-configured for your environment or compliance target? Every default in this kit came from real client engagements — reach out at cloudcheers.com and we can scope what your setup requires.