Event Sourcing: Building Event-Driven Systems

In modern distributed systems, maintaining data consistency, tracking changes, and scaling effectively can be challenging. Event Sourcing offers a powerful architectural pattern that addresses these challenges by storing all changes to an application’s state as a sequence of events. Let’s explore how to implement this pattern in a production environment. Why Event Sourcing? Before diving into implementation details, let’s understand why you might want to use Event Sourcing: Complete Audit Trail: Every state change is captured as an immutable event, providing a perfect audit history. Temporal Queries: You can determine the system’s state at any point in time by replaying events. Debug Friendly: When issues occur, you have a complete history of what led to the current state. Event Replay: You can fix bugs by correcting the event handling logic and replaying events. Scale Write/Read Separately: Event storage and read models can be scaled independently. Core Components The Event Store The Event Store is the heart of any event-sourced system. It’s responsible for storing and retrieving events while ensuring consistency. Here’s a TypeScript implementation that handles the core functionality: ...

4 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

GitOps Workflow Patterns

GitOps Fundamentals Core Principles Declarative Infrastructure Version Controlled Changes Automated Reconciliation Self-healing Systems Implementation Patterns ArgoCD Application Configuration apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: production-app spec: project: default source: repoURL: https://github.com/org/app-config.git targetRevision: HEAD path: environments/production destination: server: https://kubernetes.default.svc namespace: production syncPolicy: automated: prune: true selfHeal: true Workflow Patterns Multi-Environment Setup # environments/base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml - ingress.yaml # environments/production/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization bases: - ../base patchesStrategicMerge: - production-patches.yaml Security Practices RBAC Configuration apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: gitops-deployer rules: - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] Production Example # Complete GitOps application setup apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: full-stack-app namespace: argocd spec: project: production source: repoURL: https://github.com/org/app-config.git targetRevision: main path: environments/production directory: recurse: true destination: server: https://kubernetes.default.svc namespace: production syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true retry: limit: 5 backoff: duration: 5s factor: 2 maxDuration: 3m These patterns ensure reliable, automated deployment workflows. ...

1 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

Implementing Azure Conditional Access Policies for Geographic Security

Understanding Geographic-Based Access Controls Geographic-based access controls are crucial for organizations looking to maintain compliance with international regulations or enhance security by removing some low hanging fruit. One specific use case is blocking access from OFAC sanctioned countries while allowing access from trusted locations. Implementation Steps 1. Create a Report-Only Policy First, create a policy in report-only mode to assess impact: Navigate to Azure Portal > Azure AD > Security > Conditional Access Create a new policy Configure the following settings: Users and groups: All users Cloud apps or actions: All cloud apps Conditions: Locations > Configure > Selected locations Access controls: Block access Enable policy: Report-only 2. Configure Location Conditions Create a list of blocked locations: ...

2 min · Me