Overview: Using Terraform with Azure and AWS from Windows Server 2022

Windows Server 2022 is frequently used as a CI/CD build agent, jump host, or automation server in enterprise environments. Terraform running on Windows Server 2022 can provision resources in both Microsoft Azure and Amazon Web Services (AWS) using the same HCL configuration workflow. This guide covers provider authentication, real infrastructure examples for both clouds, state management, Terraform workspaces and modules, code quality tools, and integration with Jenkins and GitHub Actions self-hosted runners on Windows.

Azure Provider Authentication Methods

The azurerm Terraform provider supports multiple authentication methods. The most common in server environments are Service Principal with client secret, Service Principal with client certificate, Azure CLI credentials, and Managed Identity.

Service Principal with client secret is the most widely used for automation. Create a Service Principal using the Azure CLI:

az ad sp create-for-rbac --name "terraform-sp" --role="Contributor" --scopes="/subscriptions/YOUR_SUBSCRIPTION_ID"

This returns a JSON object with appId, password, and tenant. Set these as environment variables in PowerShell before running Terraform:

$env:ARM_CLIENT_ID       = "appId-value"
$env:ARM_CLIENT_SECRET   = "password-value"
$env:ARM_TENANT_ID       = "tenant-value"
$env:ARM_SUBSCRIPTION_ID = "your-subscription-id"

Azure CLI authentication is convenient for interactive development. Install the Azure CLI on Windows Server 2022, sign in, and the azurerm provider will pick up the session automatically:

az login
az account set --subscription "your-subscription-id"

Managed Identity is the preferred authentication method when your Windows Server 2022 host is an Azure VM with a system-assigned or user-assigned managed identity. In that case, set no credential environment variables; instead, tell the provider to use MSI:

provider "azurerm" {
  features {}
  use_msi = true
}

Configuring the azurerm Provider Block

A complete azurerm provider block in main.tf:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.100"
    }
  }
}

provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = false
    }
    key_vault {
      purge_soft_delete_on_destroy = true
    }
  }
}

Creating an Azure Resource Group, VNet, and VM with Terraform

The following configuration creates a Resource Group, a Virtual Network with a subnet, a public IP, a network interface, and a Windows Server 2022 virtual machine:

resource "azurerm_resource_group" "main" {
  name     = "rg-demo-eastus"
  location = "East US"
}

resource "azurerm_virtual_network" "main" {
  name                = "vnet-demo"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
}

resource "azurerm_subnet" "main" {
  name                 = "snet-demo"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_public_ip" "main" {
  name                = "pip-demo"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  allocation_method   = "Static"
}

resource "azurerm_network_interface" "main" {
  name                = "nic-demo"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.main.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.main.id
  }
}

resource "azurerm_windows_virtual_machine" "main" {
  name                = "vm-demo"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  size                = "Standard_B2s"
  admin_username      = "adminuser"
  admin_password      = var.admin_password

  network_interface_ids = [azurerm_network_interface.main.id]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2022-datacenter-azure-edition"
    version   = "latest"
  }
}

variable "admin_password" {
  type      = string
  sensitive = true
}

Run this with:

terraform init
terraform plan -var="admin_password=YourSecureP@ssw0rd"
terraform apply -var="admin_password=YourSecureP@ssw0rd" -auto-approve

AWS Provider Authentication

The AWS Terraform provider authenticates using the standard AWS credential chain: environment variables, shared credentials file, IAM instance profile. On Windows Server 2022, environment variables are the most explicit and reliable for automation:

$env:AWS_ACCESS_KEY_ID     = "AKIAIOSFODNN7EXAMPLE"
$env:AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
$env:AWS_DEFAULT_REGION    = "us-east-1"

The AWS provider block in your configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.50"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

Creating an EC2 Instance with Terraform

The following creates a security group and an EC2 instance running Amazon Linux 2023:

resource "aws_security_group" "web_sg" {
  name        = "web-sg"
  description = "Allow HTTP and SSH"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  tags = {
    Name = "TerraformWebServer"
  }
}

Creating an S3 Bucket with Terraform from Windows

S3 bucket names must be globally unique. The following creates a versioned, encrypted S3 bucket:

resource "aws_s3_bucket" "data" {
  bucket = "my-terraform-data-bucket-20260517"

  tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "data" {
  bucket                  = aws_s3_bucket.data.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Managing Terraform State and Workspaces

Terraform state tracks all resources Terraform manages. By default it is stored as terraform.tfstate locally. For teams and production use, use a remote backend (S3 or Azure Blob). Terraform workspaces allow you to manage multiple instances of the same configuration — for example, dev, staging, and production — using a single set of .tf files with a shared backend.

# Create a new workspace
terraform workspace new staging

# List workspaces
terraform workspace list

# Switch to an existing workspace
terraform workspace select production

# Show the current workspace name
terraform workspace show

Within your configuration, reference the current workspace name using terraform.workspace:

resource "azurerm_resource_group" "main" {
  name     = "rg-demo-${terraform.workspace}"
  location = "East US"
}

Using Terraform Modules

Modules are reusable, composable Terraform configurations. A module is simply a directory of .tf files. Call a local module from a root configuration:

module "network" {
  source   = "./modules/network"
  location = "East US"
  vnet_cidr = "10.1.0.0/16"
}

Call a public module from the Terraform Registry:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
}

terraform fmt and terraform validate

terraform fmt rewrites Terraform configuration files to canonical format and style. Run it before committing code:

terraform fmt -recursive

terraform validate checks the configuration for syntax errors and internal consistency without accessing any remote services or state:

terraform validate

Successful output:

Success! The configuration is valid.

Running Terraform in Jenkins on Windows

To run Terraform in a Jenkins pipeline on a Windows Server 2022 Jenkins agent, use a declarative pipeline with a bat or powershell step. Store credentials as Jenkins credentials and inject them as environment variables:

pipeline {
    agent { label 'windows-server-2022' }

    environment {
        ARM_CLIENT_ID       = credentials('azure-client-id')
        ARM_CLIENT_SECRET   = credentials('azure-client-secret')
        ARM_TENANT_ID       = credentials('azure-tenant-id')
        ARM_SUBSCRIPTION_ID = credentials('azure-subscription-id')
    }

    stages {
        stage('Terraform Init') {
            steps {
                bat 'terraform init'
            }
        }
        stage('Terraform Plan') {
            steps {
                bat 'terraform plan -out=tfplan.binary'
            }
        }
        stage('Terraform Apply') {
            steps {
                bat 'terraform apply tfplan.binary'
            }
        }
    }
}

Running Terraform in GitHub Actions with a Windows Self-Hosted Runner

For GitHub Actions using a Windows Server 2022 self-hosted runner, a typical workflow file looks like this:

name: Terraform

on:
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: self-hosted
    defaults:
      run:
        shell: pwsh

    steps:
      - uses: actions/checkout@v4

      - name: Terraform Init
        run: terraform init
        env:
          ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
          ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
          ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}

      - name: Terraform Plan
        run: terraform plan -out=tfplan.binary
        env:
          ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
          ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
          ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}

      - name: Terraform Apply
        run: terraform apply tfplan.binary
        env:
          ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
          ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
          ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}

Summary

Terraform on Windows Server 2022 is fully capable of managing both Azure and AWS infrastructure. Authentication via Service Principal for Azure and access key environment variables for AWS provides a secure and automation-friendly setup. Real infrastructure resources — VMs, VNets, EC2 instances, S3 buckets — are all manageable from the same Windows host. Terraform workspaces cleanly separate environments, modules promote reuse, and the fmt/validate commands enforce quality. CI/CD integration with Jenkins and GitHub Actions self-hosted Windows runners closes the loop for fully automated infrastructure provisioning.