Introduction
In modern cloud engineering, organizations increasingly adopt multi-cloud strategies to improve resilience, avoid vendor lock-in, and optimize cost.
In this project, I designed and deployed infrastructure across:
- Amazon Web Services (AWS)
- Microsoft Azure
Using a single Infrastructure as Code (IaC) tool:
- π Terraform
This article explains the architecture, configuration, deployment steps, and lessons learned.
Project Architecture Overview
AWS Resources
- EC2 Instance (Ubuntu Server)
- RDS MySQL Database
Azure Resources
- Resource Group
- Virtual Network + Subnet
- Windows Virtual Machine
- IIS Web Server
- Public IP + NSG
All provisioned using Terraform providers for both clouds.
Step 1: Install Prerequisites
Before starting, install:
- Terraform
- AWS CLI
- Azure CLI
- Git
- VS Code
Install Terraform
HashiCorp distributes Terraform as an executable CLI that you can install on supported operating systems, including Microsoft Windows, macOS, and several Linux distributions. You can also compile the Terraform CLI from source if a pre-compiled binary is not available for your system.
If you use a package manager to install software on your macOS, Windows, or Linux system, you can use it to install Terraform. For this project, the focus will be on the Windows installation.
Chocolatey https://chocolatey.org/is a free and open-source package management system for Windows. If you have Chocolatey installed, use it to install Terraform from your command line.
choco install terraform
NOTE: HashiCorp does not maintain Chocolatey or the Terraform package. The latest version of Terraform is always available for you to download and install manually. Refer to the Manual installation tab below for instructions.
Verify the Installation
Verify that the installation worked by opening a new terminal session and listing Terraform's available subcommands.
terraform -help
Install AWS CLI
- For Windows:
- Download the installer from AWS CLI official page (docs.aws.amazon.com in Bing).
- Run the .msi installer and follow the prompts.
Verify installation with:
aws --version
Configure AWS CLI
Once installed, configure it with your credentials:
aws configure
It will ask for:
Install Azure CLI
For Windows:
- Download the installer from Microsoftβs official page.
- Run the .msi installer and follow the prompts.
- Verify installation:
az --version
Step 2: Authenticate with Cloud Providers
AWS Login
aws configure
Provide:
- AWS Access Key ID
- AWS Secret Access Key
- Default region name (e.g., us-east-1)
- Default output format (e.g., json)
NOTE: Your AWS Access key and ID must have been created in your AWS console. For security purpose we advise creating a USER and GROUP under IAM resources.
Azure Login
az login
- A browser window will open for you to authenticate with your Azure account.
- Once logged in, the terminal will display your subscription details. Verify subscription:
az account show
Step 3: Define Terraform Providers
Create terraform.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "azurerm" {
features {}
}
Note: terraform.tf is usually used for Terraform settings or backend configuration.
What goes inside: Terraform version, Required providers, Backend configuration
Step 4: Create AWS and Azure Infrastructure
Create main.tf (Complete Working Script):
AWS Resources: EC2 Instance, AWS RDS MySQL Database,
Azure Resources: Resource Group, Virtual Machine, Web Application
############################################
# Get Latest Ubuntu 22.04 AMI
############################################
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
############################################
# Create EC2 Instance
############################################
resource "aws_instance" "isaiah" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "isaiah"
}
}
#Get default VPC
data "aws_vpc" "default" {
default = true
}
#GET subnets in default VPC
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
#Create Security Group for RDS
resource "aws_security_group" "rds_sg" {
name = "isaiah-rds-sg"
vpc_id = data.aws_vpc.default.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # β οΈ for testing only
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
#Create RDS Module
module "db" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
identifier = "isaiah-demodb"
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro" # cheaper for testing
allocated_storage = 20
db_name = "demodb"
username = "isaiah"
password = "IsaiahStrongPassword123!" # change this
port = 3306
publicly_accessible = true
vpc_security_group_ids = [aws_security_group.rds_sg.id]
create_db_subnet_group = true
subnet_ids = data.aws_subnets.default.ids
family = "mysql8.0"
major_engine_version = "8.0"
skip_final_snapshot = true
deletion_protection = false
tags = {
Owner = "Isaiah"
Environment = "dev"
}
}
#add random suffix
resource "random_string" "suffix" {
length = 5
special = false
upper = false
}
#Resource group for Azure
resource "azurerm_resource_group" "isaiah_rg" {
name = "isaiah-webapp-rg"
location = "West us 3"
}
#Add service plan
resource "azurerm_service_plan" "isaiah_plan" {
name = "isaiah-service-plan"
resource_group_name = azurerm_resource_group.isaiah_rg.name
location = azurerm_resource_group.isaiah_rg.location
sku_name = "P1v2"
os_type = "Windows"
}
#Customize windows web app
resource "azurerm_windows_web_app" "isaiah_app" {
name = "isaiah-app-${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.isaiah_rg.name
location = azurerm_resource_group.isaiah_rg.location
service_plan_id = azurerm_service_plan.isaiah_plan.id
site_config {
always_on = true
}
app_settings = {
"ENVIRONMENT" = "Production"
"OWNER" = "Isaiah"
"WEBSITE_RUN_FROM_PACKAGE" = "1"
}
connection_string {
name = "Database"
type = "MySql"
value = "Server=myserver.mysql.database.azure.com;Database=mydb;User Id=isaiah;Password=StrongPassword123;"
}
tags = {
Owner = "Isaiah"
Environment = "Dev"
}
}
# Create Resource Group
resource "azurerm_resource_group" "rg" {
location = var.resource_group_location
name = "${var.prefix}-rg"
}
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = "${var.prefix}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
# Subnet
resource "azurerm_subnet" "subnet" {
name = "${var.prefix}-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
# Public IP
resource "azurerm_public_ip" "public_ip" {
name = "${var.prefix}-public-ip"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
# Network Security Group
resource "azurerm_network_security_group" "nsg" {
name = "${var.prefix}-nsg"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
# RDP (You should replace with your public IP)
security_rule {
name = "Allow-RDP"
priority = 1000
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "*"
destination_address_prefix = "*"
}
# HTTP
security_rule {
name = "Allow-HTTP"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
# Network Interface
resource "azurerm_network_interface" "nic" {
name = "${var.prefix}-nic"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.public_ip.id
}
}
# Associate NSG
resource "azurerm_network_interface_security_group_association" "nsg_association" {
network_interface_id = azurerm_network_interface.nic.id
network_security_group_id = azurerm_network_security_group.nsg.id
}
# Storage Account for Boot Diagnostics
resource "azurerm_storage_account" "diag" {
name = "diag${random_id.rand.hex}"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
account_tier = "Standard"
account_replication_type = "LRS"
}
# Windows Virtual Machine
resource "azurerm_windows_virtual_machine" "vm" {
name = "${var.prefix}-vm"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.nic.id]
size = "Standard_B1s" # cheaper for lab
admin_username = "isaiahadmin"
admin_password = random_password.password.result
os_disk {
name = "${var.prefix}-osdisk"
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-datacenter-azure-edition"
version = "latest"
}
boot_diagnostics {
storage_account_uri = azurerm_storage_account.diag.primary_blob_endpoint
}
}
# Install IIS
resource "azurerm_virtual_machine_extension" "iis_install" {
name = "iis-install"
virtual_machine_id = azurerm_windows_virtual_machine.vm.id
publisher = "Microsoft.Compute"
type = "CustomScriptExtension"
type_handler_version = "1.8"
settings = <<SETTINGS
{
"commandToExecute": "powershell Install-WindowsFeature -Name Web-Server -IncludeManagementTools"
}
SETTINGS
}
# Random ID
resource "random_id" "rand" {
byte_length = 4
}
# Random Password
resource "random_password" "password" {
length = 16
special = true
}
NOTE: main.tf is the main infrastructure definition file.
It contains the actual resources Terraform will create.
Think of it as the core blueprint of your infrastructure.
Step 5: Create Variable.tf file
The main reason for creating variables.tf is for Azure VM. Before deploying a VM in main.tf in Terraform is important because it makes your infrastructure flexible, reusable, organized, and easier to maintain. Below is a clear explanation.
If you create a VM directly in main.tf without variables, you will hard-code values like region, VM name, or size.
variable "prefix" {
description = "Prefix for all resources"
default = "isaiah"
}
variable "resource_group_location" {
description = "Azure region"
default = "west US 3"
}
Step 6: Initialize and Deploy
1. Initialize Terraform:
terraform init
Purpose
Initializes the Terraform working directory.
Why is it important
This command prepares your project so Terraform can run properly.
_What it does
- Downloads providers (e.g., Azure, AWS, Google Cloud)
- Downloads modules
- Creates the .terraform folder
- Sets up the backend if configured
Running terraform init downloads the AWS and Azure provider plugin. It prepares the environment before Terraform can run.
2. Validate Terraform:
terraform validate
Purpose
Checks if the Terraform configuration syntax is correct.
Why it is important
It helps detect errors in your code before deployment.
What it checks
- Syntax errors
- Missing brackets
- Incorrect variable references
- Invalid configuration
It checks if your code is written correctly.
3. terraform plan
Purpose
Shows what Terraform will create, change, or delete before deployment.
Why it is important
It allows you to preview the changes before applying them.
What it shows
- Resources to be created
- Resources to be modified
- Resources to be destroyed
Plan: 3 to add, 0 to change, 0 to destroy.
This means Terraform will create 12 new resources.
Simple explanation
π It shows the execution plan before making changes.
4. terraform apply
Purpose
Executes the plan and creates the infrastructure.
What it does
- Deploys resources to the cloud
- Creates VMs
- Creates networks
- Creates storage
- Updates infrastructure
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
Simple explanation
π It actually builds the infrastructure.
Tree
The main purpose of tree is to visualize how files and folders are organized inside a directory.
Instead of listing files in a simple list like ls, tree shows the relationship between folders and subfolders.
Step 6 β Add Files to Git
git init
git add .
git commit -m "Initial commit: multi-cloud Terraform project"
git remote add origin https://github.com/izibili123/multi-cloud-terraform.git
git branch -M main
git push -u origin main
Step 7 Terraform destroy
In Terraform, the command terraform destroy is used to delete all the infrastructure resources that Terraform created.
This is done to Prevent Unnecessary Cloud Costs
Cloud resources keep running and continue charging money if not deleted.
Using terraform destroy helps you:
- stop billing
- avoid unexpected cloud charges
- clean up test environments
This is very important when working with cloud providers
terraform destroy
Plan: 0 to add, 0 to change, 22 to destroy
Then you must confirm:
Do you really want to destroy all resources?
Terraform will destroy all managed infrastructure.
Enter a value: yes
Plan: 0 to add, 0 to change, 22 to destroy
Key Lessons Learned
- Multi-cloud requires clear provider separation.
- State file protection is critical.
- Always destroy lab infrastructure.
- Azure and AWS networking models differ significantly.
- Infrastructure as Code improves reproducibility and scalability.
Why Multi-Cloud Matters
Organizations use multi-cloud to:
- Reduce vendor dependency
- Improve disaster recovery
- Optimize regional availability
- Compare cost efficiency between providers Terraform makes multi-cloud deployment seamless.
Conclusion
This project demonstrates how Terraform can orchestrate infrastructure across AWS and Azure from a single configuration.
By implementing EC2, RDS, and Azure VM resources, I built a resilient, scalable, and reproducible multi-cloud environment.
Author
Isaiah Izibili
Cloud & DevOps Enthusiast
















Top comments (0)