<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wajahat Ali Abid</title>
    <description>The latest articles on DEV Community by Wajahat Ali Abid (@wajahataliabid).</description>
    <link>https://dev.to/wajahataliabid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F197406%2Ff70359f9-e9e0-41ce-b40d-8bd556ee1c19.jpg</url>
      <title>DEV Community: Wajahat Ali Abid</title>
      <link>https://dev.to/wajahataliabid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wajahataliabid"/>
    <language>en</language>
    <item>
      <title>Automated On-Call Roster Alerting System</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:24:00 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/automated-on-call-roster-alerting-system-4gfj</link>
      <guid>https://dev.to/wajahataliabid/automated-on-call-roster-alerting-system-4gfj</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;I built an automated on-call alerting system that reads team schedules from a central Google Sheet and sends Slack notifications tagging the appropriate on-call person every weekday. The system supports multiple scheduling models (weekday-based and sprint-based) and handles team-specific channel routing, all without requiring teams to understand the underlying automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;The challenge was to provide a flexible on-call notification system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports different scheduling preferences across teams (per-day, per-sprint)&lt;/li&gt;
&lt;li&gt;Requires no technical knowledge from teams managing their own rosters&lt;/li&gt;
&lt;li&gt;Avoids alert fatigue by not resending unchanged sprint assignments&lt;/li&gt;
&lt;li&gt;Delivers notifications to both a central channel and team-specific channels&lt;/li&gt;
&lt;li&gt;Operates autonomously with zero maintenance from the DevOps team once a new team onboards&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution Architecture
&lt;/h2&gt;

&lt;p&gt;The system uses a Google Sheet as the single source of truth and a scheduled Lambda function that reads it, resolves on-call assignments, and delivers Slack alerts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;: EventBridge rules trigger the Lambda on weekdays at two different times for two modes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Source&lt;/strong&gt;: Google Sheets API reads team tabs and reserved system tabs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution Logic&lt;/strong&gt;: Determines the on-call person based on the team's tab structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management&lt;/strong&gt;: A cache tab tracks current assignments to detect changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification&lt;/strong&gt;: Slack API delivers alerts with user mentions to appropriate channels&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scheduling Modes
&lt;/h3&gt;

&lt;p&gt;The system supports two modes, automatically detected by the column headers in each team's worksheet:&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;The challenge was to provide a flexible on-call notification system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports different scheduling preferences across teams (per-day, per-sprint)&lt;/li&gt;
&lt;li&gt;Requires no technical knowledge from teams managing their own rosters&lt;/li&gt;
&lt;li&gt;Avoids alert fatigue by not resending unchanged sprint assignments&lt;/li&gt;
&lt;li&gt;Delivers notifications to both a central channel and team-specific channels&lt;/li&gt;
&lt;li&gt;Operates autonomously with zero maintenance from the DevOps team once a new team onboards&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution Architecture
&lt;/h2&gt;

&lt;p&gt;The system uses a Google Sheet as the single source of truth and a scheduled Lambda function that reads it, resolves on-call assignments, and delivers Slack alerts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;: EventBridge rules trigger the Lambda on weekdays at two different times for two modes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Source&lt;/strong&gt;: Google Sheets API reads team tabs and reserved system tabs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution Logic&lt;/strong&gt;: Determines the on-call person based on the team's tab structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management&lt;/strong&gt;: A cache tab tracks current assignments to detect changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification&lt;/strong&gt;: Slack API delivers alerts with user mentions to appropriate channels&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scheduling Modes
&lt;/h3&gt;

&lt;p&gt;The system supports two modes, automatically detected by the column headers in each team's worksheet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Default Mode (12:00 UTC daily)
&lt;/h4&gt;

&lt;p&gt;For teams that assign on-call per weekday. The system checks today's day of the week and looks up the matching person. Every weekday, an alert is sent regardless of whether the person changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection&lt;/strong&gt;: Team tab has a "Day of week" column.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85m3y6ckjr785ndjspk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85m3y6ckjr785ndjspk6.png" alt="Default Mode" width="600" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Sprint Mode (05:00 UTC daily)
&lt;/h4&gt;

&lt;p&gt;For teams that assign on-call per sprint or date range. Each row has From/To dates. The system finds which range covers today, then checks if the person differs from the last alert sent. Only when the person changes does a new alert go out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection&lt;/strong&gt;: Team tab has both "From" and "To" columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzm21s01qbny92kxi0lwh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzm21s01qbny92kxi0lwh.png" alt="Sprint Mode" width="600" height="1060"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means teams never need to configure which mode they use — the system infers it from how they structure their sheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Sheets API&lt;/strong&gt;: Central data source (schedules, user mappings, team config)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt;: Serverless execution of the alerting logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt;: Scheduled triggers for both modes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack API&lt;/strong&gt;: Notification delivery with user mentions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: Core implementation language&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Core Components
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. On-Call Job (&lt;code&gt;on_call_job.py&lt;/code&gt;)
&lt;/h4&gt;

&lt;p&gt;The main Lambda handler that orchestrates the entire flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event Configuration&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sprint"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Flow&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validates the job identifier and skips weekends&lt;/li&gt;
&lt;li&gt;Opens the Google Sheet and reads all team tabs&lt;/li&gt;
&lt;li&gt;For each team, detects scheduling mode from headers&lt;/li&gt;
&lt;li&gt;Skips teams that don't match the current mode&lt;/li&gt;
&lt;li&gt;Resolves the on-call person using mode-specific logic&lt;/li&gt;
&lt;li&gt;For Sprint mode: compares against cache and skips if unchanged&lt;/li&gt;
&lt;li&gt;Looks up the person's Slack user ID from the data tab&lt;/li&gt;
&lt;li&gt;Sends alert to the central channel&lt;/li&gt;
&lt;li&gt;Sends alert to dedicated team channel if configured&lt;/li&gt;
&lt;li&gt;Updates the cache with the current assignment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Helper Class&lt;/strong&gt; (&lt;code&gt;_OnCallJobHelper&lt;/code&gt;):&lt;br&gt;
Static methods encapsulating the resolution logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is_sprint_team(headers)&lt;/code&gt; — detects mode from column headers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolve_default_on_call(...)&lt;/code&gt; — day-of-week or cache-based lookup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resolve_sprint_on_call(...)&lt;/code&gt; — date range matching&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;has_on_call_person_changed(...)&lt;/code&gt; — cache comparison for deduplication&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Google Sheet Structure
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Reserved System Tabs
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tab&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps team member names to Slack user IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tracks current on-call person per team (managed by automation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_Teams&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Team configuration (dedicated Slack channel)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Team Tabs — Default Mode
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Day of week&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Backup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monday&lt;/td&gt;
&lt;td&gt;Alice Smith&lt;/td&gt;
&lt;td&gt;Bob Jones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuesday&lt;/td&gt;
&lt;td&gt;Charlie Brown&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Team Tabs — Sprint Mode
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Backup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-14&lt;/td&gt;
&lt;td&gt;2026-07-25&lt;/td&gt;
&lt;td&gt;Alice Smith&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-07-28&lt;/td&gt;
&lt;td&gt;2026-08-08&lt;/td&gt;
&lt;td&gt;Bob Jones&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Alert Deduplication (Sprint Mode)
&lt;/h3&gt;

&lt;p&gt;The key design challenge for sprint-based teams was avoiding daily alert spam. The solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On each run, resolve today's on-call person from the date ranges&lt;/li&gt;
&lt;li&gt;Compare against the &lt;code&gt;_Cache&lt;/code&gt; tab which stores the last alerted person&lt;/li&gt;
&lt;li&gt;If unchanged, silently skip — no alert sent&lt;/li&gt;
&lt;li&gt;If different (new sprint started), send alert and update cache&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means sprint teams only see a notification at the boundary between sprints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Channel Alerting
&lt;/h3&gt;

&lt;p&gt;Any team can configure a &lt;code&gt;Dedicated Channel&lt;/code&gt; in the &lt;code&gt;_Teams&lt;/code&gt; tab. When set, the alert is sent to both the central channel (for cross-team visibility) and the team's own channel (for focused notification).&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Service Onboarding
&lt;/h3&gt;

&lt;p&gt;Teams onboard themselves without DevOps involvement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add members to &lt;code&gt;_Data&lt;/code&gt; tab with Slack user IDs&lt;/li&gt;
&lt;li&gt;Add team config to &lt;code&gt;_Teams&lt;/code&gt; tab&lt;/li&gt;
&lt;li&gt;Create a worksheet tab with &lt;code&gt;(tbd)&lt;/code&gt; suffix while setting up&lt;/li&gt;
&lt;li&gt;Populate the schedule in their preferred format&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;(tbd)&lt;/code&gt; from the tab name — alerts begin automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;(tbd)&lt;/code&gt; convention acts as a feature flag at the data layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Google Sheets?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Non-technical users can manage schedules without learning new tools&lt;/li&gt;
&lt;li&gt;Different teams across the organization already use Sheets daily — no adoption friction&lt;/li&gt;
&lt;li&gt;Built-in tab-level protection allows locking down each team's tab so only authorized members can edit their own roster, preventing accidental cross-team changes&lt;/li&gt;
&lt;li&gt;Teams can self-service without submitting tickets to DevOps&lt;/li&gt;
&lt;li&gt;Immediate visibility into who is on call (just open the sheet)&lt;/li&gt;
&lt;li&gt;No deployment required to update schedules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Detect Mode from Headers Instead of Configuration?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zero configuration burden on teams&lt;/li&gt;
&lt;li&gt;Impossible to misconfigure — the structure IS the configuration&lt;/li&gt;
&lt;li&gt;Adding a "From" and "To" column is the only step to switch modes&lt;/li&gt;
&lt;li&gt;No separate config tab entry to keep in sync&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Cache-Based Deduplication?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sprint-based teams would get the same alert every single day otherwise&lt;/li&gt;
&lt;li&gt;Polling the sheet daily is cheap; sending alerts daily is noisy&lt;/li&gt;
&lt;li&gt;Cache comparison is simple (string equality on name) and reliable&lt;/li&gt;
&lt;li&gt;Cache also serves as a "last known state" for operational visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Two Separate Schedules?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sprint alerts at 05:00 UTC so teams know about changes before workday begins&lt;/li&gt;
&lt;li&gt;Default alerts at 12:00 UTC (noon) as a mid-day reminder of who is on call&lt;/li&gt;
&lt;li&gt;Separating them makes it easy to adjust timing per mode independently&lt;/li&gt;
&lt;li&gt;Each invocation only processes relevant teams, keeping execution fast&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Skip Weekends?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;On-call coverage during weekends follows a different process (PagerDuty)&lt;/li&gt;
&lt;li&gt;Sending alerts to empty offices creates noise and degrades trust in the system&lt;/li&gt;
&lt;li&gt;Weekend coverage for critical systems uses separate escalation paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Operational Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Common Failure Modes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name mismatch&lt;/strong&gt;: Names in team tabs must exactly match &lt;code&gt;_Data&lt;/code&gt; (case-sensitive)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing date range&lt;/strong&gt;: Sprint teams with no row covering today get silently skipped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty Slack ID&lt;/strong&gt;: Person gets mentioned as empty string in Slack (no notification)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Maintenance Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sprint teams must add future date ranges before current ones expire&lt;/li&gt;
&lt;li&gt;New team members need entries in &lt;code&gt;_Data&lt;/code&gt; with correct Slack user IDs&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;_Cache&lt;/code&gt; tab should never be manually edited&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Monitoring
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda execution logs capture which teams were processed and which were skipped&lt;/li&gt;
&lt;li&gt;Warning-level log emitted when no sprint range covers today for a team&lt;/li&gt;
&lt;li&gt;Error-level log for unknown job identifiers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Let the Data Be the Config&lt;/strong&gt;: Detecting mode from sheet structure eliminated an entire category of misconfiguration errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert Fatigue is Real&lt;/strong&gt;: The cache-based deduplication for sprint mode was critical — without it, teams would have muted the channel within a week&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Service Scales&lt;/strong&gt;: The &lt;code&gt;(tbd)&lt;/code&gt; convention for in-progress teams meant zero DevOps tickets for onboarding after the first month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-Technical Users Need Guardrails, Not Documentation&lt;/strong&gt;: Case-sensitive name matching is the #1 support issue — explicit validation in the code would be a worthwhile future addition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate Concerns by Time&lt;/strong&gt;: Running Default and Sprint at different times simplified debugging and made it possible to adjust cadence per mode without code changes&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Secure Docker Builds with Secrets &amp; Multi-Platform CI</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Fri, 10 Jul 2026 13:30:16 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/secure-docker-builds-with-secrets-multi-platform-ci-538n</link>
      <guid>https://dev.to/wajahataliabid/secure-docker-builds-with-secrets-multi-platform-ci-538n</guid>
      <description>&lt;h2&gt;
  
  
  1. Concept: Build Secrets vs. Build Args
&lt;/h2&gt;

&lt;p&gt;When building Docker images, you often need sensitive data (NuGet tokens, NPM keys, API credentials) to fetch dependencies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build Args (&lt;code&gt;ARG&lt;/code&gt;)&lt;/strong&gt;: These are stored in the image's history. Running &lt;code&gt;docker history &amp;lt;image&amp;gt;&lt;/code&gt; will reveal these values to anyone with access to the image. &lt;strong&gt;Never use these for secrets.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Secrets (&lt;code&gt;--secret&lt;/code&gt;)&lt;/strong&gt;: These are temporarily mounted into a specific &lt;code&gt;RUN&lt;/code&gt; command. They exist only in memory or as a temporary file during that specific layer's execution. They are &lt;strong&gt;never&lt;/strong&gt; committed to the image layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The Universal Dockerfile Template
&lt;/h2&gt;

&lt;p&gt;This template uses a multi-stage build, a non-root user for security, and the Secret Mount syntax.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- Stage 1: Build ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;mcr.microsoft.com/dotnet/sdk:10.0-bookworm-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build-env&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Non-sensitive configuration via ARGs&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; PACKAGE_SOURCE_URL&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SOURCE_NAME&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SOURCE_USER&lt;/span&gt;

&lt;span class="c"&gt;# Copy project files for restoration&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; *.slnx ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/**/*.csproj ./src/&lt;/span&gt;

&lt;span class="c"&gt;# Use the Secret Mount to safely handle the token&lt;/span&gt;
&lt;span class="c"&gt;# The id 'app_token' must match the CLI --secret id&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;app_token &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /run/secrets/app_token&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    dotnet nuget add &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_SOURCE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--username&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--store-password-in-clear-text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    dotnet restore

&lt;span class="c"&gt;# Copy remaining source and publish&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet publish &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;-o&lt;/span&gt; /app/output &lt;span class="nt"&gt;--no-restore&lt;/span&gt;

&lt;span class="c"&gt;# --- Stage 2: Runtime ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/dotnet/runtime:10.0-bookworm-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Security: Create a non-root user&lt;/span&gt;
&lt;span class="c"&gt;# 'useradd' is used instead of 'adduser' for compatibility with 'slim' images&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /app &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/bash appuser

&lt;span class="c"&gt;# Copy output and set ownership&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build-env --chown=appuser:appuser /app/output .&lt;/span&gt;

&lt;span class="c"&gt;# Optional: Install diagnostics tools&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["dotnet", "MyApplication.dll"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. CI/CD Integration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A. GitHub Actions Implementation
&lt;/h3&gt;

&lt;p&gt;GitHub Actions requires you to explicitly map a Secret to an environment variable in the shell before Docker can consume it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker Image&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Map the GitHub Secret to a shell environment variable&lt;/span&gt;
    &lt;span class="na"&gt;READONLY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ORG_READONLY_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# Ensure BuildKit is enabled&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;docker build \&lt;/span&gt;
      &lt;span class="s"&gt;--secret id=app_token,env=READONLY_TOKEN \&lt;/span&gt;
      &lt;span class="s"&gt;--build-arg PACKAGE_SOURCE_URL="${{ secrets.SOURCE_URL }}" \&lt;/span&gt;
      &lt;span class="s"&gt;--build-arg SOURCE_NAME="InternalFeed" \&lt;/span&gt;
      &lt;span class="s"&gt;--build-arg SOURCE_USER="${{ secrets.SOURCE_USER }}" \&lt;/span&gt;
      &lt;span class="s"&gt;-t my-app:latest .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. AWS CodeBuild Implementation
&lt;/h3&gt;

&lt;p&gt;CodeBuild retrieves secrets from AWS Secrets Manager and populates them as environment variables automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Pulls directly from AWS Secrets Manager&lt;/span&gt;
  &lt;span class="na"&gt;secrets-manager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;BUILD_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path/to/secret:key_name"&lt;/span&gt;

&lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;export DOCKER_BUILDKIT=1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;docker build \&lt;/span&gt;
          &lt;span class="s"&gt;--secret id=app_token,env=BUILD_TOKEN \&lt;/span&gt;
          &lt;span class="s"&gt;--build-arg PACKAGE_SOURCE_URL="${SOURCE_URL}" \&lt;/span&gt;
          &lt;span class="s"&gt;--build-arg SOURCE_NAME="InternalRepo" \&lt;/span&gt;
          &lt;span class="s"&gt;--build-arg SOURCE_USER="${SOURCE_USER}" \&lt;/span&gt;
          &lt;span class="s"&gt;-f Dockerfile \&lt;/span&gt;
          &lt;span class="s"&gt;-t $REPOSITORY_URI:latest .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Critical Troubleshooting Guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Issue&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Root Cause&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;adduser: not found&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Using a Debian "slim" image.&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;useradd&lt;/code&gt; (the low-level binary) instead of &lt;code&gt;adduser&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Both UserName and Password must be specified&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A variable passed to &lt;code&gt;dotnet nuget&lt;/code&gt; is empty.&lt;/td&gt;
&lt;td&gt;1. Ensure &lt;code&gt;ARG&lt;/code&gt; is declared &lt;em&gt;after&lt;/em&gt; the &lt;code&gt;FROM&lt;/code&gt; line. 2. Wrap all vars in double quotes &lt;code&gt;"$VAR"&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Secret not found&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ID mismatch between CLI and Dockerfile.&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;--secret id=XYZ&lt;/code&gt; matches &lt;code&gt;RUN --mount=type=secret,id=XYZ&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;Invalid Build Flag&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BuildKit is disabled.&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;DOCKER_BUILDKIT=1&lt;/code&gt; or use &lt;code&gt;docker buildx build&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. Best Practices Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Multi-stage builds&lt;/strong&gt;: Always separate the SDK (Build) from the Runtime (Execution) to minimize attack surface.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Non-root execution&lt;/strong&gt;: Never run your application as &lt;code&gt;root&lt;/code&gt; inside the container.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Layer Optimization&lt;/strong&gt;: Copy &lt;code&gt;.csproj&lt;/code&gt; or &lt;code&gt;.slnx&lt;/code&gt; files and &lt;code&gt;restore&lt;/code&gt; before copying the full source code to take advantage of layer caching.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Secret Hygiene&lt;/strong&gt;: Never echo secrets to the console. To verify if a secret exists during debug, check its length: &lt;code&gt;echo ${#TOKEN}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Quotes&lt;/strong&gt;: Always wrap shell variables in double quotes to prevent word-splitting if a password or URL contains special characters.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>docker</category>
      <category>security</category>
    </item>
    <item>
      <title>bash: No such file or directory</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Sat, 30 Mar 2024 18:43:03 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/bash-no-such-file-or-directory-n83</link>
      <guid>https://dev.to/wajahataliabid/bash-no-such-file-or-directory-n83</guid>
      <description>&lt;p&gt;I am using KDE Neon as my daily driver on my laptop. Recently due to KDE Neon update 6.0, there have been many bugs, but one as annoying as the one I faced the other day.&lt;/p&gt;

&lt;p&gt;After system update, my bash stopped working completely. Granted I use zsh as my shell, bash is required by many programs on Linux. But everytime, I opened any such program, say pyenv, it just wouldn't work and give following error&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/env: ‘bash’: No such file or directory linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I looked into /usr/bin and couldn't find bash there. I couldn't install bash using dpkg or apt. This was pretty perplexing for me. Here is how I resolved the error with bash. &lt;/p&gt;

&lt;p&gt;First of all, I edited the file /var/lib/dpkg/info/bash.prerm and changed first line to &lt;code&gt;#! /bin/sh&lt;/code&gt; from &lt;code&gt;#! /bin/bash&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#! /bin/sh
set -e  

case "$1" in  
&amp;nbsp;&amp;nbsp;&amp;nbsp;upgrade)  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;update-alternatives --remove builtins.7.gz \  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;/usr/share/man/man7/bash-builtins.7.gz  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;;;  

&amp;nbsp;&amp;nbsp;&amp;nbsp;remove|deconfigure)  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;;;  

&amp;nbsp;&amp;nbsp;&amp;nbsp;failed-upgrade)  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;;;  
&amp;nbsp;&amp;nbsp;&amp;nbsp;*)  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;echo "prerm called with unknown argument \`$1'" &amp;gt;&amp;amp;2  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;exit 1  
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;;;  
esac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next I looked into /var/cache/apt/archives/ and found the file &lt;code&gt;bash_5.1-6ubuntu1.1_amd64.deb&lt;/code&gt;. If you cannot find this file, you can download from &lt;a href="https://ubuntu.pkgs.org/22.04/ubuntu-main-amd64/bash_5.1-6ubuntu1_amd64.deb.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
Next I ran the command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dpkg --force-remove-reinstreq -i /var/cache/apt/archives/bash_5.1-6ubuntu1.1_amd64.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helped me restore the bash and helped me run programs again. &lt;/p&gt;

</description>
      <category>linux</category>
      <category>kde</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>Stream Windows Event Logs to Cloudwatch</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Mon, 04 Mar 2024 19:29:18 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/stream-windows-event-logs-to-cloudwatch-4681</link>
      <guid>https://dev.to/wajahataliabid/stream-windows-event-logs-to-cloudwatch-4681</guid>
      <description>&lt;p&gt;I received a requirement for sending Windows event logs to Amazon Cloudwatch because we wanted to monitor user activity on various Windows servers in our environment. There can be various other use cases for this requirement, however we will focus on setting up Windows event logs to Cloudwatch in this article.&lt;/p&gt;

&lt;p&gt;To achieve this, we first need to install Amazon Cloudwatch Agent and then configure the server to push logs to Cloudwatch Logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Cloudwatch Agent
&lt;/h2&gt;

&lt;p&gt;There are a couple of ways you can install Amazon Cloudwatch Agent on your servers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/installing-cloudwatch-agent-commandline.html" rel="noopener noreferrer"&gt;Using the CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/installing-cloudwatch-agent-ssm.html" rel="noopener noreferrer"&gt;Using AWS Systems Manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0l9r6vv2zc1rn0rok19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp0l9r6vv2zc1rn0rok19.png" alt="Windows Services app showing Cloudwatch Agent running" width="795" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Iam Role
&lt;/h2&gt;

&lt;p&gt;Create a policy &lt;code&gt;CloudwatchAgentPolicyForWindowsLogging&lt;/code&gt;  with following body&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudwatchLogsStatement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogStreams"
            ],
            "Resource": [
                "arn:aws:logs:us-west-2:123456789012:log-group:windows-event-viewer-logs",
                "arn:aws:logs:us-west-2:123456789012:log-group:windows-event-viewer-logs:log-stream:*"
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach this policy to any server you want to enable Cloudwatch Logs on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Cloudwatch Agent
&lt;/h2&gt;

&lt;p&gt;Go to &lt;code&gt;C:\Program Files\Amazon\AmazonCloudWatchAgent&lt;/code&gt; and create a file config.json&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiclgrdf2o119xj3l8ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiclgrdf2o119xj3l8ik.png" alt="Required directory open showing config.json file" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add logs section to Amazon Cloudwatch Agent configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ 
    ...,
    "logs": {
        "logs_collected": {
            "windows_events": {
                "collect_list": [
                    {
                        "event_format": "xml",
                        "event_levels": [
                            "VERBOSE",
                            "INFORMATION",
                            "WARNING",
                            "ERROR",
                            "CRITICAL"
                        ],
                        "event_name": "System",
                        "log_group_name": "windows-event-viewer-logs",
                        "log_stream_name": "{instance_id}/System",
                        "retention_in_days": 365
                    },
                    {
                        "event_format": "xml",
                        "event_levels": [
                            "VERBOSE",
                            "INFORMATION",
                            "WARNING",
                            "ERROR",
                            "CRITICAL"
                        ],
                        "event_name": "Security",
                        "log_group_name": "windows-event-viewer-logs",
                        "log_stream_name": "{instance_id}/Security",
                        "retention_in_days": 365
                    }
                ]
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open Powershell and run the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; cd 'C:/Program Files/Amazon/AmazonCloudWatchAgent/'
&amp;gt; ./amazon-cloudwatch-agent-ctl.ps1 -a fetch-config -m ec2 -c file:config.json -s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhgavbnxt8ebgksh7iwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhgavbnxt8ebgksh7iwl.png" alt="Showing output of above command" width="798" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start the Amazon Cloudwatch Agent Service and after some time, you'll see log stream created in the log group &lt;code&gt;windows-event-viewer-logs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify
&lt;/h2&gt;

&lt;p&gt;Open Cloudwatch Logs and open the log group &lt;code&gt;windows-event-viewer-logs&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ipultxtvov0eowb7vpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3ipultxtvov0eowb7vpo.png" alt="CloudWatch log group windows-event-viewer-logs" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>cloudwatch</category>
      <category>logging</category>
    </item>
    <item>
      <title>Enable ContainerLogV2 on AKS</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Wed, 28 Feb 2024 22:09:43 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/enable-containerlogv2-on-aks-28d6</link>
      <guid>https://dev.to/wajahataliabid/enable-containerlogv2-on-aks-28d6</guid>
      <description>&lt;p&gt;ContainerLogV2 provides additional data over ContainerLog (which is v1). The additional data includes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ContainerName&lt;/li&gt;
&lt;li&gt;PodName&lt;/li&gt;
&lt;li&gt;PodNamespace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To enable ContainerLogV2 on an existing AKS cluster, you need to create a configmap. Here's how you do this&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download the &lt;a href="https://aka.ms/container-azm-ms-agentconfig" rel="noopener noreferrer"&gt;template ConfigMap YAML file&lt;/a&gt; for ContainerLog&lt;/li&gt;
&lt;li&gt;Set containerlog_schema_version = "v2" in the yaml file&lt;/li&gt;
&lt;li&gt;Update the exclude_namespaces under &lt;code&gt;log_collection_settings.stdout&lt;/code&gt; and &lt;code&gt;log_collection_settings.stderr&lt;/code&gt; as per your use case&lt;/li&gt;
&lt;li&gt;Save the changes&lt;/li&gt;
&lt;li&gt;Connect to Kubernetes cluster using CLI
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectx my-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Run the following command to create the configmap.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f container-azm-ms-agentconfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps. Refer to &lt;a href="https://learn.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-logs-schema" rel="noopener noreferrer"&gt;Container insights log schema&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-data-collection-configmap" rel="noopener noreferrer"&gt;Configure data collection in Container insights using ConfigMap&lt;/a&gt; for more insights.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>aks</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Yet another implementation for Slack Commands</title>
      <dc:creator>Wajahat Ali Abid</dc:creator>
      <pubDate>Mon, 19 Sep 2022 13:27:48 +0000</pubDate>
      <link>https://dev.to/wajahataliabid/yet-another-implementation-for-slack-commands-4bmd</link>
      <guid>https://dev.to/wajahataliabid/yet-another-implementation-for-slack-commands-4bmd</guid>
      <description>&lt;p&gt;We're using slack for communication within our team. Using slack slash commands, we also handle common day-to-day tasks like triggering deployments on different servers based on requirements. For a long time, that remained our primary use case, so we had a single AWS Lambda function taking care of this job, however, with the growing team and business, we felt the need to handle several other tasks this way. Thus we created a few more lambda functions that handled different tasks but the behavior among different lambda functions wasn't consistent, since every lambda was maintained separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  General Idea
&lt;/h2&gt;

&lt;p&gt;We decided to create a single lambda function that will be solely used for handling all the slash command requirements. The commands will follow a similar structure as normal shell commands do. Some examples are as follows&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;help&lt;/td&gt;
&lt;td&gt;List down all the slash commands with usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;help deploy&lt;/td&gt;
&lt;td&gt;List down help text for deploy commands, including all the options it takes and flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deploy&lt;/td&gt;
&lt;td&gt;Deploy for a specific client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some examples of slack slash commands are as follows (assuming our slash command is /example)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/example help
/example help deploy
/example help deploy --client test-client --branch master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can limit users who can issue a command as well as channels where the command can be issued from by using a very simple configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deploy"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"example.user"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"channel_id"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;We implemented this solution in python with the help of some useful packages from the open source community. Some of these packages are&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;Fast Api&lt;/a&gt;: For the implementation of the APIs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://mangum.io/" rel="noopener noreferrer"&gt;Mangum&lt;/a&gt;: For the integration of the Aws Lambda and the Api Gateway with the FastApi &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://python-jenkins.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Python Jenkins&lt;/a&gt;: For interacting with the Jenkins server that handles deployments&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.python.org/3/library/argparse.html" rel="noopener noreferrer"&gt;Argparse&lt;/a&gt;: For handling argument parsing &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The way our implementation worked was as follows&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The User sends a command via slack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lambda function verifies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the command exists&lt;/li&gt;
&lt;li&gt;If the user is allowed to run the command&lt;/li&gt;
&lt;li&gt;If the command is allowed to be run from the specified channel&lt;/li&gt;
&lt;li&gt;If the command has all the required parameters&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and responds appropriately&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Execute the command and send the response back to the slack channel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Offload the long running commands to the Ecs container (e.g, database backup or restore)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general architecture of this solution is as follows&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8rczuzsg7f1cbaz0p22f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8rczuzsg7f1cbaz0p22f.png" alt=" " width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This helps us implement new slack commands really quickly. All the error handling is similar across the different commands, so we don't need to handle the errors separately for every new command.&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>api</category>
    </item>
  </channel>
</rss>
