Module 15 Lesson 5 of 6 🕑 ~60 min

> cat module-15-5-production-automation.md

Writing Production Automation

A script that works once on your laptop is not the same thing as automation you can trust against production. This lesson covers what separates the two: error handling, logging, safe execution, secrets, and the eight rules that keep a mistake from becoming an incident.

1 Error Handling

Professional scripts must expect things to fail: a server offline, an API unavailable, an authentication failure, permission denied, a missing file, a malformed CSV, a user that doesn't exist, a network timeout, a rate limit, a certificate problem. Handle it with try/catch:

try {
    $User = Get-ADUser -Identity "asmith" -ErrorAction Stop
}
catch {
    Write-Error "Unable to locate user: $_"
}

A finally block runs regardless of whether the try succeeded or the catch fired — useful for closing sessions, removing temporary files, closing database connections, or writing a final audit log entry:

try {
    Write-Host "Running operation"
}
catch {
    Write-Host "Operation failed"
}
finally {
    Write-Host "Cleaning up"
}

2 Logging

Automation without logging can become dangerous. If a script disables 500 accounts and someone asks three days later "which accounts did the script disable?", you need an answer that doesn't depend on memory:

$LogFile = "C:\Logs\automation.log"

Add-Content -Path $LogFile -Value "$(Get-Date) - Starting automation"

foreach ($User in $Users) {
    Add-Content -Path $LogFile -Value "$(Get-Date) - Processing $($User.Username)"
}

Good automation logs should ideally record timestamp, action, target, result, error, script version, execution host, and a correlation/run ID (Module 14's correlation-ID concept applies directly to automation logs, not just application ones). In regulated organisations, automation logs may form part of the security audit trail itself.

3 Turning Commands into Scripts & Scheduling

PowerShell becomes most useful once commands live inside .ps1 files, run with .\CheckServers.ps1:

$Servers = @("SERVER01", "SERVER02", "SERVER03")

foreach ($Server in $Servers) {
    $Online = Test-Connection -ComputerName $Server -Count 1 -Quiet
    if ($Online) {
        Write-Host "$Server is online"
    }
    else {
        Write-Host "$Server is offline"
    }
}

That's the whole idea of automation in miniature: repetitive manual work, converted into something that runs the same way every time. Scripts can then run on a schedule — Windows Task Scheduler, Azure Automation, Azure Functions, CI/CD pipelines, GitHub Actions, Azure DevOps, Jenkins, RMM platforms, or (in legacy enterprises) Scheduled Tasks, batch files, VBScript, or custom scheduling software. A scheduled script might export disabled accounts at 01:00, check certificates expiring within 30 days at 02:00, check disk space at 03:00, collect server health at 04:00, and generate an IAM reconciliation report at 05:00.

Automation should ideally be idempotent — running the script twice shouldn't unintentionally perform the same destructive operation twice. This is the exact same idempotency concept Module 13, Lesson 1 introduced for APIs, now applied to scheduled scripts instead of HTTP requests.

4 Execution Policy, Credentials & Secrets

You'll frequently meet "running scripts is disabled on this system." Windows PowerShell offers execution policies — Restricted, RemoteSigned, AllSigned, Unrestricted, Bypass — but understand this important distinction: PowerShell execution policy is not a true security boundary. Microsoft explicitly describes it as a safety feature, not a mechanism designed to stop a determined user from executing code. Never teach "just run Set-ExecutionPolicy Unrestricted" or -ExecutionPolicy Bypass as the fix for every blocked script — real corporate environments enforce actual security through Group Policy, code signing, application control, WDAC, AppLocker, and endpoint security products instead.

Never put credentials directly in scripts:

# Bad
$Username = "administrator"
$Password = "Summer2026!"

# Very bad
$ApiToken = "super-secret-token"

Use managed identities, workload identities, certificate authentication, secret vaults, Azure Key Vault, enterprise password vaults, CI/CD secret stores, or environment-specific secret injection instead. PowerShell also provides SecretManagement abstractions for registered vaults, though Microsoft's PowerShell team now describes the SecretManagement/SecretStore modules as feature complete rather than an area of active development. The broader lesson holds regardless of tooling: secrets should be retrieved securely at runtime, never embedded in source code (Module 13, Lesson 5 covers exactly this rule from the API-security side).

5 Modules, Cloud & Cross-Platform Automation

PowerShell functionality is organised into modules — Get-Module lists loaded ones, Get-Module -ListAvailable lists all installed ones, Import-Module ActiveDirectory loads a specific one, and Install-Module installs new ones. You'll encounter ActiveDirectory, Microsoft.Graph, Microsoft.Entra, ExchangeOnlineManagement, Az, MicrosoftTeams, VMware PowerCLI, AWS Tools for PowerShell, and various vendor-specific modules — review third-party modules before installing them into sensitive environments.

Cloud automation via the Az module family can create or configure virtual machines, storage accounts, networks, security groups, identity resources, Key Vault, and resource groups (connecting straight back to Module 7's Cloud Fundamentals content). PowerShell 7 is cross-platform and runs on Linux and macOS as well as Windows — meaning it can participate in environments spanning Windows servers, Linux servers, containers, Kubernetes, and cloud systems all at once. Not every Windows-specific cmdlet behaves identically cross-platform, though; Get-Service may work differently on Linux, and modules tied to the traditional Windows .NET Framework may require Windows outright. Consider platform compatibility before assuming a script will "just work" somewhere else.

Real enterprise automation often crosses several systems in one workflow — onboarding an employee might read them from the HR system, create an AD account, sync to Entra ID, assign an M365 licence, add security groups, create a mailbox, assign application access, and generate an audit record, with PowerShell participating in several of those steps at once. This is exactly why PowerShell knowledge is so valuable for IAM engineers specifically (Module 10 covers the full provisioning picture this connects to).

6 Legacy Environments & Compatibility

You'll encounter PowerShell 2.0/3.0/4.0, Windows PowerShell 5.1, WMI, COM objects, VBScript, batch scripts, old Exchange snap-ins, older VMware PowerCLI, MSOnline, AzureAD, custom .NET assemblies, and vendor PowerShell snap-ins in large enterprises. Never assume "old = useless" — a legacy script might still support a critical banking system, a factory, a warehouse, an authentication platform, a PKI system, or a network appliance. Migration needs careful testing, not assumption.

A script that runs fine in Windows PowerShell 5.1 can fail in PowerShell 7 because required modules are missing, it depends on .NET Framework, a cmdlet is Windows-only, parameter behaviour changed, authentication behaviour changed, or a vendor module simply isn't supported yet. Microsoft provides compatibility mechanisms for some Windows PowerShell modules, but migration should still be tested, never assumed to just work.

7 Safe Automation — The 8 Rules

Automation can cause enormous damage fast. One manual mistake affects one account; one scripted mistake can affect ten thousand.

  1. Query before changing. Run Get-ADUser before Set-ADUser.
  2. Test small. Before 10,000 users, test 1, then 5, then a controlled test group.
  3. Use -WhatIf. Many cmdlets support it — Remove-Item "C:\ImportantFolder" -WhatIf describes what would happen without actually doing it. Use it wherever it's available.
  4. Validate input. Never assume a CSV is correct — check for empty usernames, duplicate users, invalid departments, malformed email addresses, missing columns, unexpected delimiters, and invalid characters.
  5. Log everything important. Keep an audit record (Section 2 above).
  6. Avoid hardcoded credentials. Use proper secret management (Section 4 above).
  7. Understand rollback. Before making changes, answer "how do I undo this?" — if the answer is unclear, reconsider running the script against production at all.
  8. Peer review. Important production scripts should be reviewed by another engineer before they touch anything real.

8 Script Structure & Style

A well-structured enterprise script generally flows: configuration → functions → input validation → authentication → main processing → error handling → logging → summary:

# Configuration
$LogFile = ".\automation.log"

# Functions
function Write-Log {
    param([string]$Message)
    Add-Content -Path $LogFile -Value "$(Get-Date) - $Message"
}

# Main
Write-Log "Script started"
try {
    Write-Log "Performing operation"
}
catch {
    Write-Log "ERROR: $_"
}
Write-Log "Script finished"

This is far better than a random collection of commands. Readability matters too — compare $u=Import-Csv u.csv;foreach($x in $u){Write-Host $x.Username} against:

$Users = Import-Csv ".\users.csv"

foreach ($User in $Users) {
    Write-Host $User.Username
}

The goal is never writing the fewest characters — it's creating automation another engineer can safely understand six months later, when they have none of the context you have right now.

Lesson Outcome

You should now be able to wrap risky operations in try/catch/finally, log automation actions for later audit, turn repetitive commands into a scheduled, idempotent .ps1 script, explain why execution policy isn't a real security boundary, keep secrets out of source code, recognise legacy PowerShell technology and compatibility risk, and apply all 8 safe-automation rules and a clean script structure to anything you write. Lesson 6 closes the module with PowerShell's cybersecurity applications, hands-on labs, and a final capstone project.