Post

Getting Started with Terraform for PowerShell Scripters - Part 3

Advanced Terraform concepts and how to integrate Terraform with your existing PowerShell automation workflows.

Getting Started with Terraform for PowerShell Scripters - Part 3

📚 Series Navigation:

Advanced Terraform for PowerShell Professionals

With the fundamentals of variables, state management, and data sources from Part 2 now mastered, it’s time to explore the advanced concepts that will make you truly productive with Terraform. In this part, we’ll cover modules (Terraform’s equivalent to PowerShell modules), loops and conditionals, and most importantly, how to integrate Terraform with your existing PowerShell automation workflows.

As PowerShell professionals, you understand the power of modular, reusable code. Terraform modules work similarly to PowerShell modules but with infrastructure-specific features that enable enterprise-scale automation.

Terraform Modules: Introduction for PowerShell Users

In PowerShell, we use functions and modules to make code reusable. Here’s a simple introduction to Terraform modules:

1
2
3
4
5
6
7
8
9
10
11
function New-StandardVM {
    param(
        [string]$Name,
        [string]$ResourceGroup,
        [string]$Size = "Standard_DS2_v2"
    )
    # VM creation logic here
}

# Usage
New-StandardVM -Name "webserver" -ResourceGroup "prod-rg"

Terraform modules work similarly - they’re reusable sets of Terraform configuration files. Here’s a basic example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# modules/simple-webapp/main.tf (simplified for introduction)
variable "name" {
  description = "Name of the web application"
  type        = string
}

variable "location" {
  description = "Azure region"
  type        = string
}

variable "resource_group_name" {
  description = "Resource group name"
  type        = string
}

resource "azurerm_service_plan" "example" {
  name                = "${var.name}-plan"
  location            = var.location
  resource_group_name = var.resource_group_name

  os_type  = "Linux"
  sku_name = "P1v2"
}

resource "azurerm_linux_web_app" "example" {
  name                = "examplemortenkr"
  resource_group_name = var.resource_group_name
  location            = azurerm_service_plan.example.location
  service_plan_id     = azurerm_service_plan.example.id

  site_config {}
}

# outputs.tf
output "website_url" {
  value = "https://${azurerm_linux_web_app.example.default_hostname}"
}

Using the module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# main.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.30.0"
    }
  }
}

provider "azurerm" {
  features {}
}

locals {
  location = "West Europe"
}
# Create a resource group for the web application
resource "azurerm_resource_group" "example" {
  name     = "OurWebApp-rg"
  location = local.location
}

# Create the web application using the module
module "webapp" {
  source              = "./modules/simple-webapp"
  name                = "my-webapp"
  location            = local.location
  resource_group_name = azurerm_resource_group.example.name
}

output "website_url" {
  value = module.webapp.website_url
}

Note: This is a basic introduction to modules. We’ll explore advanced module patterns, testing, versioning, and enterprise strategies in Part 6 of this series.

Loops and Conditionals

For Each (Like PowerShell ForEach-Object)

In PowerShell:

1
2
3
4
$rgNames = @("rg1", "rg2", "rg3")
$rgNames | ForEach-Object {
    New-AzResourceGroup -Name $_ -Location "West Europe"
}

In Terraform:

1
2
3
4
5
6
7
8
9
10
11
# main.tf
variable "resource_group_names" {
  type    = list(string)
  default = ["rg1", "rg2", "rg3"]
}

resource "azurerm_resource_group" "example" {
  for_each = toset(var.resource_group_names)
  name     = each.value
  location = "West Europe"
}

Count (Simple Repeated Resources)

1
2
3
4
5
6
7
8
9
# Create 3 similar storage accounts
resource "azurerm_storage_account" "example" {
  count                    = 3
  name                     = "storage${count.index}"
  resource_group_name      = azurerm_resource_group.example.name
  location                 = azurerm_resource_group.example.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

Conditional Resources (Like PowerShell If Statements)

In PowerShell:

1
2
3
4
5
if ($Environment -eq "prod") {
    New-AzAppServicePlan -Name "plan" -Tier "Standard" # Simplified
} else {
    New-AzAppServicePlan -Name "plan" -Tier "Basic" # Simplified
}

In Terraform:

1
2
3
4
5
6
7
8
9
10
11
12
13
variable "environment" {
  type    = string
  default = "dev"
}

resource "azurerm_app_service_plan" "example" {
  # Other properties...

  sku {
    tier = var.environment == "prod" ? "Standard" : "Basic"
    size = var.environment == "prod" ? "S1" : "B1"
  }
}

Switch

If you wanted to do a similar example to above with a switch instead you could take advantage of the locals variables like shown here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
param(
    [string]$environment = "dev"
)

switch ($environment) {
    "prod" {
        $tier = "Standard"
        $size = "S1"
    }
    "staging" {
        $tier = "Standard"
        $size = "S1"
    }
    "dev" {
        $tier = "Basic"
        $size = "B1"
    }
    default {
        throw "Unknown environment: $environment"
    }
}
Write-Output "App Service Plan Tier: $tier"
Write-Output "App Service Plan Size: $size"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
variable "environment" {
  type    = string
  default = "dev"
}

locals {
  tiers = {
    prod    = "Standard"
    staging = "Standard"
    dev     = "Basic"
  }
  sizes = {
    prod    = "S1"
    staging = "S1"
    dev     = "B1"
  }
}

resource "azurerm_app_service_plan" "example" {
  sku {
    tier = local.tiers[var.environment]
    size = local.sizes[var.environment]
  }
}

Integrating Terraform with PowerShell

Running PowerShell Before/After Terraform

Terraform can’t do everything - sometimes you need PowerShell for specific tasks. Here’s how to integrate them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# deployment.ps1
param(
    [string]$Environment = "dev",
    [string]$Region = "westus"
)

# Run Terraform
Write-Host "Running Terraform deployment..."
terraform init
$tfResult = terraform apply -auto-approve -var "environment=$Environment" -var "location=$Region"

if ($LASTEXITCODE -ne 0) {
    Write-Error "Terraform deployment failed"
    exit 1
}

# Extract outputs
$websiteUrl = terraform output -raw website_url

# Post-deployment tasks
Write-Host "Performing post-deployment tasks..."
Invoke-RestMethod -Uri "$websiteUrl/api/warmup" -Method Post

Using Terraform Output in PowerShell

1
2
3
4
5
6
7
8
9
10
# Get all outputs as JSON usefull for larger datasets in the output that you need to work with
$outputs = terraform output -json | ConvertFrom-Json

# Use specific outputs
$storageAccountName = terraform output -raw storage_account_name
$connectionString = terraform output -raw connection_string

# Use in further automation
$context = New-AzStorageContext -ConnectionString $connectionString
$container = New-AzStorageContainer -Name "data" -Context $context

Using local-exec Provisioner

Terraform can run PowerShell commands directly note that when doing this it is no longer following the state of terraform and you will have to implement this your self in the scripts you are running:

Note that this will be run each time you run terraform apply/destroy since it does not follow the state like terraform does

1
2
3
4
5
6
7
resource "azurerm_storage_account" "example" {
  # Storage account configuration...

  provisioner "local-exec" {
    command = "pwsh -Command \"New-AzStorageContainer -Name 'data' -Context (New-AzStorageContext -StorageAccountName ${self.name} -StorageAccountKey ${self.primary_access_key}) -Permission Off\""
  }
}

Practical Example: Complete Environment with PowerShell Integration

Here’s a more complete example that combines Terraform and PowerShell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# main.tf - Terraform configuration
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.30.0"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "environment" {
  type = string
}

variable "location" {
  type = string
}

resource "azurerm_resource_group" "example" {
  name     = "app-${var.environment}-rg"
  location = var.location
}

module "webapp" {
  source              = "./modules/webapp"
  name                = "app-${var.environment}"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  sku_tier            = var.environment == "prod" ? "Standard" : "Basic"
}

module "database" {
  source              = "./modules/database"
  name                = "db-${var.environment}"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  tier                = var.environment == "prod" ? "GeneralPurpose" : "Basic"
}

output "website_url" {
  value = module.webapp.app_url
}

output "database_connection_string" {
  value     = module.database.connection_string
  sensitive = true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# deploy.ps1 - PowerShell wrapper
param(
    [Parameter(Mandatory=$true)]
    [ValidateSet("dev", "test", "prod")]
    [string]$Environment,

    [Parameter(Mandatory=$true)]
    [string]$Location
)

# Ensure Azure login
$account = Get-AzContext
if (-not $account) {
    Connect-AzAccount
}

# Run pre-deployment validation
Write-Host "Validating deployment prerequisites..." -ForegroundColor Cyan

if ($Environment -eq "prod") {
    $approval = Read-Host "You're deploying to PRODUCTION. Type 'yes' to confirm"
    if ($approval -ne "yes") {
        Write-Host "Deployment cancelled" -ForegroundColor Red
        exit
    }
}

# Initialize Terraform
terraform init

# Plan and apply
Write-Host "Creating Terraform plan..." -ForegroundColor Cyan
terraform plan -var "environment=$Environment" -var "location=$Location" -out=tfplan

Write-Host "Applying Terraform plan..." -ForegroundColor Cyan
terraform apply tfplan

# Get outputs for further processing
Write-Host "Deployment completed, processing outputs..." -ForegroundColor Green
$websiteUrl = terraform output -raw website_url
$connectionString = terraform output -raw database_connection_string

# Post-deployment steps
Write-Host "Performing post-deployment configuration..." -ForegroundColor Cyan

# Configure app settings
$settings = @{
    "WEBSITE_NODE_DEFAULT_VERSION" = "~16"
    "DATABASE_CONNECTION_STRING" = $connectionString
    "ENVIRONMENT" = $Environment.ToUpper()
}

$webAppName = "app-$Environment"
Set-AzWebApp -ResourceGroupName "app-$Environment-rg" -Name $webAppName -AppSettings $settings

Write-Host "Deployment and configuration complete!" -ForegroundColor Green
Write-Host "Website URL: $websiteUrl" -ForegroundColor Yellow

Introduction to Advanced State Management

As your Terraform usage grows beyond simple configurations, you’ll need to consider remote state for team collaboration. Here’s a basic introduction:

Using Azure Storage for Remote State

1
2
3
4
5
6
7
8
9
# backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-state-rg"
    storage_account_name = "tfstate23942"
    container_name       = "tfstate"
    key                  = "prod.terraform.tfstate"
  }
}

Note: This is a basic introduction to remote state. For comprehensive coverage of enterprise state management, workspaces, team collaboration patterns, and security considerations, see Part 4: Advanced State Management and Collaboration.

PowerShell Script to Set Up Remote State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# setup-terraform-state.ps1
param(
    [string]$ResourceGroup = "terraform-state-rg",
    [string]$Location = "westus",
    [string]$StorageAccountName = "tfstate$(Get-Random -Minimum 10000 -Maximum 99999)",
    [string]$ContainerName = "tfstate"
)

# Create resource group
New-AzResourceGroup -Name $ResourceGroup -Location $Location -Force

# Create storage account
$storageAccount = New-AzStorageAccount -ResourceGroupName $ResourceGroup `
                                      -Name $StorageAccountName `
                                      -Location $Location `
                                      -SkuName Standard_LRS `
                                      -Kind StorageV2 `
                                      -EnableHttpsTrafficOnly $true

# Create blob container
$context = $storageAccount.Context
New-AzStorageContainer -Name $ContainerName -Context $context -Permission Off

# Output configuration for backend.tf
$backendConfig = @"
terraform {
  backend "azurerm" {
    resource_group_name  = "$ResourceGroup"
    storage_account_name = "$StorageAccountName"
    container_name       = "$ContainerName"
    key                  = "terraform.tfstate"
  }
}
"@

Set-Content -Path "./backend.tf" -Value $backendConfig

Write-Host "Terraform backend configuration created in backend.tf" -ForegroundColor Green
Write-Host "Resource Group: $ResourceGroup"
Write-Host "Storage Account: $StorageAccountName"
Write-Host "Container: $ContainerName"

Conclusion and Next Steps

In this third part of our PowerShell-to-Terraform series, you’ve mastered advanced concepts that bring enterprise-level capabilities to your infrastructure automation:

What We’ve Achieved:

  1. Terraform Modules: Created reusable infrastructure components (like PowerShell modules)
  2. Advanced Loops: Used for_each and count for dynamic resource creation
  3. Conditional Logic: Implemented infrastructure decisions based on variables and data
  4. PowerShell Integration: Built hybrid automation workflows combining both tools
  5. Error Handling: Advanced debugging and troubleshooting techniques
  6. Performance Optimization: Module design patterns for scale and maintainability

PowerShell Developer Advantages: Now you can leverage your existing PowerShell skills while gaining Terraform’s declarative infrastructure benefits. You understand how to:

  • Design modular infrastructure like you design PowerShell modules
  • Integrate Terraform into existing PowerShell automation workflows
  • Debug infrastructure issues using familiar PowerShell techniques
  • Scale infrastructure management using proven software development patterns

From PowerShell Scripter to Infrastructure Engineer:

PowerShell ConceptsTerraform EquivalentIntegration Benefits
Functions/ModulesTerraform ModulesReusable infrastructure patterns
ForEach-Objectfor_each expressionsDynamic resource provisioning
If/Switch statementsConditional expressionsEnvironment-specific logic
Error handlingProvider error managementRobust deployment workflows
DebuggingTF_LOG and plan analysisInfrastructure troubleshooting

Infrastructure Maturity Progression: ✅ Foundation → ✅ Variables & State → ✅ Advanced Integration → Collaboration → Testing → Modules → CI/CD

Coming Next: In Part 4, we’ll tackle the enterprise challenges of state management and team collaboration - learning how to work with Terraform in team environments, implement proper state backends, and manage infrastructure at scale with multiple contributors.

You’re now equipped to build complex, modular infrastructure with PowerShell integration!

This post is licensed under CC BY 4.0 by the author.