Best Practices and Production

The Script That Broke Production

Three years into my PowerShell journey, I considered myself proficient. I'd written hundreds of scripts. Then I wrote one that brought down a production service.

The script worked perfectly in dev and test. But in production, it:

  • Had no error handling for a specific edge case

  • Used hardcoded credentials (from a test account that didn't exist in prod)

  • Didn't log what it was doing

  • Had no rollback mechanism

  • Wasn't tested with production-sized data

The outage lasted 2 hours. The post-mortem was uncomfortable. But I learned more from that failure than from 100 successful scripts. This article is every lesson I learned the hard way, so you don't have to.

Code Organization and Structure

Script Template

Use this template for production scripts:

<#
.SYNOPSIS
    Brief description of what the script does.

.DESCRIPTION
    Detailed description including prerequisites, dependencies, and expected behavior.

.PARAMETER ParameterName
    Description of what this parameter does.

.EXAMPLE
    .\Script-Name.ps1 -Parameter "Value"
    Description of what this example does.

.NOTES
    Author: Your Name
    Date: 2024-01-15
    Version: 1.0.0
    
.LINK
    https://docs.yourcompany.com/scripts/script-name
#>

#Requires -Version 7.0
#Requires -Modules Az.Compute

[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$true, HelpMessage="Enter the resource name")]
    [ValidateNotNullOrEmpty()]
    [string]$ResourceName,
    
    [Parameter(Mandatory=$false)]
    [ValidateSet("Development", "Staging", "Production")]
    [string]$Environment = "Development"
)

#region Configuration
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"  # Faster execution

$logPath = "C:\Logs\$(Split-Path -Leaf $PSCommandPath)-$(Get-Date -Format 'yyyy-MM-dd').log"
$configPath = "C:\Config\config.json"
#endregion

#region Functions
function Write-ScriptLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Message,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet("INFO", "WARNING", "ERROR")]
        [string]$Level = "INFO"
    )
    
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp [$Level] $Message"
    
    # Write to file
    $logMessage | Out-File -FilePath $logPath -Append
    
    # Write to console
    switch ($Level) {
        "ERROR"   { Write-Error $Message }
        "WARNING" { Write-Warning $Message }
        default   { Write-Verbose $Message }
    }
}

function Test-Prerequisites {
    [CmdletBinding()]
    param()
    
    Write-ScriptLog "Checking prerequisites..."
    
    # Check required modules
    $requiredModules = @("Az.Compute", "Az.Storage")
    foreach ($module in $requiredModules) {
        if (-not (Get-Module -Name $module -ListAvailable)) {
            throw "Required module not found: $module"
        }
    }
    
    # Check configuration file
    if (-not (Test-Path $configPath)) {
        throw "Configuration file not found: $configPath"
    }
    
    Write-ScriptLog "Prerequisites validated successfully"
}
#endregion

#region Main Script
try {
    Write-ScriptLog "Script started: $PSCommandPath"
    Write-ScriptLog "Parameters: ResourceName=$ResourceName, Environment=$Environment"
    
    # Validate prerequisites
    Test-Prerequisites
    
    # Load configuration
    $config = Get-Content $configPath | ConvertFrom-Json
    Write-ScriptLog "Configuration loaded"
    
    # Main logic here
    if ($PSCmdlet.ShouldProcess($ResourceName, "Process resource")) {
        # Your code here
        Write-ScriptLog "Processing $ResourceName in $Environment environment"
    }
    
    Write-ScriptLog "Script completed successfully"
    exit 0
}
catch {
    Write-ScriptLog "Script failed: $_" -Level "ERROR"
    Write-ScriptLog $_.ScriptStackTrace -Level "ERROR"
    exit 1
}
finally {
    Write-ScriptLog "Script execution ended"
}
#endregion

Naming Conventions

PowerShell Style Guide

PSScriptAnalyzer

Static code analysis tool for PowerShell.

Installation

Usage

Custom Rules

Create .vscode/PSScriptAnalyzerSettings.psd1:

Testing with Pester

Pester is PowerShell's testing framework.

Installation

Basic Test

Running Tests

Secrets Management

Never hardcode credentials or secrets!

Using Environment Variables

Using Azure Key Vault

Using Secret Management Module

CI/CD Integration

GitHub Actions Example

Azure DevOps Example

Performance Optimization

Measure Performance

Optimization Tips

Documentation

Comment Your Code

README.md for Script Repositories

Usage

Configuration

Location and format of configuration files.

Troubleshooting

Common issues and solutions.

Contributing

How to contribute to the project.

License

MIT License

Last updated