How My Terraform GitHub Actions Pipeline Works

This documents how our Terraform CI/CD pipeline is set up in GitHub Actions: specifically the auth model, the state backend, the two-file workflow structure, and how I close the loop on post-merge apply results showing up on the PR that triggered them. The Two-File Workflow Structure The pipeline is split into two files: a thin caller and a reusable template. terraform.yml — the caller. It defines the triggers and hands off to the template: ...

May 29, 2026 · 7 min · Zac Lohrenz

Re-reading The Phoenix Project in 2026: The CEO Constraint the Book Missed

Re-reading The Phoenix Project in 2026: The CEO Constraint the Book Missed The Phoenix Project has become canonical for SREs and ops engineers: required reading for understanding DevOps, Theory of Constraints, and why IT organizations fail under their own weight. But re-reading it in 2026, a decade after publication, reveals a critical structural blindspot that undermines its core thesis: the book identifies constraints everywhere except where the real one sits. ...

May 15, 2026 · 8 min · Zac Lohrenz

I Found a Security Gap in Fastly's WAF Agent Startup Sequence

The Internet Has a Security Blind Spot at Boot Time Most security people focus on the steady state. Is the WAF rule tuned correctly? Is the SIEM catching the right events? Is the policy up to date? What they often miss is the window between “the process started” and “the process is ready.” In Kubernetes, that window can be a meaningful attack surface — and in the case of Fastly’s Next-Gen WAF (formerly Signal Sciences) NGINX Ingress integration, it was. ...

April 12, 2026 · 8 min · Zac

Automating Microsoft Teams Provisioning with PowerShell and Graph API

The Challenge of Teams Sprawl When organizations adopt Microsoft Teams, they often face a common challenge: how to provision Teams consistently while maintaining security standards. Manual creation leads to inconsistent settings, security gaps, and administrative overhead. This guide shows how to automate Teams provisioning with PowerShell to ensure every team follows your organization’s security baseline. Prerequisites # Install required modules Install-Module Microsoft.Graph -Scope CurrentUser Install-Module MicrosoftTeams -Scope CurrentUser # Import modules Import-Module Microsoft.Graph Import-Module MicrosoftTeams Authentication Setup # Connect to Microsoft Graph Connect-MgGraph -Scopes "Group.ReadWrite.All", "TeamSettings.ReadWrite.All", "Directory.ReadWrite.All" # Connect to Teams PowerShell Connect-MicrosoftTeams The Automation Script Core Team Creation Function Example function New-StandardizedTeam { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$TeamName, [Parameter(Mandatory=$true)] [string]$Description, [Parameter(Mandatory=$false)] [string[]]$Owners = @(), [Parameter(Mandatory=$false)] [string[]]$Members = @() ) try { Write-Host "Creating team: $TeamName" -ForegroundColor Yellow # Create the Microsoft 365 Group first $groupParams = @{ displayName = $TeamName description = $Description mailEnabled = $true mailNickname = ($TeamName -replace '[^a-zA-Z0-9]', '') securityEnabled = $false groupTypes = @("Unified") visibility = "Private" } $group = New-MgGroup -BodyParameter $groupParams Write-Host "Group created with ID: $($group.Id)" -ForegroundColor Green # Wait for group provisioning Start-Sleep -Seconds 10 # Convert to Team $teamParams = @{ "memberSettings" = @{ "allowCreateUpdateChannels" = $false "allowDeleteChannels" = $false "allowCreatePrivateChannels" = $false } "guestSettings" = @{ "allowCreateUpdateChannels" = $false "allowDeleteChannels" = $false } "messagingSettings" = @{ "allowUserEditMessages" = $true "allowUserDeleteMessages" = $false "allowTeamMentions" = $true "allowChannelMentions" = $true } "funSettings" = @{ "allowGiphy" = $true "giphyContentRating" = "strict" "allowStickersAndMemes" = $false "allowCustomMemes" = $false } } $team = New-MgTeam -GroupId $group.Id -BodyParameter $teamParams Write-Host "Team provisioned successfully" -ForegroundColor Green # Configure General channel Set-TeamChannelModerators -GroupId $group.Id # Add owners and members Add-TeamMembers -GroupId $group.Id -Owners $Owners -Members $Members # Hide from GAL Update-MgGroup -GroupId $group.Id -HideFromAddressLists return @{ Success = $true GroupId = $group.Id TeamName = $TeamName } } catch { Write-Error "Failed to create team: $_" return @{ Success = $false Error = $_.Exception.Message } } } Channel Configuration function Set-TeamChannelModerators { param( [Parameter(Mandatory=$true)] [string]$GroupId ) # Get the General channel $channels = Get-MgTeamChannel -TeamId $GroupId $generalChannel = $channels | Where-Object { $_.DisplayName -eq "General" } if ($generalChannel) { # Update channel to require moderation $channelParams = @{ moderationSettings = @{ userNewMessageRestriction = "everyoneExceptGuests" replyRestriction = "everyone" allowNewMessageFromBots = $true allowNewMessageFromConnectors = $true } } Update-MgTeamChannel -TeamId $GroupId -ChannelId $generalChannel.Id -BodyParameter $channelParams Write-Host "General channel moderation configured" -ForegroundColor Green } } Bulk Provisioning function Import-TeamsFromCSV { param( [Parameter(Mandatory=$true)] [string]$CsvPath ) $teams = Import-Csv -Path $CsvPath $results = @() foreach ($team in $teams) { $result = New-StandardizedTeam ` -TeamName $team.TeamName ` -Description $team.Description ` -Owners ($team.Owners -split ';') ` -Members ($team.Members -split ';') $results += $result # Rate limiting Start-Sleep -Seconds 5 } # Export results $results | Export-Csv -Path "TeamsProvisioningResults.csv" -NoTypeInformation # Summary $successful = ($results | Where-Object { $_.Success -eq $true }).Count $failed = ($results | Where-Object { $_.Success -eq $false }).Count Write-Host "Provisioning Summary:" -ForegroundColor Cyan Write-Host "Successful: $successful" -ForegroundColor Green Write-Host "Failed: $failed" -ForegroundColor Red } CSV Template TeamName,Description,Owners,Members "Marketing Team","Marketing collaboration space","[email protected];[email protected]","[email protected];[email protected]" "Sales Team","Sales collaboration space","[email protected];[email protected]","[email protected];[email protected]" Advanced Configuration Apply Organization-Wide Settings function Set-OrganizationTeamsDefaults { # Get current policies $messagingPolicy = Get-CsTeamsMessagingPolicy -Identity Global # Update messaging policy Set-CsTeamsMessagingPolicy -Identity Global ` -AllowGiphy $true ` -GiphyRatingType "Strict" ` -AllowMemes $false ` -AllowStickers $false ` -AllowUserDeleteMessages $false ` -AllowUserEditMessages $true # Guest access configuration Set-CsTeamsGuestMessagingConfiguration ` -AllowGiphy $false ` -AllowMemes $false ` -AllowStickers $false ` -AllowUserDeleteMessages $false ` -AllowUserEditMessages $false Write-Host "Organization defaults applied" -ForegroundColor Green } Error Handling and Logging function Write-TeamsLog { param( [string]$Message, [string]$Level = "Info" ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "$timestamp [$Level] $Message" Add-Content -Path "TeamsProvisioning.log" -Value $logEntry switch ($Level) { "Error" { Write-Host $Message -ForegroundColor Red } "Warning" { Write-Host $Message -ForegroundColor Yellow } "Success" { Write-Host $Message -ForegroundColor Green } default { Write-Host $Message } } } Best Practices Rate Limiting: Microsoft Graph has throttling limits. Add delays between API calls. Error Recovery: Implement retry logic for transient failures. Validation: Verify team settings post-creation. Documentation: Maintain a log of all provisioned teams. Regular Audits: Schedule periodic reviews of team settings. Conclusion Automating Teams provisioning ensures consistency, improves security posture, and reduces administrative overhead. This approach scales from small deployments to enterprise-wide migrations while maintaining your organization’s governance standards. ...

April 3, 2026 · 4 min · Me

Migrating Azure Container Apps from Docker Hub to Azure Container Registry

The Challenge When managing containerized applications in Azure Container Apps, you eventually face the question: should you continue using Docker Hub, or migrate to Azure Container Registry (ACR)? For enterprise workloads, ACR offers significant advantages: integrated security, managed identities for authentication, reduced egress costs, and better integration with Azure RBAC. This post documents a complete migration from Docker Hub to ACR across multiple Container Apps in different Azure subscriptions, including the critical managed identity configuration that enables seamless authentication without storing credentials. ...

December 5, 2025 · 7 min · Me