:PROPERTIES: :ID: bf63b6c5-f32f-462e-82ad-8d4a15f7ba48 :END: #+DATE: 2026-04-10 #+filetags: :powershell:scripts:ess:esp:microlise: #+STARTUP: showall #+title: ESP — PowerShell Script Architecture * Overview :PROPERTIES: :RELATED: [[id:a6c345df-8db9-4538-b87e-0e72e2414905][ESP Deployment Pipeline — Index]] :END: The deployment scripts are structured in *three tiers*, loosely analogous to a presentation/domain/data layered architecture in software. This separation exists to: - Keep ESP-specific knowledge isolated to higher tiers - Allow Tier 3 functions to be tested in isolation - Allow the scripts to run independently of AzDO - Make incremental automation easier (replace manual prompts one function at a time) * Entry Point — How to Call the Scripts #+BEGIN_SRC powershell Invoke-Deployment.ps1 -Customer ROMAC -Environment DEV -Stages PREDEPLOY,DEPLOY,POSTDEPLOY #+END_SRC You can pass any combination of stages. For example, to only validate: #+BEGIN_SRC powershell Invoke-Deployment.ps1 -Customer ROMAC -Environment DEV -Stages VALIDATE #+END_SRC * Tier 1 — Entry Point (Presentation Layer) ** Responsibility - The *single entry point* to the whole deployment system - Loads and validates the manifest (see [[id:9bf19a8b-5581-4be8-9892-913c51df0128][ESP — The Manifest]]) - Loads all required PowerShell modules (Tier 2, Tier 3, helpers) - Calls the appropriate Tier 2 functions for the requested stages - Passes the manifest object through to Tier 2 ** Key Characteristics - One script: ~Invoke-Deployment.ps1~ - Does NOT contain deployment logic itself - Acts as a wiring layer only ** Pseudocode #+BEGIN_SRC powershell param( [string]$Customer, [string]$Environment, [string[]]$Stages ) $manifest = Load-Manifest -Customer $Customer -Environment $Environment Assert-ManifestValid -Manifest $manifest Import-Module ./Tier2/PreDeployment.psm1 Import-Module ./Tier2/Deployment.psm1 Import-Module ./Tier2/PostDeployment.psm1 Import-Module ./Helpers/Logging.psm1 foreach ($stage in $Stages) { switch ($stage) { "PREDEPLOY" { Invoke-PreDeployment -Manifest $manifest } "DEPLOY" { Invoke-Deployment -Manifest $manifest } "POSTDEPLOY" { Invoke-PostDeployment -Manifest $manifest } } } #+END_SRC * Tier 2 — Orchestration Layer (Domain Layer) ** Responsibility - One script per deployment stage (e.g. ~PreDeployment.psm1~, ~Deployment.psm1~) - Contains the *logic and sequencing* of what needs to happen - Decides *which* Tier 3 functions to call and in what order - Reacts to return values from Tier 3 (e.g. if a function fails, abort or retry) ** Key Constraints - *Does NOT* pass the manifest to Tier 3 — all required data is extracted and passed as explicit parameters - *Does NOT* directly perform any action itself — delegates entirely to Tier 3 - *Does* have knowledge of ESP and what a deployment involves ** Why "Does Not Pass Manifest to Tier 3"? Tier 3 functions are designed to be generic and reusable. If they accepted a manifest object, they'd need to know about its structure — breaking isolation. Instead, Tier 2 extracts what Tier 3 needs: #+BEGIN_SRC powershell # BAD — passes the whole manifest (couples Tier 3 to manifest structure) Stop-WindowsService -Manifest $manifest # GOOD — extracts what's needed and passes explicitly Stop-WindowsService -ServiceName $manifest.Services.DLService.Name ` -ServerName $manifest.Servers.AppServer #+END_SRC ** Scripts in This Tier | Script | Stage it covers | |-----------------------+---------------------| | ~Validation.psm1~ | VALIDATE stage | | ~Prerequisites.psm1~ | PREREQUISITES stage | | ~PreDeployment.psm1~ | PREDEPLOY stage | | ~Deployment.psm1~ | DEPLOY stage | | ~PostDeployment.psm1~ | POSTDEPLOY stage | * Tier 3 — Functional Layer (Workers) ** Responsibility - Small, *single-purpose* functions - No knowledge of ESP, the manifest, or deployment context - Accept all required data as *explicit parameters* - Can be tested in isolation with mock data ** Key Characteristics - Examples: ~Stop-WindowsService~, ~Invoke-SqlScript~, ~Copy-Files~, ~Get-RemoteSession~, ~Send-CitrixNotification~ - Initially implemented as *manual prompt wrappers* — the function prints instructions to a human and waits for confirmation - Will be replaced with real automation incrementally ** The Manual Prompt Pattern (Current State) #+BEGIN_SRC powershell function Stop-WindowsService { param([string]$ServiceName, [string]$ServerName) # Current implementation — prompts a human $response = Invoke-ManualPrompt -Message "Please stop service '$ServiceName' on '$ServerName', then press Enter." return $response } #+END_SRC ** Target Implementation (Automated) #+BEGIN_SRC powershell function Stop-WindowsService { param([string]$ServiceName, [string]$ServerName) $session = Get-RemoteSession -ServerName $ServerName Invoke-Command -Session $session -ScriptBlock { Stop-Service -Name $using:ServiceName -Force (Get-Service -Name $using:ServiceName).WaitForStatus('Stopped', '00:01:00') } } #+END_SRC ** Exceptions — Functions That Cannot Be Manual Prompts A small number of Tier 3 functions *must* be implemented for real from the start because they return data the scripts need to function: | Function | Why it can't be a manual prompt | |---------------------+-------------------------------------------------------| | ~Load-Manifest~ | Returns the manifest object — must actually read file | | ~Get-RemoteSession~ | Returns a PS session — must actually connect | | ~Read-File~ | Returns file contents — must actually read | * Helper Modules In addition to the three tiers, *general-purpose helper modules* exist that can be called from anywhere (Tier 1, 2, or 3): | Module | Purpose | |----------------+--------------------------------------------| | ~Logging.psm1~ | Write structured log output | | ~Prompt.psm1~ | The manual prompt mechanism used by Tier 3 | | | | Helpers follow the same rule as Tier 3: *the manifest is never passed to them*. * Testing Strategy Because Tier 3 functions are isolated, they can be unit tested without any connection to a real ESP environment: #+BEGIN_SRC powershell # Example Pester test for Stop-WindowsService Describe "Stop-WindowsService" { It "calls Invoke-Command with correct service name" { Mock Invoke-Command {} Mock Get-RemoteSession { return [PSCustomObject]@{ Session = "MockSession" } } Stop-WindowsService -ServiceName "DLService" -ServerName "SERVER01" Assert-MockCalled Invoke-Command -Times 1 } } #+END_SRC * Incremental Automation Plan The architecture is designed so that automation can be added *one function at a time*, without restructuring anything: 1. Deploy with all Tier 3 functions as manual prompts (guided checklist) 2. Identify the lowest-risk, simplest functions to automate first 3. Replace manual prompts with real implementations one by one 4. Each replacement can be independently tested before deployment Suggested automation order (rough): 1. ~Stop-WindowsService~ / ~Start-WindowsService~ — well-understood, low risk 2. ~Copy-Files~ — straightforward file operations 3. ~Invoke-SqlScript~ — once databases are in dacpac-ready state 4. Citrix-related functions — last, most complex * Architecture Diagram (Text) #+BEGIN_SRC AzDO Pipeline (YAML) │ └─► Invoke-Deployment.ps1 [TIER 1] │ Loads manifest │ Loads all modules │ ├─► PreDeployment.psm1 [TIER 2] │ ├─► Copy-Files [TIER 3] │ └─► ... │ ├─► Deployment.psm1 [TIER 2] │ ├─► Stop-WindowsService [TIER 3] │ ├─► Invoke-SqlScript [TIER 3] │ ├─► Start-WindowsService [TIER 3] │ └─► ... │ └─► PostDeployment.psm1 [TIER 2] ├─► Get-ServiceStatus [TIER 3] └─► ... [Helpers: Logging, Prompt — available at any tier] #+END_SRC ** Mermaid Diagram (Text) #+BEGIN_SRC mermaid graph TD %% Tier 1 subgraph "Tier 1 - Entry Point" A[Invoke-Deployment.ps1] end %% Tier 2 subgraph "Tier 2 - Deployment Phases" B[PreDeployment.psm1] C[Deployment.psm1] D[PostDeployment.psm1] end %% Tier 3 subgraph "Tier 3 - Actions" E[Copy-Files] F[Stop-WindowsService] G[Invoke-SqlScript] H[Start-WindowsService] I[Get-ServiceStatus] end %% Helpers subgraph "Helpers (All Tiers)" J[Logging Helper] K[Prompt Helper] end %% Relationships A --> B A --> C A --> D B --> E C --> F C --> G C --> H D --> I A --> J A --> K #+END_SRC