Kyle Bracke

IT Systems Engineer

ERP Systems Hybrid Cloud Infrastructure Automation Network Engineering AI & Emerging Tech

Who I Am

I've always been more interested in how things work than in the job title.

I'm an IT Systems Engineer with about a decade of experience. My background is in Network and Telecommunications Management, and I've spent most of my career at a mid-size distribution company building and running the IT stack end to end.

What started as a standard sysadmin job turned into a lot more over time. When the company needed an ERP system, I ended up running the whole implementation myself: design, configuration, data migration, user training, all of it. When the network became a liability, I redesigned it. When manual processes were eating people's time, I automated them. That pattern has repeated a lot.

Day-to-day I'm managing a full stack: Microsoft 365, Entra ID, Active Directory, Hyper-V, SAP Business One, network infrastructure, backup, and endpoint security. There's a lot to it, but most of it runs itself at this point, which is kind of the point.

I went to Illinois State University for Network and Telecom Management. Having a broad foundation has been useful in practice. Enterprise environments don't stay in one lane, and being able to jump between infrastructure, cloud, security, and application layers has mattered more than I expected early on.

Lately I've been spending time with AI tools and agentic workflows, both for my own productivity and to understand where they actually fit in an enterprise context. I use AI daily to speed up scripting and problem-solving, and I'm experimenting with more autonomous workflows to figure out what's genuinely worth building.

10+ Years in IT
SAP Self-Implemented ERP
โˆž Always Learning

Technical Stack

A breakdown of the technologies and platforms I work with on a regular basis.

๐Ÿ—„๏ธ

ERP & Business Systems

  • SAP Business One
  • Boyum Usability Package
  • Microsoft SQL Server
  • T-SQL & Query Optimization
  • Custom Reporting & Integration
  • Business Process Automation
โ˜๏ธ

Cloud & Identity

  • Microsoft Azure
  • Microsoft Entra ID
  • Azure AD Connect
  • Microsoft 365 Administration
  • Exchange Online & Teams Voice
  • SharePoint Online
๐Ÿ–ฅ๏ธ

Infrastructure

  • Windows Server
  • Active Directory & Group Policy
  • Hyper-V Virtualization
  • Azure Backup & MABS
  • Disaster Recovery Planning
  • Domain Controllers & DNS
โšก

Automation & AI

  • PowerShell
  • SQL Automation & Agents
  • Microsoft Power Automate
  • Automated Reporting & Workflows
  • AI Tools & Productivity
  • Agentic AI โ€” Exploring
๐Ÿ”

Security & Compliance

  • Microsoft Defender
  • Intune Endpoint Management
  • Conditional Access Policies
  • Zero Trust Principles
  • Security Baseline Configuration
  • Endpoint Compliance
๐ŸŒ

Network Engineering

  • TCP/IP & Routing
  • SD-WAN Design & Implementation
  • VLAN Segmentation
  • VPN Architecture
  • Firewall Policy Administration
  • Site-to-Site Connectivity

What I've Built

Real projects from a live enterprise environment. What the problem was, what I built, and what actually changed.

02
Entra ID Active Directory Microsoft 365

Hybrid Identity Architecture

The Challenge: We were moving more into the cloud but identity was still entirely on-prem. Access was inconsistent, security enforcement was manual, and there was no unified way to manage cloud and on-premises resources together.

My Approach: Set up Azure AD Connect with Entra ID, configured hybrid join and password hash sync, and built out conditional access policies. Brought Exchange Online, Teams Voice, SharePoint, and Intune into a consistent admin experience.

Result: Single sign-on works cleanly across on-prem and cloud. Conditional access enforces security baselines automatically without manual checks on every device.
03
PowerShell SQL Automation Power Automate

Automation-First Operations

The Challenge: A lot of the daily work was repetitive and manual. Finance reports compiled by hand, user provisioning done inconsistently, operational tasks that ran on people's time instead of running themselves.

My Approach: Built automation across three layers: PowerShell for admin and system tasks, SQL Agent jobs for database workflows, and Power Automate for business processes that cross system boundaries. Reports now run on a schedule and land in inboxes without anyone pulling them.

Result: Most of the repetitive work is gone. Reports that used to take hours go out automatically. Provisioning that was inconsistent is now fast and predictable every time.
04
SD-WAN VLAN VPN Firewall

Network Infrastructure Modernization

The Challenge: The network was flat. No segmentation, aging firewall rules, and a setup where a problem in one area could spread everywhere. It worked until it didn't.

My Approach: Redesigned the whole thing. VLAN segmentation across traffic types, SD-WAN to connect two sites and share on-premises resources reliably, rebuilt VPN for remote access, and rewrote the firewall policy from scratch with explicit deny rules and proper logging.

Result: Much better security posture. Traffic is separated properly, the attack surface is smaller, and both sites can access shared on-prem resources without relying on a patchwork of workarounds.
05
Azure Backup MABS Disaster Recovery

Backup & Disaster Recovery

The Challenge: We needed a real backup and recovery strategy. Not just scheduled jobs running in the background, but defined recovery targets, documented procedures, and actual confidence that a restore would work when it mattered.

My Approach: Built a layered setup using Azure Backup for cloud workloads and MABS for on-prem systems. Documented RTO/RPO targets, wrote recovery runbooks, and set up a recurring test cadence to verify that restores actually work before there's a reason to need them.

Result: Everything critical is covered. Recovery procedures are documented and tested. Backup failures alert automatically, and I'm not relying on hope that things will work out.

Automation in Practice

PowerShell handles most of my automation work. The scripts below are representative of what I build and maintain day-to-day.

New-ADUserOnboarding.ps1 PowerShell
#Requires -Modules ActiveDirectory, Microsoft.Graph.Users
<#
.SYNOPSIS
    New employee onboarding: AD account, Entra ID sync, M365 license.
    Logs to file and rolls back the AD account on failure.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$FirstName,
    [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$LastName,
    [Parameter(Mandatory)]
    [ValidateSet('Finance','Operations','Sales','IT','Warehouse')]
    [string]$Department,
    [string]$Manager,
    [string]$Title,
    [ValidateSet('E3','E5')] [string]$LicenseTier = 'E3'
)

$ErrorActionPreference = 'Stop'
$LogFile = "C:\Logs\Onboarding\$($LastName)_$(Get-Date -f 'yyyyMMdd_HHmmss').log"

function Write-Log {
    param([string]$Message, [ValidateSet('INFO','WARN','ERROR')] [string]$Level = 'INFO')
    $Entry = "$(Get-Date -f 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
    $Entry | Tee-Object -FilePath $LogFile -Append | Write-Host -ForegroundColor $(
        switch ($Level) { 'WARN'{'Yellow'} 'ERROR'{'Red'} default{'Cyan'} }
    )
}

function Wait-EntraSync {
    param([string]$UPN, [int]$TimeoutSec = 180)
    Write-Log "Triggering delta sync and waiting for Entra ID propagation..."
    Start-ADSyncSyncCycle -PolicyType Delta | Out-Null
    $Elapsed = 0
    do {
        Start-Sleep -Seconds 15; $Elapsed += 15
        if (Get-MgUser -Filter "userPrincipalName eq '$UPN'" -ErrorAction SilentlyContinue) {
            Write-Log "Entra ID sync confirmed after ${Elapsed}s"; return
        }
    } while ($Elapsed -lt $TimeoutSec)
    throw "Entra sync timed out after ${TimeoutSec}s for $UPN"
}

try {
    $Username = "$($FirstName.Substring(0,1).ToLower())$($LastName.ToLower())"
    $UPN      = "[email protected]"
    $TempPass = ConvertTo-SecureString "Temp$(Get-Random -Min 1000 -Max 9999)!Kx" -AsPlainText -Force

    if (Get-ADUser -Filter "SamAccountName -eq '$Username'" -ErrorAction SilentlyContinue) {
        throw "AD account '$Username' already exists. Verify with HR before proceeding."
    }

    Write-Log "Creating AD account: $Username ($Department)"
    New-ADUser -Name "$FirstName $LastName" `
        -GivenName $FirstName -Surname $LastName `
        -SamAccountName $Username -UserPrincipalName $UPN `
        -Department $Department -Title $Title -Manager $Manager `
        -AccountPassword $TempPass -Enabled $true -ChangePasswordAtLogon $true `
        -Path "OU=$Department,OU=Users,DC=company,DC=local"
    Write-Log "AD account created: $Username"

    Connect-MgGraph -Scopes 'User.ReadWrite.All','Organization.Read.All' -NoWelcome
    Wait-EntraSync -UPN $UPN

    $SkuMap  = @{ E3 = 'ENTERPRISEPACK'; E5 = 'ENTERPRISEPREMIUM' }
    $License = Get-MgSubscribedSku | Where-Object {
        $_.SkuPartNumber -eq $SkuMap[$LicenseTier] -and
        $_.ConsumedUnits -lt $_.PrepaidUnits.Enabled
    }
    if (-not $License) { throw "No available $LicenseTier licenses in tenant." }

    Set-MgUserLicense -UserId $UPN `
        -AddLicenses @{ SkuId = $License.SkuId } `
        -RemoveLicenses @()
    Write-Log "M365 $LicenseTier license assigned to $UPN"
    Write-Log "Onboarding complete. Temp password requires reset at first login."
}
catch {
    Write-Log "FAILED: $_" -Level ERROR
    if (Get-ADUser -Filter "SamAccountName -eq '$Username'" -ErrorAction SilentlyContinue) {
        Remove-ADUser -Identity $Username -Confirm:$false
        Write-Log "Rolled back AD account '$Username'" -Level WARN
    }
    exit 1
}
Get-SQLHealthReport.ps1 PowerShell + T-SQL
<#
.SYNOPSIS
    Nightly SQL Server health check. Covers backup age, index fragmentation,
    and long-running queries. Emails a formatted report with status flags.
#>

$ErrorActionPreference = 'Stop'
$Config = @{
    SQLServer     = 'SQL-SERVER-01'
    Databases     = @('CompanyDB', 'SAPB1DB', 'ReportingDB')
    Recipients    = @('[email protected]', '[email protected]')
    SmtpServer    = 'smtp.company.com'
    FragThreshPct = 30      # flag indexes above this fragmentation %
    LongQuerySec  = 60      # flag queries running longer than this
    BackupMaxSec  = 86400   # 24 hours
}

$Results = [System.Collections.Generic.List[PSCustomObject]]::new()
$Errors  = [System.Collections.Generic.List[string]]::new()

foreach ($DB in $Config.Databases) {
    try {
        $SizeBackup = Invoke-Sqlcmd -ServerInstance $Config.SQLServer -Database $DB `
            -ErrorAction Stop -Query @"
SELECT
    DB_NAME()                                    AS DatabaseName,
    CAST(SUM(size * 8.0 / 1024) AS INT)          AS SizeMB,
    (SELECT TOP 1 backup_finish_date
     FROM   msdb.dbo.backupset
     WHERE  database_name = DB_NAME() AND type = 'D'
     ORDER  BY backup_finish_date DESC)           AS LastFullBackup
FROM sys.database_files
WHERE type = 0
"@
        # Pull index data and filter in PowerShell to avoid SQL comparison operators
        $IndexData = Invoke-Sqlcmd -ServerInstance $Config.SQLServer -Database $DB -Query @"
SELECT index_id, avg_fragmentation_in_percent
FROM   sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED')
WHERE  index_type_desc != 'HEAP'
"@
        $FragCount = ($IndexData |
            Where-Object { $_.avg_fragmentation_in_percent -gt $Config.FragThreshPct }).Count

        $AgeSec = if ($SizeBackup.LastFullBackup) {
            [int]((Get-Date) - $SizeBackup.LastFullBackup).TotalSeconds
        } else { [int]::MaxValue }

        $Results.Add([PSCustomObject]@{
            Database       = $DB
            SizeMB         = $SizeBackup.SizeMB
            LastFullBackup = if ($SizeBackup.LastFullBackup) {
                                 $SizeBackup.LastFullBackup.ToString('yyyy-MM-dd HH:mm')
                             } else { 'NEVER' }
            BackupAgeHours = [Math]::Round($AgeSec / 3600, 1)
            BackupStatus   = if ($AgeSec -le $Config.BackupMaxSec) { 'OK' } else { 'STALE' }
            FragIdxCount   = $FragCount
        })
    }
    catch { $Errors.Add("${DB}: $_") }
}

# Long-running query check (fetch all active requests, filter in PowerShell)
try {
    $AllActive = Invoke-Sqlcmd -ServerInstance $Config.SQLServer -Database master -Query @"
SELECT r.session_id, DB_NAME(r.database_id) AS DatabaseName,
       r.status, r.command,
       r.total_elapsed_time / 1000           AS ElapsedSec,
       SUBSTRING(t.text, 1, 100)             AS QuerySnippet
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id != @@SPID
"@
    $LongRunning = $AllActive | Where-Object {
        $_.session_id -gt 50 -and $_.ElapsedSec -gt $Config.LongQuerySec
    } | Select-Object -First 5
} catch { $LongRunning = @() }

$HasIssues = ($Results | Where-Object { $_.BackupStatus -eq 'STALE' }) -or $Errors -or $LongRunning
$Subject   = "SQL Health $(if ($HasIssues) {'[ACTION REQUIRED]'} else {'[OK]'}) - $(Get-Date -f 'yyyy-MM-dd')"

$Body  = "SQL Server Health Report - $(Get-Date -f 'yyyy-MM-dd HH:mm')`n"
$Body += ("=" * 60) + "`n`n"
$Body += ($Results | Format-Table Database, SizeMB, LastFullBackup, BackupAgeHours, BackupStatus, FragIdxCount -AutoSize | Out-String)
if ($LongRunning) { $Body += "`nLONG-RUNNING QUERIES:`n$($LongRunning | Format-Table -AutoSize | Out-String)" }
if ($Errors)      { $Body += "`nERRORS:`n$($Errors -join "`n")" }

Send-MailMessage -To $Config.Recipients -From '[email protected]' `
    -Subject $Subject -Body $Body -SmtpServer $Config.SmtpServer

Write-Host "Report sent to $($Config.Recipients -join ', '). Issues detected: $([int][bool]$HasIssues)"
Test-BackupIntegrity.ps1 PowerShell
#Requires -Modules Az.RecoveryServices, Az.Accounts
<#
.SYNOPSIS
    Azure Backup job audit with Teams alerting and retry logic.
    Checks the past 48 hours across a Recovery Services Vault.
    Runs nightly via Task Scheduler using managed identity auth.
#>

$ErrorActionPreference = 'Stop'
$Config = @{
    VaultName      = 'CompanyBackupVault'
    ResourceGroup  = 'Company-RG-Backup'
    LookbackHours  = 48
    MaxAuthRetries = 3
    TeamsWebhook   = $env:TEAMS_BACKUP_WEBHOOK   # stored in system env, never hardcoded
    AlertEmail     = '[email protected]'
    SmtpServer     = 'smtp.company.com'
}

function Send-TeamsAlert {
    param([string]$Title, [string]$Details, [string]$Color = 'attention')
    if (-not $Config.TeamsWebhook) { return }
    try {
        $Card = @{
            type        = 'message'
            attachments = @(@{
                contentType = 'application/vnd.microsoft.card.adaptive'
                content     = @{
                    '$schema' = 'http://adaptivecards.io/schemas/adaptive-card.json'
                    type      = 'AdaptiveCard'
                    version   = '1.2'
                    body      = @(
                        @{ type = 'TextBlock'; text = $Title;   weight = 'Bolder'; color = $Color },
                        @{ type = 'TextBlock'; text = $Details; wrap   = $true }
                    )
                }
            })
        } | ConvertTo-Json -Depth 10
        Invoke-RestMethod -Uri $Config.TeamsWebhook -Method Post -Body $Card `
            -ContentType 'application/json' -ErrorAction SilentlyContinue | Out-Null
    }
    catch { Write-Warning "Teams alert failed: $_" }
}

# Connect via managed identity with retry
$AuthAttempt = 0
do {
    try   { Connect-AzAccount -Identity -ErrorAction Stop; break }
    catch {
        $AuthAttempt++
        if ($AuthAttempt -ge $Config.MaxAuthRetries) {
            throw "Azure authentication failed after $AuthAttempt attempts: $_"
        }
        Write-Warning "Auth attempt $AuthAttempt failed. Retrying in 30s..."
        Start-Sleep -Seconds 30
    }
} while ($AuthAttempt -lt $Config.MaxAuthRetries)

$Vault = Get-AzRecoveryServicesVault -Name $Config.VaultName -ResourceGroupName $Config.ResourceGroup
Set-AzRecoveryServicesVaultContext -Vault $Vault

$Jobs    = Get-AzRecoveryServicesBackupJob -From (Get-Date).AddHours(-$Config.LookbackHours) -VaultId $Vault.ID
$Summary = [PSCustomObject]@{
    Total    = $Jobs.Count
    Success  = ($Jobs | Where-Object Status -eq 'Completed').Count
    Warnings = ($Jobs | Where-Object Status -eq 'CompletedWithWarnings').Count
    Failed   = ($Jobs | Where-Object Status -eq 'Failed').Count
    Running  = ($Jobs | Where-Object Status -eq 'InProgress').Count
}
Write-Host ($Summary | Format-List | Out-String)

$FailedJobs  = $Jobs | Where-Object Status -eq 'Failed'
$WarningJobs = $Jobs | Where-Object Status -eq 'CompletedWithWarnings'

if ($FailedJobs) {
    $Details = $FailedJobs | ForEach-Object {
        $Duration = if ($_.StartTime -and $_.EndTime) {
            "$([Math]::Round(($_.EndTime - $_.StartTime).TotalMinutes, 1))m"
        } else { 'N/A' }
        "$($_.WorkloadName) | Duration: $Duration | $($_.ErrorDetails.ErrorMessage -replace '\s+', ' ')"
    }
    $AlertText = "$($Summary.Failed) backup job(s) failed in vault '$($Config.VaultName)':`n$($Details -join "`n")"

    Send-TeamsAlert -Title "Azure Backup Failure ($($Summary.Failed) jobs)" `
        -Details ($Details -join ' | ') -Color 'attention'
    Send-MailMessage -To $Config.AlertEmail -From '[email protected]' `
        -Subject "ALERT: Azure Backup Failure - $(Get-Date -f 'yyyy-MM-dd')" `
        -Body $AlertText -SmtpServer $Config.SmtpServer
    Write-Warning $AlertText
}

if ($WarningJobs) {
    $WarnDetails = $WarningJobs | ForEach-Object { "$($_.WorkloadName): $($_.ErrorDetails.ErrorMessage)" }
    Write-Warning "$($Summary.Warnings) job(s) completed with warnings:`n$($WarnDetails -join "`n")"
    Send-TeamsAlert -Title "Backup Warnings ($($Summary.Warnings) jobs)" `
        -Details ($WarnDetails -join '; ') -Color 'warning'
}

if (-not $FailedJobs -and -not $WarningJobs) {
    Write-Host "All $($Summary.Success) backup jobs completed successfully." -ForegroundColor Green
}

Get In Touch

LinkedIn is the best place to reach me. Most of my code is private, but GitHub is there too.