Quick answer: To use AWS, Azure and GCP in one Terraform configuration, declare all three providers in required_providers, add a provider block for each with its region or project, authenticate each one through environment variables or CLI login rather than hard-coded keys, and then write resources prefixed aws_, azurerm_ or google_. Terraform downloads every plugin during terraform init and routes each resource to the right cloud automatically.
This guide walks through a complete multi-cloud setup: declaring the providers, authenticating safely to each cloud, creating a storage bucket on all three platforms from a single apply, using aliases for multiple regions or accounts, and passing providers into modules. You will also see when a multi-cloud configuration is a good idea and when it is better to keep clouds in separate root modules.
How Terraform handles several providers at once
Terraform core is cloud-agnostic. Each provider is a separate plugin, and a single configuration can load as many as it needs. During init, Terraform reads required_providers, downloads each plugin from the registry and records exact versions in .terraform.lock.hcl. During plan and apply, it starts each provider as its own process and sends each resource to the provider whose prefix it carries. From your point of view, an aws_s3_bucket and a google_storage_bucket sit side by side in the same file and the same dependency graph.
If the idea of a provider is new to you, read Terraform Core Concepts and Syntax: A Beginner’s Guide first.
Step 1: Declare and configure the three providers
Put version requirements and provider configuration in a dedicated providers.tf:
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region # e.g. ap-south-1 (Mumbai)
}
provider "azurerm" {
features {} # required block, even when empty
subscription_id = var.azure_subscription_id
}
provider "google" {
project = var.gcp_project_id
region = var.gcp_region # e.g. asia-south1 (Mumbai)
}
Notice that no credentials appear in the file. Each provider has a documented chain of places it looks for authentication, and all of them are better than literal keys.
Step 2: Authenticate to each cloud safely
The original advice of putting access_key and secret_key in the provider block is exactly what you should not do β those files end up in Git. Use the environment instead:
| Cloud | Local development | CI/CD pipeline | Environment variables read by the provider |
|---|---|---|---|
| AWS | aws configure or aws sso login, then AWS_PROFILE |
OIDC β IAM role (GitHub Actions, GitLab) | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_PROFILE |
| Azure | az login |
Workload identity federation or service principal | ARM_CLIENT_ID, ARM_TENANT_ID, ARM_SUBSCRIPTION_ID, ARM_CLIENT_SECRET or ARM_USE_OIDC |
| GCP | gcloud auth application-default login |
Workload identity federation | GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_PROJECT |
Each provider also lets you set these values as arguments (for example client_id on azurerm), which is useful when values come from a secrets manager at runtime β but never as plain strings committed to the repository.
Step 3: Create resources on all three clouds
With providers configured, the resources themselves are ordinary. The example below creates an object storage bucket in each cloud and exposes the three names as outputs. Storage names must be globally unique, so a random suffix is added:
resource "random_id" "suffix" {
byte_length = 3
}
# AWS
resource "aws_s3_bucket" "assets" {
bucket = "tkh-assets-${random_id.suffix.hex}"
}
# Azure (storage accounts need a resource group; names are lowercase, max 24 chars)
resource "azurerm_resource_group" "assets" {
name = "rg-tkh-assets"
location = "Central India"
}
resource "azurerm_storage_account" "assets" {
name = "tkhassets${random_id.suffix.hex}"
resource_group_name = azurerm_resource_group.assets.name
location = azurerm_resource_group.assets.location
account_tier = "Standard"
account_replication_type = "LRS"
}
# GCP
resource "google_storage_bucket" "assets" {
name = "tkh-assets-${random_id.suffix.hex}"
location = "ASIA-SOUTH1"
force_destroy = true
}
output "bucket_names" {
value = {
aws = aws_s3_bucket.assets.bucket
azure = azurerm_storage_account.assets.name
gcp = google_storage_bucket.assets.name
}
}
Run terraform init (which now downloads four plugins, including random), then terraform plan and terraform apply. One command, three clouds. Because all three resources share a dependency on random_id.suffix, Terraform creates that first and then provisions the buckets in parallel.
Step 4: Use aliases for multiple regions or accounts
A second configuration of the same provider needs an alias. This is how you deploy to two AWS regions, two Azure subscriptions or two GCP projects from one root module:
provider "aws" {
region = "ap-south-1"
}
provider "aws" {
alias = "singapore"
region = "ap-southeast-1"
}
resource "aws_s3_bucket" "primary" {
bucket = "tkh-data-primary"
}
resource "aws_s3_bucket" "replica" {
provider = aws.singapore
bucket = "tkh-data-replica"
}
Resources without a provider argument use the default (unaliased) configuration. When calling a module, pass aliased providers explicitly:
module "dr_site" {
source = "./modules/web"
providers = {
aws = aws.singapore
}
}
Inside the module, declare the providers it expects in required_providers but do not add provider blocks β modules should receive provider configurations from the root, never define their own.
Should everything live in one configuration?
One root module for three clouds is a great demo and sometimes the right design β for example when a GCP DNS zone must point at an AWS load balancer and you want that dependency expressed in code. But it also means one state file, one blast radius and one set of credentials covering every cloud. For production, many teams split by cloud or by layer (network, data, application) and connect them with terraform_remote_state data sources or outputs stored in a parameter store. Choose the structure that matches how your teams and permissions are organised, not what fits in a single file.
Six multi-provider mistakes to avoid
- Hard-coding credentials in
providerblocks. Use environment variables, CLI logins or OIDC; never commit keys. - Forgetting
features {}onazurerm. It is mandatory even when empty; omitting it fails atinit. - Unpinned provider versions. Three providers means three chances for a surprise major upgrade. Pin all of them and commit the lock file.
- Defining provider blocks inside modules. Pass providers in via the
providersargument instead. - Ignoring naming rules per cloud. Azure storage account names are lowercase alphanumeric only and at most 24 characters; S3 bucket names allow hyphens; GCP bucket names are globally unique. Validate names with
validationblocks on your variables. - One giant state for everything. Split by cloud or layer once the configuration grows beyond a prototype.
Frequently asked questions
Does Terraform run the three providers in parallel?
Yes. Terraform builds one dependency graph and walks it with up to ten concurrent operations by default (-parallelism), regardless of which provider each resource belongs to.
Can I use the same variable names across clouds?
Yes, but prefix them for clarity β aws_region, azure_location, gcp_region β because the three clouds name regions differently (ap-south-1, Central India, asia-south1 all mean Mumbai or nearby).
What happens if one cloud’s credentials are missing?
terraform plan fails for that provider as soon as it tries to authenticate, and nothing is applied anywhere. Terraform does not partially apply across providers.
Do I need the random provider?
Not strictly, but it is the simplest way to generate unique bucket and storage account names, and it is maintained by HashiCorp like the three cloud providers.
Key takeaways
- Declare every provider in
required_providerswith a pinned version, and add oneproviderblock per cloud. - Authenticate via environment variables, CLI logins or OIDC β never literal keys in HCL.
- Resource prefixes (
aws_,azurerm_,google_) tell Terraform which cloud to call. - Use
aliasfor multiple regions or accounts and pass providers into modules explicitly. - Split large multi-cloud setups by cloud or layer to limit blast radius.
Ready to deploy across AWS, Azure and GCP with confidence? Our DevOps course covers Terraform and multi-cloud infrastructure with live projects, mentor support and placement assistance. For step-by-step video demos, subscribe to our YouTube channel.

