CDN Architecture: Design Patterns for Global Scale

Content Delivery Networks (CDNs) are fundamental to modern web architecture. Let’s explore key design patterns and implementation strategies for optimal content delivery. CDN Architecture Fundamentals Edge Location Strategy Effective CDN implementation requires careful planning of edge locations: Geographic Distribution Place edge nodes near user concentrations Consider regional traffic patterns Account for network topology Cache Strategy Static content: Aggressive caching Dynamic content: TTL-based invalidation API responses: Selective caching Implementation Patterns 1. Origin Shield Configuration # Nginx origin shield configuration location / { proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; proxy_cache_valid 200 302 1h; proxy_cache_valid 404 1m; proxy_cache my_cache_zone; proxy_cache_key $scheme$proxy_host$request_uri; } 2. Cache Control Headers Implement proper cache control headers: ...

2 min · Me

Free Web Hosting: Building a Professional Site with Cloudflare Pages

If you have a custom domain and want a professional website without ongoing hosting costs, consider building a static website delivered through a Content Delivery Network (CDN). This approach offers excellent performance, high availability, and simple maintenance. My website uses this architecture, with the domain registration as my only recurring cost. Prerequisites Before getting started, you’ll need: A domain name A Git account (GitHub, GitLab, or similar) Basic command line familiarity About 1-2 hours for initial setup Domain Registration Your choice of domain registrar can significantly impact your annual costs. For supported top-level domains (TLDs), Cloudflare’s Domain Registration service (https://www.cloudflare.com/products/registrar/) stands out by charging only wholesale prices without markup or hidden fees. ...

3 min · Me

Implementing Automated Azure Resource Locks with PowerShell Runbooks

Why Resource Locks Matter Azure resource locks are an important security feature that prevent accidental deletion or modification of important resources. However, manual implementation can be tedious and locks may be inadvertently removed, forgotten to put back, or not added to new resources. This post explains how to automate the process using Azure Automation runbooks. Implementation Overview Creating the Automation Runbook Create a new Azure Automation account or use an existing one Create a PowerShell runbook that will: Scan for resources without locks Apply appropriate lock types (CanNotDelete or ReadOnly) Skip dynamic resources like AKS nodes Sample PowerShell Script # Azure Resource Lock Automation Script - Fixed Authentication # This script processes locks in batches to avoid timeout limits #Requires -Module Az.Accounts, Az.Resources param( [string[]]$SubscriptionIds = @(), [string[]]$ExemptResourceGroups = @(), [string[]]$ExemptResources = @(), [string]$LockName = "DenyDelete", [string]$LockNotes = "Delete lock", [switch]$WhatIf = $false, [switch]$IncludeResources = $true, [int]$BatchSize = 10, [int]$MaxExecutionMinutes = 150, [string]$StateTableName = "LockAutomationState", [string]$StorageAccountName = "", [string]$StorageResourceGroup = "", [string[]]$TargetResourceTypes = @( "Microsoft.Compute/virtualMachines", "Microsoft.Compute/virtualMachineScaleSets", "Microsoft.Sql/servers", "Microsoft.Sql/managedInstances", "Microsoft.DBforPostgreSQL/servers", "Microsoft.DBforMySQL/servers", "Microsoft.DBforMariaDB/servers", "Microsoft.DocumentDB/databaseAccounts", "Microsoft.Storage/storageAccounts", "Microsoft.Network/virtualNetworks", "Microsoft.Network/networkSecurityGroups", "Microsoft.Network/routeTables", "Microsoft.Network/publicIPAddresses", "Microsoft.Network/loadBalancers", "Microsoft.Network/applicationGateways", "Microsoft.Network/dnszones", "Microsoft.Network/privateDnsZones", "Microsoft.KeyVault/vaults", "Microsoft.RecoveryServices/vaults", "Microsoft.ContainerRegistry/registries", "Microsoft.Kubernetes/connectedClusters", "Microsoft.ContainerService/managedClusters", "Microsoft.Web/sites", "Microsoft.Web/serverfarms", "Microsoft.Logic/workflows", "Microsoft.DataFactory/factories", "Microsoft.Synapse/workspaces", "Microsoft.Network/natGateways", "Microsoft.Network/vpnGateways", "Microsoft.Purview/accounts", "Microsoft.Security/pricings", "Microsoft.OperationsManagement/solutions" ) ) # Global variables for tracking $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:SkippedCount = 0 $script:ErrorCount = 0 $script:BatchNumber = 0 $script:TimeoutReached = $false # Check execution time limit function Test-ExecutionTimeLimit { $elapsed = (Get-Date) - $script:StartTime $remainingMinutes = $MaxExecutionMinutes - $elapsed.TotalMinutes if ($remainingMinutes -le 5) { # Stop with 5 minutes buffer $script:TimeoutReached = $true Write-Output "⚠️ TIMEOUT WARNING: Only $([math]::Round($remainingMinutes, 1)) minutes remaining. Stopping execution." return $false } return $true } function Write-BatchSummary { param( [int]$BatchNum, [string]$SubscriptionId, [int]$BatchProcessed, [int]$BatchSuccess, [int]$BatchSkipped, [int]$BatchErrors, [datetime]$BatchStartTime ) $batchElapsed = (Get-Date) - $BatchStartTime $totalElapsed = (Get-Date) - $script:StartTime Write-Output "`n📊 --- Batch $BatchNum Summary ---" Write-Output "🎯 Subscription: $SubscriptionId" Write-Output "📋 Batch processed: $BatchProcessed" Write-Output "✅ Batch successful: $BatchSuccess" Write-Output "⏭️ Batch skipped: $BatchSkipped" Write-Output "❌ Batch errors: $BatchErrors" Write-Output "⏱️ Batch time: $([math]::Round($batchElapsed.TotalMinutes, 1)) minutes" Write-Output "🕐 Total elapsed: $([math]::Round($totalElapsed.TotalMinutes, 1)) minutes" Write-Output "⏳ Remaining time: $([math]::Round($MaxExecutionMinutes - $totalElapsed.TotalMinutes, 1)) minutes" Write-Output "📊 --- End Batch Summary ---`n" } # Simplified state management (in-memory for this version) $script:ProcessedResourceGroups = @{} function Connect-ToAzure { try { Write-Output "🔍 Checking Azure authentication status..." # Check if we already have a PowerShell Az context $context = Get-AzContext -ErrorAction SilentlyContinue if ($context) { Write-Output "✓ Already connected to Azure PowerShell as: $($context.Account.Id)" Write-Output "✓ Current subscription: $($context.Subscription.Name) ($($context.Subscription.Id))" return $true } Write-Output "⚠️ No existing Azure PowerShell context found" # Check if we're running in Azure Automation (has specific environment variables) $isAzureAutomation = $env:AUTOMATION_ASSET_ACCOUNTID -or $env:AUTOMATION_RESOURCE_GROUP if ($isAzureAutomation) { # Try to connect using Managed Identity (for Azure Automation) try { Write-Output "🔄 Detected Azure Automation environment. Attempting to connect using Managed Identity..." Connect-AzAccount -Identity -ErrorAction Stop $context = Get-AzContext Write-Output "✓ Successfully connected using Managed Identity" Write-Output "✓ Connected as: $($context.Account.Id)" return $true } catch { Write-Output "❌ Managed Identity connection failed: $($_.Exception.Message)" } } else { # Running locally - try to import Azure CLI credentials Write-Output "🔄 Running locally. Attempting to import Azure CLI credentials..." try { # Try to connect using Azure CLI credentials Connect-AzAccount -UseDeviceAuthentication:$false -ErrorAction Stop $context = Get-AzContext Write-Output "✓ Successfully connected using existing credentials" Write-Output "✓ Connected as: $($context.Account.Id)" return $true } catch { Write-Output "❌ Failed to connect using existing credentials: $($_.Exception.Message)" # Last resort - ask user to connect manually Write-Output "💡 Please run 'Connect-AzAccount' first to authenticate to Azure PowerShell" Write-Output "💡 Note: 'az login' is for Azure CLI, but this script requires Azure PowerShell authentication" Write-Error "Authentication required. Please run 'Connect-AzAccount' before running this script." return $false } } Write-Error "Authentication required. Please authenticate to Azure before running this script." return $false } catch { Write-Error "Failed to connect to Azure: $($_.Exception.Message)" return $false } } function Add-ResourceGroupLockOptimized { param( [string]$SubscriptionId, [string]$ResourceGroupName, [string]$LockName, [string]$Notes, [bool]$WhatIf ) try { $script:ProcessedCount++ # Quick check for existing lock $existingLock = Get-AzResourceLock -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue | Where-Object { $_.Properties.level -eq "CanNotDelete" } | Select-Object -First 1 if ($existingLock) { Write-Output "✓ RG '$ResourceGroupName' already locked" $script:SkippedCount++ return } if ($WhatIf) { Write-Output "WHATIF: Would lock RG '$ResourceGroupName'" return } New-AzResourceLock -ResourceGroupName $ResourceGroupName -LockName $LockName -LockLevel CanNotDelete -LockNotes $Notes -Force -ErrorAction Stop Write-Output "✓ Locked RG '$ResourceGroupName'" $script:SuccessCount++ } catch { Write-Warning "Failed to lock RG '$ResourceGroupName': $($_.Exception.Message)" $script:ErrorCount++ } } function Add-ResourceLockOptimized { param( [object]$Resource, [string]$LockName, [string]$Notes, [bool]$WhatIf, [string[]]$TargetResourceTypes ) try { # Quick type check if ($Resource.ResourceType -notin $TargetResourceTypes) { return } $script:ProcessedCount++ # Quick check for existing lock $existingLock = Get-AzResourceLock -ResourceName $Resource.Name -ResourceType $Resource.ResourceType -ResourceGroupName $Resource.ResourceGroupName -ErrorAction SilentlyContinue | Where-Object { $_.Properties.level -eq "CanNotDelete" } | Select-Object -First 1 if ($existingLock) { $script:SkippedCount++ return } if ($WhatIf) { Write-Output "WHATIF: Would lock resource '$($Resource.Name)' ($($Resource.ResourceType))" return } New-AzResourceLock -ResourceName $Resource.Name -ResourceType $Resource.ResourceType -ResourceGroupName $Resource.ResourceGroupName -LockName $LockName -LockLevel CanNotDelete -LockNotes $Notes -Force -ErrorAction Stop Write-Output "✓ Locked resource '$($Resource.Name)' ($($Resource.ResourceType))" $script:SuccessCount++ } catch { Write-Warning "Failed to lock resource '$($Resource.Name)': $($_.Exception.Message)" $script:ErrorCount++ } } function Test-ResourceGroupExemption { param([string]$ResourceGroupName, [string[]]$ExemptList) foreach ($exemption in $ExemptList) { if ($ResourceGroupName -like $exemption) { return $true } } return $false } # Main execution try { Write-Output "=== Azure Resource Lock Automation - Batch Mode ===" Write-Output "Started at: $(Get-Date)" Write-Output "Max execution time: $MaxExecutionMinutes minutes" Write-Output "Batch size: $BatchSize resource groups" # Connect to Azure (or verify existing connection) if (-not (Connect-ToAzure)) { Write-Error "Failed to establish Azure connection. Exiting." exit 1 } # Get subscriptions if ($SubscriptionIds.Count -eq 0) { Write-Output "📋 No specific subscriptions provided. Getting all enabled subscriptions..." Write-Output "🔄 Querying Azure for enabled subscriptions..." $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } $SubscriptionIds = $subscriptions.Id Write-Output "✓ Found $($SubscriptionIds.Count) enabled subscriptions" # List the subscriptions we found foreach ($sub in $subscriptions) { Write-Output " - $($sub.Name) ($($sub.Id))" } } else { Write-Output "📋 Using provided subscription IDs: $($SubscriptionIds.Count) subscription(s)" } Write-Output "`n🚀 Starting processing of $($SubscriptionIds.Count) subscription(s)..." foreach ($subscriptionId in $SubscriptionIds) { # Check time limit before each subscription if (-not (Test-ExecutionTimeLimit)) { Write-Output "Time limit reached. Stopping subscription processing." break } Write-Output "`n🎯 === Processing Subscription: $subscriptionId ===" try { # Set context to the subscription Write-Output "🔄 Setting Azure context to subscription..." $context = Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop Write-Output "✓ Set context to subscription: $($context.Subscription.Name)" # Get resource groups Write-Output "📦 Getting resource groups from subscription..." $resourceGroups = Get-AzResourceGroup -ErrorAction Stop Write-Output "✓ Found $($resourceGroups.Count) resource groups in subscription" if ($resourceGroups.Count -eq 0) { Write-Output "⚠️ No resource groups found in this subscription. Skipping..." continue } # Show exemption info if any if ($ExemptResourceGroups.Count -gt 0) { Write-Output "🚫 Exempted resource group patterns: $($ExemptResourceGroups -join ', ')" } Write-Output "⏳ Processing resource groups in batches of $BatchSize..." # Process in batches for ($i = 0; $i -lt $resourceGroups.Count; $i += $BatchSize) { # Check time limit before each batch if (-not (Test-ExecutionTimeLimit)) { break } $script:BatchNumber++ $batchStartTime = Get-Date $batchProcessed = 0 $batchSuccess = 0 $batchSkipped = 0 $batchErrors = 0 $batch = $resourceGroups | Select-Object -Skip $i -First $BatchSize Write-Output "📋 Processing batch $($script:BatchNumber): RGs $($i + 1) to $($i + $batch.Count) of $($resourceGroups.Count)" Write-Output "⏱️ Batch started at: $(Get-Date -Format 'HH:mm:ss')" foreach ($rg in $batch) { Write-Output "🔍 Processing RG: $($rg.ResourceGroupName)" # Check time limit during batch processing if (-not (Test-ExecutionTimeLimit)) { Write-Output "⏰ Time limit reached during batch processing. Stopping current batch." break } # Check exemptions if (Test-ResourceGroupExemption -ResourceGroupName $rg.ResourceGroupName -ExemptList $ExemptResourceGroups) { Write-Output "🚫 Skipping exempted RG: $($rg.ResourceGroupName)" $batchSkipped++ continue } # Track counts before processing $beforeProcessed = $script:ProcessedCount $beforeSuccess = $script:SuccessCount $beforeSkipped = $script:SkippedCount $beforeErrors = $script:ErrorCount # Lock resource group Write-Output "🔒 Checking/adding lock for RG: $($rg.ResourceGroupName)" Add-ResourceGroupLockOptimized -SubscriptionId $subscriptionId -ResourceGroupName $rg.ResourceGroupName -LockName $LockName -Notes $LockNotes -WhatIf $WhatIf # Lock individual resources if enabled if ($IncludeResources) { try { Write-Output "📦 Getting resources from RG: $($rg.ResourceGroupName)" $resources = Get-AzResource -ResourceGroupName $rg.ResourceGroupName -ErrorAction Stop if ($resources.Count -gt 0) { Write-Output " Found $($resources.Count) resources to evaluate" foreach ($resource in $resources) { # Check time limit during resource processing if (-not (Test-ExecutionTimeLimit)) { Write-Output "⏰ Time limit reached during resource processing. Stopping current RG." break } # Only show output for resources we're actually processing if ($resource.ResourceType -in $TargetResourceTypes) { Write-Output " 🔍 Evaluating: $($resource.Name) ($($resource.ResourceType))" } Add-ResourceLockOptimized -Resource $resource -LockName $LockName -Notes $LockNotes -WhatIf $WhatIf -TargetResourceTypes $TargetResourceTypes } } else { Write-Output " No resources found in RG" } } catch { Write-Warning "❌ Failed to get resources from RG $($rg.ResourceGroupName): $($_.Exception.Message)" $batchErrors++ } } # Calculate batch-specific counts $batchProcessed += ($script:ProcessedCount - $beforeProcessed) $batchSuccess += ($script:SuccessCount - $beforeSuccess) $batchSkipped += ($script:SkippedCount - $beforeSkipped) $batchErrors += ($script:ErrorCount - $beforeErrors) # Break if time limit reached if ($script:TimeoutReached) { break } } # Write batch summary Write-BatchSummary -BatchNum $script:BatchNumber -SubscriptionId $subscriptionId -BatchProcessed $batchProcessed -BatchSuccess $batchSuccess -BatchSkipped $batchSkipped -BatchErrors $batchErrors -BatchStartTime $batchStartTime # Break if time limit reached if ($script:TimeoutReached) { break } } } catch { Write-Error "Failed to process subscription $subscriptionId : $($_.Exception.Message)" continue } # Break if time limit reached if ($script:TimeoutReached) { Write-Output "Time limit reached. Stopping all processing." break } } Write-Output "`n🏁 === FINAL EXECUTION SUMMARY ===" Write-Output "🏁 Completed at: $(Get-Date)" $totalElapsed = (Get-Date) - $script:StartTime Write-Output "⏱️ Total execution time: $([math]::Round($totalElapsed.TotalMinutes, 1)) minutes" Write-Output "📊 Batches processed: $($script:BatchNumber)" Write-Output "📋 Total items processed: $($script:ProcessedCount)" Write-Output "✅ Successful locks: $($script:SuccessCount)" Write-Output "⏭️ Skipped (already locked): $($script:SkippedCount)" Write-Output "❌ Errors: $($script:ErrorCount)" if ($script:TimeoutReached) { Write-Output "⚠️ EXECUTION STOPPED DUE TO TIME LIMIT" Write-Output "💡 Consider running again to continue processing remaining items." } if ($WhatIf) { Write-Output "🔍 *** This was a PREVIEW run. No actual changes were made. ***" } Write-Output "🏁 === END SUMMARY ===" } catch { Write-Error "Script execution failed: $($_.Exception.Message)" exit 1 } ## Scheduling the Automation ### Configure Recurring Schedule 1. Set up a schedule in Azure Automation 2. Configure the runbook to execute every 6 hours 3. Ensure proper permissions are assigned to the Automation Account ```powershell $schedule = New-AzAutomationSchedule ` -AutomationAccountName "automatic-resource-locks" ` -Name "ResourceLockSchedule" ` -StartTime "2024-01-01T00:00:00" ` -HourInterval 6 Best Practices Maintain an exclusion list for resources that shouldn’t be locked Implement logging to track lock changes Set up alerts for lock removal events Regular review of locked resources to ensure proper protection Monitoring and Maintenance Regularly check the Automation account’s job history to ensure the runbook executes successfully. Monitor for any failures and adjust the script as needed based on your infrastructure changes. ...

9 min · Me