How to Use Terraform with Azure and AWS from Windows Server 2025
Terraform’s real power emerges when you use it to provision actual cloud resources. Windows Server 2025 makes an excellent Terraform workstation because PowerShell, the Azure CLI, and the AWS CLI all integrate smoothly with native Windows tooling. This tutorial takes you from provider authentication through provisioning an Azure Virtual Machine and an AWS EC2 instance and S3 bucket — covering both the azurerm and aws Terraform providers in depth. By the end you will have running cloud resources created entirely from declarative configuration files executed from a Windows Server 2025 PowerShell session.
Prerequisites
- Terraform installed and on PATH (see the Terraform installation tutorial)
- Windows Server 2025 with PowerShell 7.x recommended
- An active Azure subscription and an Azure Active Directory account with Contributor rights
- An AWS account with IAM credentials that have appropriate EC2/S3 permissions
- Administrator rights to install software on the server
Step 1: Install the Azure CLI on Windows Server 2025
The Azure CLI allows you to authenticate with Azure interactively, which the azurerm Terraform provider can then reuse — avoiding the need to manage long-lived service principal secrets in development environments.
# Download the Azure CLI MSI installer
$AzCliUrl = "https://aka.ms/installazurecliwindowsx64"
$AzCliMsi = "$env:TEMPAzureCLI.msi"
Write-Host "Downloading Azure CLI installer..."
Invoke-WebRequest -Uri $AzCliUrl -OutFile $AzCliMsi -UseBasicParsing
# Silent install — no UI, no restart prompt
Start-Process msiexec.exe -ArgumentList "/i `"$AzCliMsi`" /qn /norestart" -Wait -NoNewWindow
# Reload PATH so az.cmd is found in the current session
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path", "User")
# Verify installation
az --version
Step 2: Authenticate with Azure
For interactive development, use az login. For automated or CI pipelines, use a service principal with environment variables. Both methods are shown below.
# Interactive browser-based login (opens browser on desktop sessions)
az login
# After login, list available subscriptions
az account list --output table
# Set the target subscription
az account set --subscription "00000000-0000-0000-0000-000000000003"
# Confirm active subscription
az account show --output table
Once logged in via the Azure CLI, the azurerm Terraform provider automatically uses these cached credentials when you set use_cli = true in the provider block (the default). Alternatively, for service principal authentication without the CLI, set these environment variables:
# Service principal credentials for non-interactive environments
$env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000001"
$env:ARM_CLIENT_SECRET = "YourSPSecret~Example!"
$env:ARM_TENANT_ID = "00000000-0000-0000-0000-000000000002"
$env:ARM_SUBSCRIPTION_ID = "00000000-0000-0000-0000-000000000003"
Step 3: Provision Azure Resources with Terraform
Create a working directory and write a complete main.tf that provisions a resource group, virtual network, subnet, public IP, network interface, and a Linux virtual machine on Azure.
New-Item -ItemType Directory -Path "C:TerraformProjectsazure-demo" -Force | Out-Null
Set-Location "C:TerraformProjectsazure-demo"
@'
terraform {
required_version = ">= 1.8.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.110"
}
}
}
provider "azurerm" {
features {}
# When using az login, the provider uses CLI credentials automatically.
# For service principals, set ARM_* environment variables.
}
# ── Resource Group ────────────────────────────────────────────────────────────
resource "azurerm_resource_group" "demo" {
name = "ws2025-demo-rg"
location = "East US"
}
# ── Virtual Network ───────────────────────────────────────────────────────────
resource "azurerm_virtual_network" "demo" {
name = "ws2025-demo-vnet"
resource_group_name = azurerm_resource_group.demo.name
location = azurerm_resource_group.demo.location
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "demo" {
name = "demo-subnet"
resource_group_name = azurerm_resource_group.demo.name
virtual_network_name = azurerm_virtual_network.demo.name
address_prefixes = ["10.0.1.0/24"]
}
# ── Public IP and NIC ─────────────────────────────────────────────────────────
resource "azurerm_public_ip" "demo" {
name = "ws2025-demo-pip"
resource_group_name = azurerm_resource_group.demo.name
location = azurerm_resource_group.demo.location
allocation_method = "Static"
sku = "Standard"
}
resource "azurerm_network_interface" "demo" {
name = "ws2025-demo-nic"
resource_group_name = azurerm_resource_group.demo.name
location = azurerm_resource_group.demo.location
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.demo.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.demo.id
}
}
# ── Linux VM ──────────────────────────────────────────────────────────────────
resource "azurerm_linux_virtual_machine" "demo" {
name = "ws2025-demo-vm"
resource_group_name = azurerm_resource_group.demo.name
location = azurerm_resource_group.demo.location
size = "Standard_B2s"
admin_username = "azureuser"
network_interface_ids = [azurerm_network_interface.demo.id]
admin_ssh_key {
username = "azureuser"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
tags = {
Environment = "Demo"
ManagedBy = "Terraform"
}
}
output "vm_public_ip" {
value = azurerm_public_ip.demo.ip_address
}
'@ | Set-Content -Path "main.tf" -Encoding UTF8
# Initialise providers, then plan and apply
terraform init
terraform plan -out="azure.tfplan"
terraform apply "azure.tfplan"
# Retrieve the public IP of the provisioned VM
terraform output vm_public_ip
Step 4: Configure AWS Credentials from PowerShell
AWS credentials can be provided via the AWS CLI aws configure command, which writes to ~/.aws/credentials, or via environment variables which take precedence over the file. For Windows Server build agents and automation scenarios, environment variables are preferred.
# Option A: Use the AWS CLI config wizard
# Requires AWS CLI installed: winget install Amazon.AWSCLI
aws configure
# Follow the interactive prompts for Access Key, Secret, Region, output format
# Option B: Environment variables (takes precedence over ~/.aws/credentials)
$env:AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
$env:AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
$env:AWS_DEFAULT_REGION = "us-east-1"
# Confirm credentials work
aws sts get-caller-identity
Step 5: Provision AWS Resources with Terraform
Create a second working directory for the AWS project. The configuration below provisions a VPC, subnet, security group, EC2 instance, and S3 bucket.
New-Item -ItemType Directory -Path "C:TerraformProjectsaws-demo" -Force | Out-Null
Set-Location "C:TerraformProjectsaws-demo"
@'
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
# Credentials sourced from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars
# or from ~/.aws/credentials if env vars are absent
}
# ── VPC & Networking ──────────────────────────────────────────────────────────
resource "aws_vpc" "demo" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "ws2025-demo-vpc" }
}
resource "aws_subnet" "demo" {
vpc_id = aws_vpc.demo.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "us-east-1a"
tags = { Name = "ws2025-demo-subnet" }
}
resource "aws_internet_gateway" "demo" {
vpc_id = aws_vpc.demo.id
tags = { Name = "ws2025-demo-igw" }
}
resource "aws_route_table" "demo" {
vpc_id = aws_vpc.demo.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.demo.id
}
}
resource "aws_route_table_association" "demo" {
subnet_id = aws_subnet.demo.id
route_table_id = aws_route_table.demo.id
}
# ── Security Group ────────────────────────────────────────────────────────────
resource "aws_security_group" "demo" {
name = "ws2025-demo-sg"
description = "Allow SSH inbound, all outbound"
vpc_id = aws_vpc.demo.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ── EC2 Instance ──────────────────────────────────────────────────────────────
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "demo" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.demo.id
vpc_security_group_ids = [aws_security_group.demo.id]
root_block_device {
volume_size = 20
volume_type = "gp3"
delete_on_termination = true
}
tags = {
Name = "ws2025-demo-ec2"
Environment = "Demo"
ManagedBy = "Terraform"
}
}
# ── S3 Bucket ─────────────────────────────────────────────────────────────────
resource "aws_s3_bucket" "demo" {
bucket = "ws2025-demo-bucket-tf-unique-9312"
tags = {
Environment = "Demo"
ManagedBy = "Terraform"
}
}
resource "aws_s3_bucket_versioning" "demo" {
bucket = aws_s3_bucket.demo.id
versioning_configuration {
status = "Enabled"
}
}
# ── Outputs ───────────────────────────────────────────────────────────────────
output "ec2_public_ip" {
value = aws_instance.demo.public_ip
}
output "s3_bucket_name" {
value = aws_s3_bucket.demo.bucket
}
'@ | Set-Content -Path "main.tf" -Encoding UTF8
# Run the full Terraform workflow for AWS
terraform init
terraform plan -out="aws.tfplan"
terraform apply "aws.tfplan"
# Show outputs
terraform output
# When done testing, destroy everything
terraform destroy -auto-approve
Step 6: Destroy Azure Resources When Done
Set-Location "C:TerraformProjectsazure-demo"
terraform destroy -auto-approve
Conclusion
You have now provisioned real Azure and AWS resources from Terraform configurations running on Windows Server 2025. By combining the Azure CLI’s interactive authentication with service principal environment variables for automation, and using AWS environment variables alongside the AWS CLI profile system, you can manage both clouds from the same Windows Server build agent. The declarative approach means your infrastructure definitions are version-controlled, peer-reviewable, and reproducible — the same terraform apply command will produce identical infrastructure on every run, eliminating configuration drift and manual provisioning errors. From here, explore Terraform workspaces for managing multiple environments (dev, staging, production) from a single configuration and variable files (terraform.tfvars) to parameterize environment-specific values.