XMPro Documentation PR Preview Infrastructure
This Terraform module creates temporary Azure infrastructure for previewing XMPro documentation pull requests. It provides isolated environments for each PR to enable reviewers to see documentation changes before they're merged.
Overview
When a PR is created against the documentation, this module:
- Creates isolated Azure resources - Each PR gets its own resource group and storage account
- Deploys static website hosting - Azure Storage Account with static website features enabled
- Uploads DocFX documentation - Pipeline builds and uploads the documentation site
- Provides preview URL - Public URL for reviewers to preview documentation changes
- Automatic cleanup - Resources are destroyed when the PR is closed
Architecture
Resource Layout
Resource Group (rg-xmpro-docs-pr-preview-{pr_number})
└── Storage Account (xmprodocpr{pr_number})
└── Static Website ($web container)
└── DocFX documentation site
State Management
Azure Storage Backend
├── Container: terraform-state-docs
├── State Files:
│ ├── docs-pr-1234.tfstate # PR #1234 state
│ ├── docs-pr-1235.tfstate # PR #1235 state
│ └── docs-pr-1236.tfstate # PR #1236 state
└── Concurrent Build Support: ✓ No locking conflicts
Key Design Principles:
- Complete Isolation: Each PR gets separate resource group and storage account
- Concurrent Safety: PR-specific state files prevent locking conflicts
- Cost Effective: Uses cheapest Azure storage options
- Automatic Cleanup: Resources destroyed when PR closes
Usage
This module is automatically used by Azure DevOps pipelines when PRs are created. It's not intended for manual deployment.
Automated Pipeline Integration
PR Creation/Update:
- Pipeline:
docs/docs-build-pr.yml - Trigger: Pull request to
mainorrelease-*branches - Actions: Build documentation → Deploy preview → Comment on PR with URL
PR Closure:
- Pipeline:
docs/docs-cleanup-pr.yml - Trigger: Pull request closed
- Actions: Destroy Azure resources → Remove state file
State Management Details
Backend Configuration
The module uses a direct initialization process with PR-specific state files:
# main.tf includes backend configuration
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
backend "azurerm" {
# Backend configuration provided via -backend-config during init
}
}
Initialization Process:
# Direct initialization with PR-specific state file
terraform init \
-backend-config="storage_account_name=$(storageaccount)" \
-backend-config="container_name=terraform-state-docs" \
-backend-config="key=docs-pr-${PR_NUMBER}.tfstate"
Why This Approach?
Problem: Multiple PRs building simultaneously would conflict on the same state file
Solution: Each PR gets its own state file: docs-pr-{pr_number}.tfstate
Benefits:
- No state locking conflicts between concurrent PR builds
- Isolated state management per PR
- Direct backend configuration prevents state file mismatches
- Consistent state management across pipeline steps
Pipeline Workflow
1. PR Build Pipeline (docs/docs-build-pr.yml)
Triggers: Pull request to main or release-* branches
Stages:
graph LR
A[Build DocFX] --> B[Deploy Preview]
B --> C[Upload Docs]
C --> D[Comment on PR]
Detailed Steps:
- Build Stage: DocFX builds documentation, creates
_siteartifact - Deploy Stage:
- Initialize Terraform directly with PR-specific state file
- Apply Terraform to create/update Azure resources
- Sync documentation to storage account using
az storage blob sync - Display preview URL with changed documentation pages
2. PR Cleanup Pipeline (docs/docs-cleanup-pr.yml)
Triggers: Pull request closed
Actions:
- Initialize Terraform with PR-specific state file
- Run
terraform destroyto remove all Azure resources - State file is automatically removed during destroy
Variables
| Name | Description | Type | Default | Notes |
|---|---|---|---|---|
pr_number |
Pull request number | string | Required | Used in resource naming |
build_id |
Azure DevOps build ID | string | Required | Used for tagging |
location |
Azure region for resources | string | eastus |
Where resources are created |
resource_group_name |
Base name of resource group | string | rg-xmpro-docs-pr-preview |
PR number is appended |
Outputs
| Name | Description | Usage |
|---|---|---|
docs_preview_url |
Static website URL | Posted in PR comments for reviewers |
storage_account_name |
Storage account name | Used by pipeline to upload files |
storage_account_key |
Storage account access key | Used by pipeline for authentication |
resource_group_name |
Full resource group name | Used for resource management |
Azure Resources Created
Per PR Resources
# Resource Group
rg-xmpro-docs-pr-preview-{pr_number}
# Storage Account (with static website)
xmprodocpr{pr_number}
├── $web container (static website files)
├── Primary web endpoint (public URL)
└── Access keys (for upload authentication)
Resource Naming Rules
- Storage Account:
xmprodocpr{pr_number}(max 24 chars, lowercase only) - Resource Group:
rg-xmpro-docs-pr-preview-{pr_number} - State File:
docs-pr-{pr_number}.tfstate
Dependencies
Azure DevOps Variable Groups
terraform-devtestplan-state: Contains backend configurationservice_name: Azure service connection namekey_vault_name: Key Vault containing secretsstorageaccount: Terraform state storage accountcontainer: Container name (overridden toterraform-state-docs)
Azure Key Vault Secrets
Retrieved automatically via terraform-devtestplan-state group:
sas-token: Storage account SAS token for state backendaz-client-id: Service principal client IDaz-client-secret: Service principal secretaz-subscription: Azure subscription IDaz-tenant: Azure tenant ID
Cost Considerations
Resource Costs Per PR:
- Storage Account: ~$0.05/month (Standard LRS, minimal usage)
- Static Website: No additional cost
- Resource Group: No cost
Cost Management:
- Resources are automatically cleaned up when PRs are closed
- Maximum lifetime: Duration of PR (typically days, not months)
- Estimated cost per PR: < $0.50 USD total
Cost Optimization:
- Uses cheapest storage replication (LRS)
- No compute resources (static hosting only)
- Automatic cleanup prevents cost accumulation
Security
Data Protection
- Static websites are publicly accessible (by design for PR reviews)
- HTTPS enforced on all storage account endpoints
- No sensitive data should be included in documentation
- Access keys marked sensitive in Terraform outputs
Authentication & Authorization
- Service Principal authentication for Terraform operations
- Azure Key Vault for secret management
- RBAC permissions controlled via Azure DevOps service connections
- State file access controlled via storage account permissions
Security Best Practices
- Documentation contains only public information
- No API keys, passwords, or internal URLs in docs
- Preview URLs are time-limited (PR lifetime only)
- Regular cleanup prevents data persistence
Cleanup Process
Automatic Cleanup Triggers
Resources are automatically cleaned up when:
- PR is closed (merged or abandoned)
- PR cleanup pipeline runs successfully
Manual Cleanup (if needed)
# If automatic cleanup fails, manually run:
cd docs/terraform-pr
terraform init \
-backend-config="storage_account_name=STORAGE_ACCOUNT" \
-backend-config="container_name=terraform-state-docs" \
-backend-config="key=docs-pr-{PR_NUMBER}.tfstate" \
-reconfigure
terraform destroy \
-var="pr_number={PR_NUMBER}" \
-var="build_id=manual-cleanup" \
-auto-approve
Cleanup Verification
- Check Azure Portal: Resource group should be deleted
- Check state storage:
docs-pr-{pr_number}.tfstateshould be removed - No ongoing costs should appear in Azure billing
Troubleshooting
Common Issues
1. Preview URL Not Working
Symptoms: 404 error or blank page Solutions:
- Verify storage account was created:
az storage account show -n xmprodocpr{pr_number} - Check static website enabled: Look for
primary_web_endpointin outputs - Confirm files uploaded: Check
$webcontainer contents - Wait 2-3 minutes for DNS propagation
2. State Lock Issues
Symptoms: "Backend configuration changed" or lock errors Solutions:
- Concurrent builds: PR-specific state files prevent most locking conflicts
- Stuck locks: Locks auto-expire after 15 minutes
- Manual unlock: Use
terraform force-unlockwith lock ID - State mismatch: Ensure backend configuration includes proper azurerm block
3. Storage Account Name Conflicts
Symptoms: "Storage account name already exists" Solutions:
- Check if previous cleanup failed: Look for existing
xmprodocpr{pr_number} - Manual cleanup of orphaned resources
- Storage account names are globally unique across Azure
4. Pipeline Authentication Failures
Symptoms: "Unauthorized" or "Forbidden" errors Solutions:
- Verify
terraform-devtestplan-statevariable group exists - Check Azure Key Vault permissions for service principal
- Confirm service connection is valid and not expired
5. Terraform Init Failures
Symptoms: Backend initialization errors Solutions:
- Verify
terraform-state-docscontainer exists in storage account - Check SAS token permissions and expiration
- Ensure storage account allows Terraform operations
- Verify
main.tfincludes proper backend "azurerm" block
Debug Information
Useful Commands
# Check storage account details
az storage account show -n xmprodocpr{pr_number} -g rg-xmpro-docs-pr-preview-{pr_number}
# List state files
az storage blob list --container-name terraform-state-docs --account-name {state_storage_account}
# Check static website configuration
az storage blob service-properties show --account-name xmprodocpr{pr_number}
Pipeline Variables to Check
System.PullRequest.PullRequestId: Should contain PR numberstorageaccount: Backend storage account namesas-token: Should not be empty or expiredkey_vault_name: Key Vault containing secrets
Getting Help
If issues persist:
- Check Azure DevOps pipeline logs for detailed error messages
- Verify Azure Portal for resource creation status
- Check variable group values in Azure DevOps
- Contact platform team with PR number and error details
Maintenance
Regular Tasks
- Monitor costs: Review Azure billing for unexpected charges
- Clean up orphaned resources: Check for resources not cleaned up automatically
- Update documentation: Keep README current with any pipeline changes
- Review security: Ensure no sensitive data in documentation
Updates Required When
- Azure provider version changes: Update
required_providersinmain.tf - Pipeline template changes: Update deployment template if process changes
- Variable group changes: Update documentation if new secrets added
- Naming conventions change: Update resource naming rules
This module provides a robust, cost-effective solution for documentation PR previews with proper isolation and automatic cleanup.