Module-Based Pipeline Abstraction for Multi-Platform Portability¶
Context and Problem Statement¶
The XMS Landing Zone supports multiple customers and may need to run on different CI/CD platforms (Azure DevOps, GitHub Actions, GitLab CI). Platform-specific pipeline YAML files with embedded business logic create several problems:
- Duplicated logic across different platform definitions
- Complex YAML that is difficult to test and debug
- Vendor lock-in to specific pipeline syntax
- Inability to run deployments locally or test logic outside CI/CD
- Changes require modifying multiple pipeline files to maintain parity
- Error handling and logging logic is scattered across YAML definitions
How can we maintain deployment consistency across multiple automation platforms while keeping pipelines simple and maintainable?
Decision Drivers¶
- Need to support multiple customers with different CI/CD platform preferences
- Desire to test deployment logic locally without running full pipelines
- Requirement for unit testing of deployment orchestration
- Goal to reduce pipeline complexity and maintenance burden
- Need for consistent behavior across development, CI, and production environments
- Desire to version deployment logic independently from pipeline infrastructure
- Need to handle platform-specific operations (artifact uploads, logging) without duplicating core logic
Considered Options¶
- Module-based abstraction (thin pipelines + PowerShell module)
- Pipeline templates with shared YAML
- Separate implementations per platform
- Container-based CLI tool
Decision Outcome¶
Chosen option: "Module-based abstraction", because it provides the best balance of portability, testability, and maintainability.
All deployment logic, configuration parsing, Terraform orchestration, and validation is implemented in the Xebia.Mcp PowerShell module at src/Xebia.Mcp/. Pipeline YAML files are thin wrappers that only handle:
- Triggers and scheduling
- Authentication and credential setup
- Calling PowerShell functions with parameters
- Publishing artifacts and test results
- Matrix strategies for parallel execution
Implementation Pattern¶
Pipeline YAML (thin orchestrator):
# Azure DevOps
- task: PowerShell@2
displayName: 'Create release plan'
inputs:
pwsh: true
targetType: inline
script: |
Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
$config = Resolve-XebiaMcpConfiguration -Environment 'prod'
Invoke-XebiaMcpReleasePlan -Config $config -Region 'neu' -DeployName 'foundation' -Level '1'
# GitHub Actions
- name: Create release plan
shell: pwsh
run: |
Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
$config = Resolve-XebiaMcpConfiguration -Environment 'prod'
Invoke-XebiaMcpReleasePlan -Config $config -Region 'neu' -DeployName 'foundation' -Level '1'
PowerShell module contains all logic:
- Parallelism decisions (firewall=1, postdeployment=10, others=100)
- Terraform state backend configuration
- Plan file creation and JSON summary generation
- Artifact upload commands
- Warning detection and formatting
- Deployment matrix calculation
- Configuration validation
Module Organization¶
Functions are organized by domain in src/Xebia.Mcp/Functions/Public/:
| Folder | Purpose | Example Functions |
|---|---|---|
Config/ |
Configuration file access and validation | Get-XebiaEnvironmentItem, Test-XebiaJsonSchema |
Runner/ |
Pipeline-facing operations (may contain platform-specific commands) | Invoke-XebiaMcpReleasePlan, Invoke-XebiaMcpReleaseApply |
Terraform/ |
Terraform operations | Invoke-XebiaTerraformPlan, Initialize-TerraformWorkspace |
Pester/ |
Test execution | Invoke-XebiaPesterTests, Format-PesterResults |
Logging/ |
Logging and output | Write-XebiaLog, Format-DeploymentSummary |
Runner Functions are the only functions that may contain platform-specific commands (e.g., ##vso[artifact.upload] for Azure DevOps). All other functions are platform-agnostic.
Consequences¶
- Good, because deployment logic can be tested with Pester unit tests
- Good, because developers can run deployments locally using the same functions as CI/CD
- Good, because migrating between platforms requires minimal effort (only YAML changes)
- Good, because business logic lives in one place, reducing duplication
- Good, because PowerShell functions can be versioned and released independently
- Good, because error handling and validation logic is consistent across all platforms
- Good, because debugging is easier (step through PowerShell locally vs. pipeline reruns)
- Bad, because developers must learn PowerShell in addition to Terraform
- Bad, because Runner functions may contain platform-specific commands requiring conditional logic
- Neutral, because PowerShell is available on all major CI/CD platforms (Azure DevOps, GitHub Actions, GitLab CI)
Validation¶
Compliance is validated through:
- Code review: New pipeline logic should be rejected if not extracted to module
- Test coverage: All public functions must have corresponding
.Unit.Tests.ps1files - Pipeline complexity metrics: YAML files should remain under 200 lines
- Equivalence testing: Azure DevOps and GitHub Actions pipelines must call identical functions
- Standards enforcement:
docs/coding-standards/pipeline-architecture.mdprovides guidance for all AI tools
Implementation Examples¶
Example 1: Release Planning¶
Before (embedded in pipeline):
- script: |
PARALLELISM=100
if [ "$DEPLOY_NAME" = "firewall" ]; then
PARALLELISM=1
elif [ "$DEPLOY_NAME" = "postdeployment" ]; then
PARALLELISM=10
fi
terraform -chdir=infra/deploy/$DEPLOY_NAME plan \
-parallelism=$PARALLELISM \
-out=plan.tfplan \
-var-file=config/environments/$ENVIRONMENT/$REGION/global.auto.tfvars
terraform show -json plan.tfplan > plan.json
CREATES=$(jq '.resource_changes | map(select(.change.actions | contains(["create"]))) | length' plan.json)
DESTROYS=$(jq '.resource_changes | map(select(.change.actions | contains(["delete"]))) | length' plan.json)
echo "##vso[task.logissue type=warning]Plan: $CREATES to add, $DESTROYS to destroy"
displayName: 'Create Terraform plan'
After (module-based):
- task: PowerShell@2
displayName: 'Create release plan'
inputs:
pwsh: true
targetType: inline
script: |
Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
$config = Resolve-XebiaMcpConfiguration -Environment '${{ parameters.environment }}'
Invoke-XebiaMcpReleasePlan -Config $config -Region '${{ parameters.region }}' -DeployName '${{ parameters.deployName }}' -Level '${{ parameters.level }}'
All the parallelism logic, state backend configuration, plan parsing, and warning emission is now in Invoke-XebiaMcpReleasePlan where it can be tested and reused.
Example 2: Configuration Validation¶
Before (embedded in pipeline):
- script: |
for file in config/environments/$ENVIRONMENT/**/*.json; do
SCHEMA=$(echo $file | sed 's/.*\///' | sed 's/-.*//')
if ! test-json -Schema schemas/${SCHEMA}.schema.json -Path $file; then
echo "##vso[task.logissue type=error]Invalid JSON: $file"
exit 1
fi
done
displayName: 'Validate configuration files'
After (module-based):
- task: PowerShell@2
displayName: 'Validate configuration'
inputs:
pwsh: true
targetType: inline
script: |
Import-Module ./src/Xebia.Mcp/Xebia.Mcp.psd1
Test-XebiaMcpConfig -Environment '${{ parameters.environment }}'
The validation logic, schema discovery, error formatting, and reporting is in Test-XebiaMcpConfig and tested with Pester.
Related Decisions¶
- ADR-0009: Serialize firewall layer apply for rule collection group updates - Example of parallelism control logic in the module
More Information¶
Documentation:
docs/coding-standards/pipeline-architecture.md- Detailed guidance for implementing this patterndocs/coding-standards/powershell-functions.md- Function structure and standardsdocs/coding-standards/powershell-unit-tests.md- Test coverage requirementsCONTRIBUTING.md- Quick start guide for contributors
Code examples:
src/Xebia.Mcp/Functions/Public/Runner/Invoke-XebiaMcpReleasePlan.ps1- Pipeline-facing function with platform-specific commandssrc/Xebia.Mcp/Functions/Public/Terraform/Invoke-XebiaTerraformPlan.ps1- Platform-agnostic core logicsrc/Xebia.Mcp/Functions/Public/Config/Test-XebiaJsonSchema.ps1- Configuration validation logic
Testing:
src/Xebia.Mcp/Functions/Public/**/*.Unit.Tests.ps1- Unit test examplessrc/Xebia.Mcp/Tests/Smoke/- Smoke test examples
This decision was formalized on 2026-05-21 based on existing implementation patterns already in use. The ADR documents the rationale and serves as guidance for future development.