<?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: Cyril Sebastian</title>
    <description>The latest articles on DEV Community by Cyril Sebastian (@cyrilsebastian).</description>
    <link>https://dev.to/cyrilsebastian</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2190542%2F25259711-53e6-4b72-a4a6-9e60ecda484b.jpeg</url>
      <title>DEV Community: Cyril Sebastian</title>
      <link>https://dev.to/cyrilsebastian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cyrilsebastian"/>
    <language>en</language>
    <item>
      <title>Keep It Running: Email Keep It Running: Email Infrastructure Operations Guide</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Fri, 27 Mar 2026 14:59:53 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/keep-it-running-email-keep-it-running-email-infrastructure-operations-guide-1hh8</link>
      <guid>https://dev.to/cyrilsebastian/keep-it-running-email-keep-it-running-email-infrastructure-operations-guide-1hh8</guid>
      <description>&lt;p&gt;Making email infrastructure a pro.&lt;/p&gt;




&lt;h2&gt;
  
  
  Daily Operations (5 Minutes)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Morning Health Check
&lt;/h3&gt;

&lt;p&gt;Create this script and run it daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/email-health.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
YESTERDAY=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"yesterday"&lt;/span&gt; +&lt;span class="s2"&gt;"%b %d"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Email Health (&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="sh"&gt;)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# Service status
systemctl is-active --quiet postfix &amp;amp;&amp;amp; echo "Postfix" || echo "Postfix DOWN"
systemctl is-active --quiet ses-logger &amp;amp;&amp;amp; echo "Logger" || echo "Logger DOWN"

# Email stats
SENT=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/postfix.log 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"status=sent"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
DELIVERED=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/mail.log 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
BOUNCED=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$YESTERDAY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/mail.log 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"status=bounced"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;

echo ""
echo "📊 Volume"
echo "   Sent: &lt;/span&gt;&lt;span class="nv"&gt;$SENT&lt;/span&gt;&lt;span class="sh"&gt;"
echo "   Delivered: &lt;/span&gt;&lt;span class="nv"&gt;$DELIVERED&lt;/span&gt;&lt;span class="sh"&gt;"
echo "   Bounced: &lt;/span&gt;&lt;span class="nv"&gt;$BOUNCED&lt;/span&gt;&lt;span class="sh"&gt;"

if [ &lt;/span&gt;&lt;span class="nv"&gt;$SENT&lt;/span&gt;&lt;span class="sh"&gt; -gt 0 ]; then
  DELIVERY_RATE=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;DELIVERED &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; SENT&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="sh"&gt;
  BOUNCE_RATE=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;BOUNCED &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; SENT&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="sh"&gt;
  echo ""
  echo "📈 Rates"
  echo "   Delivery: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DELIVERY_RATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;%"
  echo "   Bounce: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BOUNCE_RATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;%"

  [ &lt;/span&gt;&lt;span class="nv"&gt;$BOUNCE_RATE&lt;/span&gt;&lt;span class="sh"&gt; -gt 5 ] &amp;amp;&amp;amp; echo "   ⚠️  High bounce rate!"
fi

# Queue status
QUEUE=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mailq | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $5}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
[ "&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE&lt;/span&gt;&lt;span class="sh"&gt;" = "empty" ] &amp;amp;&amp;amp; echo "" &amp;amp;&amp;amp; echo "Queue empty" || echo "" &amp;amp;&amp;amp; echo "⚠️  Queue: &lt;/span&gt;&lt;span class="nv"&gt;$QUEUE&lt;/span&gt;&lt;span class="sh"&gt; messages"

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/email-health.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./email-health.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Automate it&lt;/strong&gt; (runs at 9 AM, emails you results):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"0 9 * * * ~/email-health.sh | mail -s 'Email Health Report' admin@yourdomain.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; | crontab -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Essential Monitoring
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Real-Time Log Watching
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Monitor live email flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Watch everything&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/&lt;span class="k"&gt;*&lt;/span&gt;.log

&lt;span class="c"&gt;# Watch only delivered emails&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt;

&lt;span class="c"&gt;# Watch bounces&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--line-buffered&lt;/span&gt; &lt;span class="s2"&gt;"status=bounced"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Key Metrics to Track
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Alert If&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Delivery Rate&lt;/td&gt;
&lt;td&gt;&amp;gt;95%&lt;/td&gt;
&lt;td&gt;&amp;lt;90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bounce Rate&lt;/td&gt;
&lt;td&gt;&amp;lt;5%&lt;/td&gt;
&lt;td&gt;&amp;gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue Size&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&amp;gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service Uptime&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;Any downtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Quick metric checks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Today's delivery rate&lt;/span&gt;
&lt;span class="nv"&gt;SENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%b&lt;span class="se"&gt;\ &lt;/span&gt;%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"status=sent"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DELIVERED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%b&lt;span class="se"&gt;\ &lt;/span&gt;%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Delivery rate: &lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;DELIVERED &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; SENT&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;

&lt;span class="c"&gt;# Average delivery time (milliseconds)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'delay=\K\d+'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{sum+=$1; n++} END {print "Avg delay: " sum/n "ms"}'&lt;/span&gt;

&lt;span class="c"&gt;# Top recipient domains&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'to=&amp;lt;[^@]+@\K[^&amp;gt;]+'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adding New Senders
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Edit whitelist&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/allowed_senders

&lt;span class="c"&gt;# Add line:&lt;/span&gt;
&lt;span class="c"&gt;# newsender@yourdomain.com    OK&lt;/span&gt;

&lt;span class="c"&gt;# 2. Rebuild database&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/allowed_senders

&lt;span class="c"&gt;# 3. Reload (no restart needed!)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload postfix

&lt;span class="c"&gt;# 4. Test&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; newsender@yourdomain.com &lt;span class="nb"&gt;test&lt;/span&gt;@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No downtime!&lt;/strong&gt; Reload picks up changes instantly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Removing Senders
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Comment out or remove from whitelist&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/allowed_senders
&lt;span class="c"&gt;# #oldsender@yourdomain.com    OK&lt;/span&gt;

&lt;span class="c"&gt;# 2. Rebuild and reload&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/allowed_senders
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload postfix

&lt;span class="c"&gt;# 3. Verify rejection&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; oldsender@yourdomain.com &lt;span class="nb"&gt;test&lt;/span&gt;@example.com
&lt;span class="c"&gt;# Should see: "Sender address rejected"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Managing Mail Queue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;View queue:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mailq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Flush queue&lt;/strong&gt; (retry all deferred emails):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postqueue &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete specific email:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get queue ID from mailq&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postsuper &lt;span class="nt"&gt;-d&lt;/span&gt; QUEUE_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete all queued emails:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postsuper &lt;span class="nt"&gt;-d&lt;/span&gt; ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete only deferred emails:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postsuper &lt;span class="nt"&gt;-d&lt;/span&gt; ALL deferred
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Searching Email History
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Find specific email:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"user@example.com"&lt;/span&gt; /var/log/postfix/&lt;span class="k"&gt;*&lt;/span&gt;.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find by sender:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"from=&amp;lt;sender@yourdomain.com&amp;gt;"&lt;/span&gt; /var/log/postfix/postfix.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Find bounces to specific domain:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"gmail.com"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"bounced"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Get complete email journey:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get message ID from sent log&lt;/span&gt;
&lt;span class="nv"&gt;MSG_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"user@example.com"&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'status=sent \(250 Ok \K[^)]+'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Find all events for that message&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /var/log/postfix/&lt;span class="k"&gt;*&lt;/span&gt;.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem 1: Postfix Won't Start
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptoms:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postfix
&lt;span class="c"&gt;# Job for postfix.service failed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Check config syntax&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postfix check

&lt;span class="c"&gt;# 2. View detailed error&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; postfix &lt;span class="nt"&gt;-n&lt;/span&gt; 20 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;span class="c"&gt;# 3. Common issues:&lt;/span&gt;

&lt;span class="c"&gt;# Port in use?&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :25
&lt;span class="c"&gt;# Kill conflicting process: sudo systemctl stop sendmail&lt;/span&gt;

&lt;span class="c"&gt;# Permission issue?&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; postfix:postfix /var/log/postfix
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; postfix:postfix /var/spool/postfix

&lt;span class="c"&gt;# Check line number from 'postfix check' output&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/main.cf +LINE_NUMBER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Problem 2: Emails Stuck in Queue
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mailq  &lt;span class="c"&gt;# Shows queued emails&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-100&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=deferred"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common causes and fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong SES credentials:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify credentials&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"[email-smtp.ap-south-1.amazonaws.com]:587"&lt;/span&gt; /etc/postfix/sasl_passwd

&lt;span class="c"&gt;# Update if needed&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/sasl_passwd
&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/sasl_passwd
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network blocked:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test SES connectivity&lt;/span&gt;
telnet email-smtp.ap-south-1.amazonaws.com 587

&lt;span class="c"&gt;# Check security group allows outbound 587&lt;/span&gt;
&lt;span class="c"&gt;# Check route table has internet gateway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SES quota exceeded:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ses get-send-quota &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;span class="c"&gt;# If near limit, wait or request increase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After fixing, flush the queue:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postqueue &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Problem 3: Logger Service Keeps Crashing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ses-logger &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/ses-logger-error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;boto3 missing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import boto3"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3-boto3
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wrong queue URL:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get correct URL&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; ses-events-queue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Update service&lt;/span&gt;
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"s|Environment=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SQS_QUEUE_URL=.*&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;|Environment=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SQS_QUEUE_URL=&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;|"&lt;/span&gt; /etc/systemd/system/ses-logger.service

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;IAM permissions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify role attached&lt;/span&gt;
aws sts get-caller-identity

&lt;span class="c"&gt;# Should show: PostfixSESLogger role&lt;/span&gt;
&lt;span class="c"&gt;# If not, reattach IAM instance profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Problem 4: No Delivery Events in Logs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Check SQS queue has messages&lt;/span&gt;
aws sqs get-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; ses-events-queue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-names&lt;/span&gt; ApproximateNumberOfMessages &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If messages are accumulating:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Logger not processing → Check &lt;code&gt;sudo journalctl -u ses-logger&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restart logger → &lt;code&gt;sudo systemctl restart ses-logger&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If no messages in queue:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 2. Verify SES publishing to SNS&lt;/span&gt;
aws ses get-identity-notification-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identities&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1

&lt;span class="c"&gt;# Should show all three topics configured&lt;/span&gt;

&lt;span class="c"&gt;# 3. Reconfigure if needed&lt;/span&gt;
&lt;span class="nv"&gt;SNS_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sns list-topics &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Topics[?contains(TopicArn, 'ses-events-topic')].TopicArn | [0]"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;EVENT &lt;span class="k"&gt;in &lt;/span&gt;Delivery Bounce Complaint&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;aws ses set-identity-notification-topic &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--identity&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--notification-type&lt;/span&gt; &lt;span class="nv"&gt;$EVENT&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--sns-topic&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNS_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Problem 5: High Bounce Rate (&amp;gt;10%)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Analyze bounce reasons:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=bounced"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'reason=\(\K[^\)]+'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common reasons:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"User unknown" (invalid addresses):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extract bounced addresses&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=bounced"&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"bounce_type=Permanent"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'to=&amp;lt;\K[^&amp;gt;]+'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bounced_addresses.txt

&lt;span class="c"&gt;# Remove from your mailing list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Mailbox full":&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Temporary issue, will resolve&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry after 24 hours&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;"550 Spam":&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Review email content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check SPF/DKIM/DMARC setup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify sender reputation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Problem 6: Emails Going to Spam
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Verification checklist:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Check SPF&lt;/span&gt;
dig +short TXT yourdomain.com | &lt;span class="nb"&gt;grep &lt;/span&gt;spf
&lt;span class="c"&gt;# Should include: include:amazonses.com&lt;/span&gt;

&lt;span class="c"&gt;# 2. Check DKIM&lt;/span&gt;
aws ses get-identity-dkim-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identities&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;span class="c"&gt;# Should show: DkimEnabled=true, Status=Success&lt;/span&gt;

&lt;span class="c"&gt;# 3. Check DMARC&lt;/span&gt;
dig +short TXT _dmarc.yourdomain.com
&lt;span class="c"&gt;# Should return DMARC policy&lt;/span&gt;

&lt;span class="c"&gt;# 4. Check SES reputation&lt;/span&gt;
aws ses get-account-sending-enabled &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;span class="c"&gt;# Should be enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Content checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Avoid spam trigger words (FREE!, ACT NOW!)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Include unsubscribe link&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Balance text/image ratio (60% text minimum)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use a consistent "From" name and address&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authenticate with SPF/DKIM/DMARC&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Performance Optimization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Postfix Tuning
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For higher throughput:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add/update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Increase concurrent deliveries
&lt;/span&gt;&lt;span class="py"&gt;default_destination_concurrency_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;
&lt;span class="py"&gt;default_destination_recipient_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;

&lt;span class="c"&gt;# Reduce queue lifetime
&lt;/span&gt;&lt;span class="py"&gt;maximal_queue_lifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;
&lt;span class="py"&gt;bounce_queue_lifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;

&lt;span class="c"&gt;# Connection caching
&lt;/span&gt;&lt;span class="py"&gt;smtp_connection_cache_on_demand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;smtp_connection_cache_destinations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;email-smtp.ap-south-1.amazonaws.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Logger Optimization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For high volume (&amp;gt;1000 events/min):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;/usr/local/bin/ses_logger.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Increase batch size
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxNumberOfMessages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Up from 10
&lt;/span&gt;    &lt;span class="n"&gt;WaitTimeSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Scaling Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When to Scale
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Scale Trigger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU Usage&lt;/td&gt;
&lt;td&gt;Sustained &amp;gt;70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emails/day&lt;/td&gt;
&lt;td&gt;&amp;gt;40,000 (80% of quota)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue Size&lt;/td&gt;
&lt;td&gt;Sustained &amp;gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;&amp;gt;80% used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Vertical Scaling (Bigger Instance)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Current performance by instance:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instance&lt;/th&gt;
&lt;th&gt;vCPU&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;Emails/day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;t3a.small&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2GB&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3a.medium&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4GB&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3a.large&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;c6a.xlarge&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;8GB&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Security Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Restrict Relay Access
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tighten network access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Only specific IPs
&lt;/span&gt;&lt;span class="py"&gt;mynetworks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1, 10.10.3.125&lt;/span&gt;

&lt;span class="c"&gt;# Or specific subnet
&lt;/span&gt;&lt;span class="py"&gt;mynetworks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.0.0/21&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Rate Limiting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prevent abuse:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Max 100 connections/min per client
&lt;/span&gt;&lt;span class="py"&gt;smtpd_client_connection_rate_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;

&lt;span class="c"&gt;# Max 100 emails/min per client
&lt;/span&gt;&lt;span class="py"&gt;smtpd_client_message_rate_limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Monitor IAM Usage
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Enable CloudTrail for audit:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail create-trail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; email-infrastructure-audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--s3-bucket-name&lt;/span&gt; my-audit-logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AWS Documentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/" rel="noopener noreferrer"&gt;SES Developer Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cloudwatch/" rel="noopener noreferrer"&gt;CloudWatch Logs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Postfix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://www.postfix.org/documentation.html" rel="noopener noreferrer"&gt;Official Docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://www.postfix.org/DEBUG_README.html" rel="noopener noreferrer"&gt;Troubleshooting Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Series Complete! 🎉&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/cyrilsebastian/building-production-email-infrastructure-with-postfix-aws-ses-architecture-design-32k3"&gt;Part 1: Architecture &amp;amp; Design&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/cyrilsebastian/from-zero-to-production-building-postfix-aws-ses-in-2-hours-48o8"&gt;Part 2: Implementation Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 3: Operations&lt;/strong&gt; ← . You just finished this&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔗 &lt;strong&gt;If this helped or resonated with you, connect with me on&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;. Let’s learn and grow together.&lt;/p&gt;

&lt;p&gt;👉 Stay tuned for more behind-the-scenes write-ups and system design breakdowns.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>ses</category>
    </item>
    <item>
      <title>From Zero to Production: Building Postfix + AWS SES in 2 Hours</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Tue, 17 Mar 2026 17:49:24 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/from-zero-to-production-building-postfix-aws-ses-in-2-hours-48o8</link>
      <guid>https://dev.to/cyrilsebastian/from-zero-to-production-building-postfix-aws-ses-in-2-hours-48o8</guid>
      <description>&lt;h2&gt;
  
  
  What You Need Before Starting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AWS account with SES access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EC2 instance (t3a.medium, Amazon Linux 2023)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your domain's DNS access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;2 hours of focused time&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. Everything else, we'll build together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: AWS SES Setup (15 mins)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Verify Your Domain
&lt;/h3&gt;

&lt;p&gt;AWS Console → SES → Verified Identities → &lt;strong&gt;Create Identity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identity type: Domain
Domain: yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the TXT record SES provides to your DNS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Type: TXT
Name: _amazonses.yourdomain.com
Value: [provided by SES]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify it worked:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ses get-identity-verification-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identities&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;"VerificationStatus": "Success"&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Configure DKIM (5 mins)
&lt;/h3&gt;

&lt;p&gt;In SES Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Click your domain → DKIM tab → Edit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable &lt;strong&gt;Easy DKIM&lt;/strong&gt; → Save&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add the &lt;strong&gt;3 CNAME records&lt;/strong&gt; SES provides to your DNS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ses get-identity-dkim-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identities&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should show &lt;code&gt;"DkimEnabled": true&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: SPF + DMARC (3 mins)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Add SPF record:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Type: TXT
Name: yourdomain.com
Value: "v=spf1 include:amazonses.com ~all"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Add DMARC record:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Type: TXT  
Name: _dmarc.yourdomain.com
Value: "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 4: Get SMTP Credentials (2 mins)
&lt;/h3&gt;

&lt;p&gt;SES Console → SMTP Settings → &lt;strong&gt;Create SMTP Credentials&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Save these immediately&lt;/strong&gt; (you won't see them again):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Username: AKAWSSAMPLEEXAMPLE
Password: wJalrXUtnuTde/EXAMPLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 2: Postfix Setup (20 mins)
&lt;/h2&gt;

&lt;p&gt;SSH into your server and let's configure Postfix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Postfix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum update &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postfix cyrus-sasl-plain mailx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Configure SES Credentials
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/sasl_passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this line (use YOUR credentials):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[email-smtp.ap-south-1.amazonaws.com]:587 YOUR_USERNAME:YOUR_PASSWORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/postfix/sasl_passwd
&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/sasl_passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Configure Postfix Main Settings
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replace entire contents with:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Basic Settings
&lt;/span&gt;&lt;span class="py"&gt;myhostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;mail.yourdomain.com&lt;/span&gt;
&lt;span class="py"&gt;mydomain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yourdomain.com&lt;/span&gt;
&lt;span class="py"&gt;myorigin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$mydomain&lt;/span&gt;
&lt;span class="py"&gt;inet_interfaces&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
&lt;span class="py"&gt;mynetworks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.0/8, 10.0.0.0/16&lt;/span&gt;
&lt;span class="py"&gt;mydestination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 

&lt;span class="c"&gt;# AWS SES Relay
&lt;/span&gt;&lt;span class="s"&gt;relayhost = [email-smtp.ap-south-1.amazonaws.com]:587&lt;/span&gt;
&lt;span class="py"&gt;smtp_sasl_auth_enable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;smtp_sasl_security_options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;noanonymous&lt;/span&gt;
&lt;span class="py"&gt;smtp_sasl_password_maps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;hash:/etc/postfix/sasl_passwd&lt;/span&gt;

&lt;span class="c"&gt;# TLS Security
&lt;/span&gt;&lt;span class="py"&gt;smtp_use_tls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;span class="py"&gt;smtp_tls_security_level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;encrypt&lt;/span&gt;
&lt;span class="py"&gt;smtp_tls_CAfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/certs/ca-bundle.crt&lt;/span&gt;

&lt;span class="c"&gt;# Sender Validation
&lt;/span&gt;&lt;span class="py"&gt;smtpd_sender_restrictions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;check_sender_access&lt;/span&gt; &lt;span class="err"&gt;hash:/etc/postfix/allowed_senders,&lt;/span&gt;
    &lt;span class="err"&gt;reject&lt;/span&gt;

&lt;span class="py"&gt;smtpd_recipient_restrictions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="err"&gt;permit_mynetworks,&lt;/span&gt;
    &lt;span class="err"&gt;reject_unauth_destination&lt;/span&gt;

&lt;span class="c"&gt;# Logging
&lt;/span&gt;&lt;span class="py"&gt;maillog_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/postfix/postfix.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Create Sender Whitelist
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/postfix/allowed_senders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add approved senders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;info@yourdomain.com         OK
noreply@yourdomain.com      OK
@yourdomain.com             REJECT Not authorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/allowed_senders
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/log/postfix
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;postfix:postfix /var/log/postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Start Postfix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postfix check  &lt;span class="c"&gt;# Should output nothing&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postfix
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test Email"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; info@yourdomain.com your-email@example.com
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/postfix.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;status=sent (250 Ok...)&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 3: Event Pipeline (25 mins)
&lt;/h2&gt;

&lt;p&gt;This is where we set up bounce/delivery tracking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create IAM Role
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trust policy&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;trust-policy.json
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Version"&lt;/span&gt;: &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;,
  &lt;span class="s2"&gt;"Statement"&lt;/span&gt;: &lt;span class="o"&gt;[{&lt;/span&gt;
    &lt;span class="s2"&gt;"Effect"&lt;/span&gt;: &lt;span class="s2"&gt;"Allow"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Principal"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Service"&lt;/span&gt;: &lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"Action"&lt;/span&gt;: &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
  &lt;span class="o"&gt;}]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="c"&gt;# Create role&lt;/span&gt;
aws iam create-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; PostfixSESLogger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; file://trust-policy.json

&lt;span class="c"&gt;# Permissions policy&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;policy.json 
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Version"&lt;/span&gt;: &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;,
  &lt;span class="s2"&gt;"Statement"&lt;/span&gt;: &lt;span class="o"&gt;[{&lt;/span&gt;
    &lt;span class="s2"&gt;"Effect"&lt;/span&gt;: &lt;span class="s2"&gt;"Allow"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Action"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"sqs:ReceiveMessage"&lt;/span&gt;,
      &lt;span class="s2"&gt;"sqs:DeleteMessage"&lt;/span&gt;,
      &lt;span class="s2"&gt;"sqs:GetQueueUrl"&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"Resource"&lt;/span&gt;: &lt;span class="s2"&gt;"arn:aws:sqs:ap-south-1:*:ses-events-queue"&lt;/span&gt;
  &lt;span class="o"&gt;}]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


&lt;span class="c"&gt;# Attach policy&lt;/span&gt;
aws iam put-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; PostfixSESLogger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; SESLogging &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file:///policy.json

&lt;span class="c"&gt;# Create instance profile&lt;/span&gt;
aws iam create-instance-profile &lt;span class="nt"&gt;--instance-profile-name&lt;/span&gt; PostfixSESLogger
aws iam add-role-to-instance-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-profile-name&lt;/span&gt; PostfixSESLogger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; PostfixSESLogger

&lt;span class="c"&gt;# Attach to instance&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://169.254.169.254/latest/meta-data/instance-id&lt;span class="si"&gt;)&lt;/span&gt;
aws ec2 associate-iam-instance-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-instance-profile&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PostfixSESLogger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait 10 seconds, then verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity  &lt;span class="c"&gt;# Should show the role&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Create SQS Queue
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs create-queue &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-name&lt;/span&gt; ses-events-queue &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;QUEUE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_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;--attribute-names&lt;/span&gt; QueueArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Attributes.QueueArn'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Queue URL: &lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Queue ARN: &lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Create SNS Topic and Subscribe SQS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SNS_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sns create-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; ses-events-topic &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'TopicArn'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Allow SNS to send to SQS&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;sqs-policy.json
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Version"&lt;/span&gt;: &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;,
  &lt;span class="s2"&gt;"Statement"&lt;/span&gt;: &lt;span class="o"&gt;[{&lt;/span&gt;
    &lt;span class="s2"&gt;"Effect"&lt;/span&gt;: &lt;span class="s2"&gt;"Allow"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Principal"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Service"&lt;/span&gt;: &lt;span class="s2"&gt;"sns.amazonaws.com"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="s2"&gt;"Action"&lt;/span&gt;: &lt;span class="s2"&gt;"sqs:SendMessage"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Resource"&lt;/span&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;,
    &lt;span class="s2"&gt;"Condition"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ArnEquals"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNS_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;}]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


aws sqs set-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_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;--attributes&lt;/span&gt; &lt;span class="nv"&gt;Policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/sqs-policy.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1

&lt;span class="c"&gt;# Subscribe SQS to SNS&lt;/span&gt;
aws sns subscribe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNS_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; sqs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notification-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Configure SES to Publish Events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;EVENT &lt;span class="k"&gt;in &lt;/span&gt;Delivery Bounce Complaint&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;aws ses set-identity-notification-topic &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--identity&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--notification-type&lt;/span&gt; &lt;span class="nv"&gt;$EVENT&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--sns-topic&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SNS_ARN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Disable email forwarding&lt;/span&gt;
aws ses set-identity-feedback-forwarding-enabled &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identity&lt;/span&gt; yourdomain.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-forwarding-enabled&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 4: Logger Deployment (30 mins)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3-boto3
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import boto3; print('✓ boto3 installed')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Create Logger Script
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /usr/local/bin/ses_logger.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Paste this complete script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="n"&gt;REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ap-south-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openlog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postfix/ses-events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logoption&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_PID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;facility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_MAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: to=&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;, relay=amazonses.com, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_WARNING&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bounce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_INFO&lt;/span&gt;
    &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;notificationType&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mail&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;msg_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;messageId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Delivery&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;delivery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delivery&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recipients&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;processingTimeMillis&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dsn=2.0.0, status=delivered, delay=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="nf"&gt;log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bounce&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;bounce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bounce&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bounce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bouncedRecipients&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dsn=5.0.0, status=bounced, type=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bounce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bounceType&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="nf"&gt;log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;emailAddress&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;queue_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SQS_QUEUE_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: SQS_QUEUE_URL not set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SES Logger Started&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Queue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sqs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;MaxNumberOfMessages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;WaitTimeSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                    &lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;ReceiptHandle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ReceiptHandle&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/bin/ses_logger.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Create Systemd Service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sqs get-queue-url &lt;span class="nt"&gt;--queue-name&lt;/span&gt; ses-events-queue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'QueueUrl'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/systemd/system/ses-logger.service
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SES Event Logger
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;simple
&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;root
&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SQS_QUEUE_URL=&lt;/span&gt;&lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"AWS_DEFAULT_REGION=ap-south-1"&lt;/span&gt;
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/bin/python3 /usr/local/bin/ses_logger.py
&lt;span class="nv"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always
&lt;span class="nv"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;span class="nv"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;append:/var/log/postfix/ses-logger.log
&lt;span class="nv"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;append:/var/log/postfix/ses-logger-error.log

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Start Logger
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;ses-logger
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start ses-logger
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should show &lt;code&gt;Active: active (running)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;View logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/ses-logger.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 5: Testing (20 mins)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test 1: Complete Flow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Send test email:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test from infrastructure"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test Email"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; info@yourdomain.com your-email@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Watch both logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 - Sent status (immediate)&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=sent"&lt;/span&gt;

&lt;span class="c"&gt;# Terminal 2 - Delivered status (10-30 sec delay)&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"status=delivered"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Postfix log (immediate):
status=sent (250 Ok 0109019c...)

# Mail log (after 10-30 seconds):
status=delivered, delay=3558ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Test 2: Bounce Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use SES bounce simulator&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Bounce test"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Bounce Test"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; info@yourdomain.com bounce@simulator.amazonses.com

&lt;span class="c"&gt;# Watch for bounce (1-2 mins)&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/mail.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"bounced"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;status=bounced, type=Permanent&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Test 3: Sender Validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Try unauthorized sender&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Should fail"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; unauthorized@yourdomain.com &lt;span class="nb"&gt;test&lt;/span&gt;@example.com

&lt;span class="c"&gt;# Check rejection&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep &lt;/span&gt;reject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;Sender address rejected: Access denied&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Built
&lt;/h2&gt;

&lt;p&gt;In 2 hours, you created:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;- Postfix SMTP relay&lt;/strong&gt; with sender validation&lt;br&gt;&lt;br&gt;
- &lt;strong&gt;AWS SES integration&lt;/strong&gt; with DKIM/SPF/DMARC&lt;br&gt;&lt;br&gt;
- &lt;strong&gt;Real-time tracking&lt;/strong&gt; for delivery and bounces&lt;br&gt;&lt;br&gt;
- &lt;strong&gt;Unified logging&lt;/strong&gt; - both "sent" and "delivered" in one place&lt;br&gt;&lt;br&gt;
- &lt;strong&gt;Cost-effective&lt;/strong&gt; - ~$30/month vs $90+ for SaaS&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Restart services:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postfix
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;View logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/postfix.log  &lt;span class="c"&gt;# Sent&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/postfix/mail.log     &lt;span class="c"&gt;# Delivered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check queue:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mailq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Search for email:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"user@example.com"&lt;/span&gt; /var/log/postfix/&lt;span class="k"&gt;*&lt;/span&gt;.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common Issues
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Email stuck in queue?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check why&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt; /var/log/postfix/postfix.log | &lt;span class="nb"&gt;grep &lt;/span&gt;deferred
&lt;span class="c"&gt;# Flush after fixing&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;postqueue &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logger not running?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check errors&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ses-logger &lt;span class="nt"&gt;-n&lt;/span&gt; 50
&lt;span class="c"&gt;# Restart&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ses-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No delivery events?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check SQS has messages&lt;/span&gt;
aws sqs get-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_QUEUE_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-names&lt;/span&gt; ApproximateNumberOfMessages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Read &lt;strong&gt;Part 3: Operations &amp;amp; Troubleshooting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;If this helped or resonated with you, connect with me on&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;. Let’s learn and grow together.&lt;/p&gt;

&lt;p&gt;👉 Stay tuned for more behind-the-scenes write-ups and system design breakdowns.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building Production Email Infrastructure with Postfix + AWS SES: Architecture &amp; Design</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Thu, 12 Mar 2026 18:23:20 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/building-production-email-infrastructure-with-postfix-aws-ses-architecture-design-32k3</link>
      <guid>https://dev.to/cyrilsebastian/building-production-email-infrastructure-with-postfix-aws-ses-architecture-design-32k3</guid>
      <description>&lt;p&gt;Learn how to build a scalable, cost-effective email infrastructure using Postfix and AWS SES with complete bounce tracking. Part 1 covers architecture decisions, security, and design patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Architecture &amp;amp; Design Decisions
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Part 1: Architecture &amp;amp; Design&lt;/strong&gt; ← You are here&lt;br&gt;&lt;br&gt;
Part 2: Implementation Guide&lt;br&gt;&lt;br&gt;
Part 3: Operations &amp;amp; Troubleshooting&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What we're building:&lt;/strong&gt; A production email system combining Postfix SMTP relay with AWS SES, complete with real-time bounce tracking, all visible in unified logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Track emails from send → delivery → bounce in one log file, works in private subnets, costs ~$30/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who it's for:&lt;/strong&gt; DevOps engineers, backend developers, and SREs managing email infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Have you ever sent an email and wondered: "Did it actually reach the inbox? Or did it bounce? When?"&lt;/p&gt;

&lt;p&gt;Traditional SMTP relays tell you when they &lt;em&gt;sent&lt;/em&gt; the email, but not when it was &lt;em&gt;delivered&lt;/em&gt;. This gap creates a blind spot in your infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We're Building
&lt;/h3&gt;

&lt;p&gt;An email infrastructure that provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complete visibility&lt;/strong&gt;: Both "sent" and "delivered" statuses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time bounce tracking&lt;/strong&gt;: Know immediately when emails fail&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unified logging&lt;/strong&gt;: Everything in one log file (grep-friendly!)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Private subnet compatible&lt;/strong&gt;: No public endpoints needed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost-effective&lt;/strong&gt;: ~$30/month for 50,000 emails&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enterprise deliverability&lt;/strong&gt;: AWS SES's 99.9% delivery rate&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Who This Series Is For
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;DevOps Engineers&lt;/strong&gt; looking to build a reliable email infrastructure&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Backend Developers&lt;/strong&gt; integrating email into applications&lt;br&gt;&lt;br&gt;
&lt;strong&gt;SREs&lt;/strong&gt; need observability into email delivery&lt;/p&gt;
&lt;h3&gt;
  
  
  Series Overview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; (this post): Understand the architecture and design decisions&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Part 2&lt;/strong&gt;: Step-by-step implementation guide&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Part 3&lt;/strong&gt;: Operations, monitoring, and troubleshooting&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem: Email Observability
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Traditional SMTP Relay Limitations
&lt;/h3&gt;

&lt;p&gt;When you send an email through a standard SMTP relay:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postfix: status=sent (250 Ok)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;But "sent" doesn't mean "delivered"!&lt;/strong&gt; It just means your mail server handed the email to the next server.&lt;/p&gt;

&lt;h4&gt;
  
  
  What You Don't Know
&lt;/h4&gt;

&lt;p&gt;- Did it reach the recipient's inbox?&lt;br&gt;&lt;br&gt;
- Did it bounce?&lt;br&gt;&lt;br&gt;
- Was it marked as spam?&lt;br&gt;&lt;br&gt;
- How long did the delivery take?&lt;br&gt;&lt;br&gt;
- Which ISP was slow?&lt;/p&gt;

&lt;p&gt;This creates a &lt;strong&gt;visibility gap&lt;/strong&gt; in your infrastructure.&lt;/p&gt;
&lt;h3&gt;
  
  
  Solution: The Best of Both Worlds
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your App → Postfix → SES → Recipient
     ↓         ↓        ↓
   Logs    Logs    SNS→SQS→Logger
                         ↓
                  Unified Logs ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;This approach delivers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both statuses&lt;/strong&gt;: "sent" (Postfix) AND "delivered" (SES)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Private subnet&lt;/strong&gt;: No public endpoints required&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Cost-effective&lt;/strong&gt;: ~$30/month&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Standard SMTP&lt;/strong&gt;: No code changes needed&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Unix-friendly&lt;/strong&gt;: Logs searchable with grep/awk&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Full control&lt;/strong&gt;: Own your infrastructure&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Big Picture
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════╗
║     Application Layer            ║
║  "Send email to user@example.com"║
╚════════════╤═════════════════════╝
             │ SMTP • Port 25
             ▼
╔══════════════════════════════════╗
║      Postfix (Private Subnet)    ║
║  ├─ ✓ Sender authorized          ║
║  ├─ ➡ Forwarding to SES...       ║
║  └─ 📋 LOG: status=sent          ║
╚════════════╤═════════════════════╝
             │ SMTP + TLS • Port 587
             ▼
╔══════════════════════════════════╗
║     AWS Simple Email Service    ║
║  "Delivering to recipient..."    ║
╚════════════╤═════════════════════╝
             │
     ┌───────┴───────┐
     ▼               ▼
╔════════════╗ ╔════════════════════╗
║           ║ ║    Event Flow     ║
║  Recipient ║ ║  SNS → SQS → Logger║
║  Mail Server║╚══════════╤═════════╝
╚════════════╝            │
                          ▼
                 ╔════════════════════╗
                ║    Your Logs ✓    ║
                ║  ├─   sent        ║
                ║  ├─   delivered   ║
                ║  └─    bounced     ║
                 ╚════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Data Flow Summary
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application&lt;/strong&gt; sends email to Postfix via SMTP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Postfix&lt;/strong&gt; validates sender, relays to SES&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SES&lt;/strong&gt; delivers email to recipient&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SES&lt;/strong&gt; publishes event (delivery/bounce) to SNS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SNS&lt;/strong&gt; forwards event to SQS queue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python logger&lt;/strong&gt; polls SQS, writes to syslog&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Both "sent" and "delivered" in your logs!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Core Components Explained
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Postfix: The Smart Relay
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; SMTP relay with sender validation and forwarding logic&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Receives emails from your application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Validates sender addresses against the whitelist&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forwards to AWS SES via authenticated SMTP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logs "sent" status immediately&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why Postfix?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Industry standard&lt;/strong&gt;: Powers millions of servers&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Highly configurable&lt;/strong&gt;: Fine-grained control over routing&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Excellent logging&lt;/strong&gt;: Detailed, parseable log format&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Battle-tested&lt;/strong&gt;: Decades of production use&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Handles thousands of concurrent connections&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sender validation (whitelist)
&lt;/span&gt;&lt;span class="py"&gt;smtpd_sender_restrictions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="s"&gt;check_sender_access hash:/etc/postfix/allowed_senders,&lt;/span&gt;
    &lt;span class="err"&gt;reject&lt;/span&gt;

&lt;span class="c"&gt;# Only approved senders can use relay
# Example whitelist:
# info@example.com    OK
# noreply@example.com OK
# @example.com        REJECT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Log Output Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feb 25 00:05:15 mail postfix/smtp[123]: ABC123: 
  to=&amp;lt;user@example.com&amp;gt;, 
  relay=email-smtp.ap-south-1.amazonaws.com:587, 
  delay=0.14, 
  dsn=2.0.0, 
  status=sent (250 Ok 0109019c...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this tells you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Postfix accepted the email&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Email forwarded to SES&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SES accepted the email (250 Ok)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Took 0.14 seconds&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. AWS SES: The Delivery Engine
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Actual email delivery to recipients&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Delivers emails to recipient mail servers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handles DKIM signing for authentication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manages IP reputation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Publishes delivery/bounce events&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why SES?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;99.9% deliverability&lt;/strong&gt;: Enterprise-grade infrastructure&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Global reach&lt;/strong&gt;: AWS's worldwide network&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Pay-as-you-go&lt;/strong&gt;: $0.10 per 1,000 emails&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Built-in auth&lt;/strong&gt;: Automatic SPF, DKIM, DMARC&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Event publishing&lt;/strong&gt;: Real-time delivery notifications&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Scalable&lt;/strong&gt;: From 100 to millions of emails&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event Types:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delivery&lt;/strong&gt; - Email reached the recipient's inbox&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bounce&lt;/strong&gt; - Email rejected (permanent or temporary)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complaint&lt;/strong&gt; - Recipient marked as spam&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why not use SES directly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While SES has an API, using Postfix as a relay provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Standard SMTP interface (no code changes)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sender validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy provider switching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Centralized configuration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better logging&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  3. The Event Pipeline: SNS → SQS → Logger
&lt;/h3&gt;

&lt;p&gt;This is the secret sauce that brings delivery confirmations into your logs.&lt;/p&gt;
&lt;h4&gt;
  
  
  SNS (Simple Notification Service)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Event broadcaster from SES&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SES delivers email
    ↓
SES publishes event to SNS
    ↓
SNS broadcasts to subscribers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why SNS?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Real-time notifications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fan-out to multiple destinations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Native SES integration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filter by event type&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  SQS (Simple Queue Service)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Message buffer between SNS and logger&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample Event:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delivery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mail"&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="nl"&gt;"messageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0109019c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"destination"&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="s2"&gt;"user@example.com"&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="nl"&gt;"delivery"&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="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:05:18.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"smtpResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"250 ok dirdel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"processingTimeMillis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3558&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;p&gt;&lt;strong&gt;Why SQS instead of HTTP webhooks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;critical design decision&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP Webhook Approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SES → SNS → HTTP POST to your server
                      ↓
                Need public endpoint
                Need ALB/Load balancer  
                Security concerns
                Webhook authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SQS Polling Approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SES → SNS → SQS Queue
              ↓
      Python script polls (outbound only)
              ↓
        No public endpoint needed!
        Works in private subnet
        Messages buffered if logger down
        IAM-based authentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits of SQS:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private subnet compatible&lt;/strong&gt;: Polling is outbound-only&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Resilient&lt;/strong&gt;: Messages buffered for 14 days&lt;br&gt;&lt;br&gt;
&lt;strong&gt;No ALB needed&lt;/strong&gt;: Saves $18/month&lt;br&gt;&lt;br&gt;
&lt;strong&gt;More reliable&lt;/strong&gt;: No missed webhooks&lt;br&gt;&lt;br&gt;
&lt;strong&gt;IAM auth&lt;/strong&gt;: No webhook secrets to manage&lt;/p&gt;


&lt;h3&gt;
  
  
  4. The Logger: Python + Syslog
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Poll SQS and write events to Postfix logs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Polls SQS every 20 seconds (long polling)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parses SES delivery/bounce events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writes to syslog (same facility as Postfix)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deletes processed messages from the queue&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code Snippet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syslog&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize SQS client (uses IAM role automatically)
&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ap-south-1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Poll queue (long polling reduces API calls)
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxNumberOfMessages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;WaitTimeSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;# Wait up to 20s for messages
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Process each event
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_ses_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Write to syslog (appears in Postfix logs!)
&lt;/span&gt;    &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openlog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;postfix/ses-events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                   &lt;span class="n"&gt;facility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_MAIL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;syslog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: to=&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status=delivered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove from queue
&lt;/span&gt;    &lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_message&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why Syslog?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same log file&lt;/strong&gt;: Appears alongside Postfix logs&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Auto-rotation&lt;/strong&gt;: System handles log management&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Searchable&lt;/strong&gt;: Standard Unix tools (grep, awk)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Integration&lt;/strong&gt;: Works with existing log aggregators&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Familiar format&lt;/strong&gt;: Same as Postfix log entries&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feb 25 00:05:19 mail postfix/ses-events[456]: 0109019c...: 
  to=&amp;lt;user@example.com&amp;gt;, 
  relay=amazonses.com, 
  dsn=2.0.0, 
  status=delivered, 
  delay=3607ms,
  response=(250 ok dirdel)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this tells you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Email successfully delivered&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Took 3.6 seconds from SES to the inbox&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final SMTP response from recipient server&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Complete Email Journey
&lt;/h2&gt;

&lt;p&gt;Let's trace a single email through the entire system with precise timings.&lt;/p&gt;

&lt;h3&gt;
  
  
  T+0ms: Application Sends Email
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;From&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;To&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.23&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt; SMTP connection to Postfix relay&lt;/p&gt;




&lt;h3&gt;
  
  
  T+10ms: Postfix Validates &amp;amp; Queues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Checks performed:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Is sender in whitelist? (&lt;code&gt;info@example.com&lt;/code&gt; → OK)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is sender from allowed network? (&lt;code&gt;10.0.0.0/16&lt;/code&gt; → OK)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Queue email for delivery&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Log entries:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postfix/smtpd[123]: connect from ip-10-10-3-125
postfix/smtpd[123]: ABC123: client=ip-10-0-0-125
postfix/cleanup[124]: ABC123: message-id=&amp;lt;...&amp;gt;
postfix/qmgr[125]: ABC123: from=&amp;lt;info@example.com&amp;gt;, size=432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  T+150ms: Postfix → SES Relay
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Establish TLS 1.3 connection to SES&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authenticate with SMTP credentials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transmit email content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Receive confirmation&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Log entry (FIRST "sent" status!):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postfix/smtp[126]: Trusted TLS connection established to 
  email-smtp.ap-south-1.amazonaws.com:587: 
  TLSv1.3 with cipher TLS_AES_256_GCM_SHA384

postfix/smtp[126]: ABC123: to=&amp;lt;user@example.com&amp;gt;, 
  relay=email-smtp.ap-south-1.amazonaws.com:587, 
  delay=0.14, 
  status=sent (250 Ok 0109019c...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you know at this point:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Email left your infrastructure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SES accepted the email&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Total time: 150ms&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  T+1500ms: SES Processes &amp;amp; Delivers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SES internal process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add DKIM signature&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Perform SPF/DMARC checks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select optimal sending IP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect to the recipient's mail server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deliver email&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Receive final confirmation&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This happens entirely within AWS&lt;/strong&gt; - you don't see these steps&lt;/p&gt;




&lt;h3&gt;
  
  
  T+1550ms: SES Publishes Event to SNS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Event generated:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Delivery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mail"&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="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:05:15.140Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0109019c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"destination"&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="s2"&gt;"user@example.com"&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="nl"&gt;"delivery"&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="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:05:18.698Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"recipients"&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="s2"&gt;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"smtpResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"250 ok dirdel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"processingTimeMillis"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3558&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"remoteMtaIp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"74.198.68.21"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reportingMTA"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a8-123.smtp-out.amazonses.com"&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;p&gt;&lt;strong&gt;Published to SNS topic:&lt;/strong&gt; &lt;code&gt;ses-events-topic&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  T+1600ms: SNS → SQS Forward
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SNS wraps the event:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"MessageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"TopicArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:ap-south-1:...:ses-events-topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;notificationType&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Delivery&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:05:18.750Z"&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;p&gt;&lt;strong&gt;Delivered to SQS queue:&lt;/strong&gt; &lt;code&gt;ses-events-queue&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  T+5000ms: Logger Polls &amp;amp; Processes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Logger wakes up&lt;/strong&gt; (polls every 20 seconds with long polling)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Retrieve message from SQS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parse SNS wrapper&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Extract SES event&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Format for syslog&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write to log file&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delete message from queue&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Log entry (SECOND "delivered" status!):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feb 25 00:05:19 mail postfix/ses-events[456]: 0109019c...: 
  to=&amp;lt;user@example.com&amp;gt;, 
  relay=amazonses.com, 
  dsn=2.0.0, 
  status=delivered, 
  delay=3558ms,
  response=(250 ok dirdel)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you know now:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Email delivered to inbox&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Delivery took 3.5 seconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final confirmation from Gmail&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Final Result: Unified Logs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Complete journey in logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# T+150ms - Postfix → SES
Feb 25 00:05:15 mail postfix/smtp[126]: ABC123: 
  to=&amp;lt;user@example.com&amp;gt;, 
  status=sent (250 Ok)

# T+5000ms - SES → Inbox confirmed
Feb 25 00:05:19 mail postfix/ses-events[456]: 0109019c...: 
  to=&amp;lt;user@example.com&amp;gt;, 
  status=delivered, 
  delay=3558ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Search for any email:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"user@example.com"&lt;/span&gt; /var/log/postfix/&lt;span class="k"&gt;*&lt;/span&gt;.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postfix.log:  status=sent (handed to SES)
mail.log:     status=delivered (reached inbox)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complete visibility from send to delivery!&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Private Subnet?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Security Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduced attack surface&lt;/strong&gt;: No public IP = can't be scanned&lt;br&gt;&lt;br&gt;
&lt;strong&gt;No direct internet access&lt;/strong&gt;: Blocks many attack vectors&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Network-level isolation&lt;/strong&gt;: Additional security layer&lt;br&gt;&lt;br&gt;
&lt;strong&gt;AWS best practice&lt;/strong&gt;: Recommended architecture&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Compliance-friendly&lt;/strong&gt;: Easier to meet security requirements&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────┐
│     Private Subnet             │
│                                 │
│  ┌──────────┐                  │
│  │ Postfix  │ (No public IP)   │
│  └────┬─────┘                  │
│       │ Outbound HTTPS only    │
└───────┼─────────────────────────┘
        │
        ↓ Via NAT Gateway
  ┌─────────────┐
  │   AWS SES   │
  │   AWS SQS   │
  └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point:&lt;/strong&gt; Both SES and SQS are "pull" services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Postfix initiates connection to SES (outbound)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logger initiates connection to SQS (outbound)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No inbound connections needed!&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;SQS Polling Benefits:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SES → SNS → SQS ← Python polls (outbound only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Simple infrastructure:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No public endpoints&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No TLS cert management&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No inbound firewall rules&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IAM authentication (built-in)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic retry (queue buffering)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Security Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Network Security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Multi-layer defense:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│        Private Subnet               │
│  ┌──────────────────────────┐      │
│  │ Security Group           │      │
│  │ - Port 25: 10.0.0.0/21 │      │
│  │ - Port 22: Admin IPs     │      │
│  │ - Outbound: All          │      │
│  │                          │      │
│  │   ┌──────────┐          │      │
│  │   │ Postfix  │          │      │
│  │   │ (Private)│          │      │
│  │   └────┬─────┘          │      │
│  └────────┼─────────────────┘      │
└───────────┼─────────────────────────┘
            │ Outbound only
            ↓ (TLS 1.3)
      ┌─────────────┐
      │   AWS SES   │
      │  (Public)   │
      └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security controls:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Network isolation&lt;/strong&gt;: Private subnet, no public IP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sender validation&lt;/strong&gt;: Whitelist checks before relay&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt;: TLS 1.3 to SES&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: SMTP credentials for SES&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWS Documentation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/" rel="noopener noreferrer"&gt;Amazon SES Developer Guide&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;Amazon SQS Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/sns/" rel="noopener noreferrer"&gt;Amazon SNS Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Postfix Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="http://www.postfix.org/documentation.html" rel="noopener noreferrer"&gt;Official Postfix Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Email Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://dmarc.org/" rel="noopener noreferrer"&gt;DMARC.org&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About This Series
&lt;/h2&gt;

&lt;p&gt;This is &lt;strong&gt;Part 1&lt;/strong&gt; of a 3-part series on building production email infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 1: Architecture &amp;amp; Design&lt;/strong&gt; ← You just read this&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 2: Implementation Guide&lt;/strong&gt; - Step-by-step setup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Part 3: Operations &amp;amp; Troubleshooting&lt;/strong&gt; - Day-to-day management&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Next in series:&lt;/em&gt; &lt;em&gt;Part 2: Implementation Guide&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;If this helped or resonated with you, connect with me on&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;. Let’s learn and grow together.&lt;/p&gt;

&lt;p&gt;👉 Stay tuned for more behind-the-scenes write-ups and system design breakdowns.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ses</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Run Docker Remotely with Portainer Instead of Docker Desktop</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Tue, 29 Jul 2025 04:41:00 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/run-docker-remotely-with-portainer-instead-of-docker-desktop-2o6b</link>
      <guid>https://dev.to/cyrilsebastian/run-docker-remotely-with-portainer-instead-of-docker-desktop-2o6b</guid>
      <description>&lt;p&gt;Managing Docker remotely, especially on a Linux server, is a powerful alternative to using Docker Desktop on your local machine. In this guide, we'll walk through how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host and manage Docker containers remotely.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Portainer&lt;/strong&gt; as a graphical interface to control Docker.&lt;/li&gt;
&lt;li&gt;Replace Docker Desktop with a more flexible and lightweight remote workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're a DevOps engineer, a cloud enthusiast, or simply exploring alternatives to Docker Desktop, this setup will enhance your productivity.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Why Portainer Instead of Docker Desktop?
&lt;/h2&gt;

&lt;p&gt;Docker Desktop is great, but it has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires a GUI and lots of system resources.&lt;/li&gt;
&lt;li&gt;Licensing costs for teams.&lt;/li&gt;
&lt;li&gt;Tied to your local machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Portainer&lt;/strong&gt; is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;li&gt;Web-based&lt;/li&gt;
&lt;li&gt;Platform-agnostic&lt;/li&gt;
&lt;li&gt;Perfect for headless servers and remote access.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🗂 Step-by-Step Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Connect to Your Server
&lt;/h3&gt;

&lt;p&gt;SSH into your remote server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh your-user@your-server-ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch to root if necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo -i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Create a Directory for Docker Configs
&lt;/h3&gt;

&lt;p&gt;Well organize Portainer and any other services under &lt;code&gt;/opt/docker&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;mkdir -p /opt/docker &amp;amp;&amp;amp; cd /opt/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Prepare &lt;code&gt;portainer.yml&lt;/code&gt; File
&lt;/h3&gt;

&lt;p&gt;Create a Docker Compose file for Portainer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vim portainer.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.8'

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    ports:
      - "9000:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data

volumes:
  portainer_data:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and exit (&lt;code&gt;Ctrl + O&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Ctrl + X&lt;/code&gt;).&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Start Portainer
&lt;/h3&gt;

&lt;p&gt;Now launch Portainer with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose -f portainer.yml up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Container Portainer started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To follow logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose -f portainer.yml logs -f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. Access Portainer in a Browser
&lt;/h3&gt;

&lt;p&gt;Open your browser and go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;:9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be prompted to set up an admin password and connect to your local Docker environment (it'll detect the Docker socket you mapped).&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Confirm It's Working
&lt;/h2&gt;

&lt;p&gt;To check if Portainer is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONTAINER ID IMAGE PORTS NAMESxxxxxxx portainer/portainer-ce 0.0.0.0:9000-&amp;gt;9000/tcp portainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also view logs anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker logs portainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Run Docker Remotely Like It's Local
&lt;/h3&gt;

&lt;p&gt;You can run any Docker CLI commands remotely, &lt;strong&gt;without directly accessing your lab machine&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Option 1: Set a Permanent Remote Docker Context
&lt;/h4&gt;

&lt;p&gt;Edit your &lt;code&gt;.zshrc&lt;/code&gt; file on your Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo 'export DOCKER_HOST=ssh://username@remote_ip' &amp;gt;&amp;gt; ~/.zshrc source ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can run &lt;strong&gt;any&lt;/strong&gt; Docker CLI command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker ps docker compose up -d docker logs &amp;lt;container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it'll execute on the remote machine automatically. No &lt;code&gt;ssh&lt;/code&gt;, no fuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You now have a clean and efficient way to manage Docker &lt;strong&gt;remotely&lt;/strong&gt; without relying on Docker Desktop. Using &lt;strong&gt;Portainer&lt;/strong&gt; , you gain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A lightweight web UI&lt;/li&gt;
&lt;li&gt;Easy container and image management&lt;/li&gt;
&lt;li&gt;Portability across systems&lt;/li&gt;
&lt;li&gt;Remote team-friendly access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is ideal for remote development, headless servers, or teams building CI/CD and DevOps pipelines.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>cloudcomputing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Self-Hosted Streaming with Jellyfin and ngrok – A Personal Weekend Project</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Thu, 10 Jul 2025 17:37:29 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/self-hosted-streaming-with-jellyfin-and-ngrok-a-personal-weekend-project-39o2</link>
      <guid>https://dev.to/cyrilsebastian/self-hosted-streaming-with-jellyfin-and-ngrok-a-personal-weekend-project-39o2</guid>
      <description>&lt;p&gt;It all started with a simple need: I wanted to watch some old movies and personal family videos stored on my desktop. My niece had also backed up a bunch of vacation clips from her phone onto my machine, and now that she's back home, I needed an easy way to send them back to her without juggling USB drives or cloud uploads.&lt;/p&gt;

&lt;p&gt;Being someone from a DevOps/SRE background, these small personal needs often spiral into fun infrastructure experiments. So, I figured, why not use this as a chance to self-host a media server?&lt;/p&gt;

&lt;p&gt;That's when I turned to &lt;a href="https://jellyfin.org/" rel="noopener noreferrer"&gt;Jellyfin&lt;/a&gt;, a free, open-source media streaming solution. Paired with &lt;code&gt;ngrok&lt;/code&gt;, I could securely share access with family and now had my very own private Netflix running directly from my Linux desktop. This was one of those side projects that started as a practical fix but turned into an unexpectedly enjoyable weekend build.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through exactly how I set it all up using Docker and ngrok, clean, simple, and DevOps-style.&lt;/p&gt;

&lt;h3&gt;
  
  
  📌 Why Jellyfin?
&lt;/h3&gt;

&lt;p&gt;As someone who values open-source tools and self-hosted alternatives, Jellyfin hits the sweet spot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fully open-source&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No telemetry or licensing headaches&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supports local media, subtitles, transcoding, and even user profiles&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had a folder full of personal and family videos collecting digital dust. Jellyfin gave them a Netflix-like interface without the surveillance.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 Step 1: Run Jellyfin in Docker
&lt;/h2&gt;

&lt;p&gt;To keep things clean and reproducible (DevOps mantra!), I went with Docker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d \ --name jellyfin \ -p 8096:8096 \ -v /home/user/Documents/p2p:/media \ jellyfin/jellyfin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; runs it in the background&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Port 8096 is Jellyfin's default web UI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; mount points my local media directory into the container at &lt;code&gt;/media&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the container spins up, hit &lt;a href="http://localhost:8096" rel="noopener noreferrer"&gt;&lt;code&gt;http://localhost:8096&lt;/code&gt;&lt;/a&gt; on your browser and follow the setup wizard. You'll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create an admin account&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add your media libraries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure transcoding and user access&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple and smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 Step 2: Access Jellyfin from Anywhere Using &lt;code&gt;ngrok&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Since I didn't want to mess with router port forwarding or dynamic DNS at home (and certainly not expose ports to the internet unsafely), &lt;code&gt;ngrok&lt;/code&gt; was the perfect plug-and-play solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install ngrok
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgztar -xvf ngrok-v3-stable-linux-amd64.tgzsudo mv ngrok /usr/local/binngrok version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need an &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok account&lt;/a&gt; to get an auth token. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok config add-authtoken &amp;lt;YOUR_AUTH_TOKEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a Tunnel for Port 8096
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 8096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boom! You'll get a public HTTPS URL &lt;a href="https://abc123.ngrok.io" rel="noopener noreferrer"&gt;&lt;code&gt;https://abc123.ngrok.io&lt;/code&gt;&lt;/a&gt; that tunnels securely to your local Jellyfin instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: Protect it with Basic Auth
&lt;/h3&gt;

&lt;p&gt;To prevent unauthorized access, use basic auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http -auth="username:password" 8096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace with your credentials. Now, even if someone stumbles upon the link, they'll need to authenticate first.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Alternate: Run ngrok in Docker
&lt;/h2&gt;

&lt;p&gt;If you like consistency (like me), you might prefer running &lt;code&gt;ngrok&lt;/code&gt; in a container too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -it \ -e NGROK_AUTHTOKEN=&amp;lt;YOUR_AUTH_TOKEN&amp;gt; \ ngrok/ngrok http 8096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional: include &lt;code&gt;-auth="username:password"&lt;/code&gt; in the command above if you want the same security via Docker.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 A Quick Word on Security
&lt;/h2&gt;

&lt;p&gt;This setup is intended for personal use. If you're planning to stream across multiple users or set up a family server, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Running Jellyfin behind a proper reverse proxy (like Nginx)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using a free domain with Let's Encrypt certs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Disabling public tunnels when not in use&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not exposing write access to the mounted media directory (read-only is safer)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Real-World Use Cases
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Personal Netflix Clone&lt;/strong&gt; Watch your ripped DVDs or archived home videos from anywhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test Media Playback Over Slow Networks&lt;/strong&gt; Useful if you're tuning transcoding profiles for a home server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Portable Demos&lt;/strong&gt; Great for showing media apps at meetups or events without deploying to a cloud server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Media Backup Viewer&lt;/strong&gt; Remote preview of a NAS or cold storage drive contents.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🧠 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;For anyone in DevOps, self-hosting isn't just about saving money. It's about owning your stack, learning through tinkering, and reusing familiar tools (like Docker, tunneling, and logs) in a low-stress, real-life scenario.&lt;/p&gt;

&lt;p&gt;This Jellyfin + ngrok combo was a less than an hour-long weekend project, but the satisfaction of seeing your own media beautifully indexed and remotely accessible is real.&lt;/p&gt;

&lt;p&gt;Give it a try. This might just become your favorite side gig for relaxing after a long sprint.&lt;/p&gt;




&lt;p&gt;If you found this helpful or tried something similar, I'd love to hear about your setup, tweaks, or war stories. Leave a comment on &lt;a href="https://hashnode.com/@cyrilsebastian" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt; or connect via &lt;a href="https://linkedin.com/in/yourprofile" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, I'm always happy to connect and geek out about self-hosting and home lab fun.&lt;/p&gt;

&lt;p&gt;Happy streaming! 🎥🍿&lt;/p&gt;




&lt;p&gt;ImageCredits: Photo by &lt;a href="https://unsplash.com/@popcornmatch?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Marques Kaspbrak&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/black-flat-screen-tv-turned-on-on-brown-wooden-tv-rack-n1amn-SHKzw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>jellyfin</category>
      <category>ngrok</category>
      <category>docker</category>
    </item>
    <item>
      <title>My learnings after migrating the infrastructure from GCP to AWS.</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Wed, 02 Jul 2025 13:57:41 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/my-learnings-after-migrating-the-infrastructure-from-gcp-to-aws-17k4</link>
      <guid>https://dev.to/cyrilsebastian/my-learnings-after-migrating-the-infrastructure-from-gcp-to-aws-17k4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g" class="crayons-story__hidden-navigation-link"&gt;Planning a Cloud Migration? 10 Lessons from Production Cutovers&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/cyrilsebastian" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F2190542%2F25259711-53e6-4b72-a4a6-9e60ecda484b.jpeg" alt="cyrilsebastian profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/cyrilsebastian" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cyril Sebastian
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cyril Sebastian
                
              
              &lt;div id="story-author-preview-content-2639158" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/cyrilsebastian" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F2190542%2F25259711-53e6-4b72-a4a6-9e60ecda484b.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cyril Sebastian&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 30 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g" id="article-link-2639158"&gt;
          Planning a Cloud Migration? 10 Lessons from Production Cutovers
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/awschallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;awschallenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>aws</category>
      <category>gcp</category>
      <category>devops</category>
      <category>awschallenge</category>
    </item>
    <item>
      <title>Planning a Cloud Migration? 10 Lessons from Production Cutovers</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Mon, 30 Jun 2025 10:51:50 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g</link>
      <guid>https://dev.to/cyrilsebastian/planning-a-cloud-migration-10-lessons-from-production-cutovers-285g</guid>
      <description>&lt;p&gt;Moving a production-grade application serving millions of daily users from one cloud provider to another is a high-stakes operation. After executing a complete &lt;a href="https://tech.cyrilsebastian.com/gcp-to-aws-migration-part-2-real-cutover-issues-and-recovery" rel="noopener noreferrer"&gt;GCP to AWS migration&lt;/a&gt;including 21TB of data, MongoDB replica sets, MySQL clusters, and Apache Solr search infrastructure, here are ten critical lessons that separate successful migrations from costly disasters. Connect with me on &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;LinkedIn for more&lt;/a&gt; real-world DevOps insights.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Hidden Dependencies Surface During Cutover Weekend
&lt;/h2&gt;

&lt;p&gt;Even though we had tools for the job, coordinating syncs across staging, pre-prod, and production environments required careful orchestration and monitoring. Your application architecture diagram rarely tells the complete story. During our migration, we discovered hardcoded GCP configurations buried deep in environment variables and application configs that weren't documented anywhere. Network monitoring weeks before cutover revealed missed communication patterns, including internal DNS dependencies that required AWS Route 53 private hosted zones to replicate GCP's automatic internal DNS resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Data Transfer Costs Are a Hidden Budget Killer
&lt;/h2&gt;

&lt;p&gt;Data Transfer Out from GCP was a hidden but substantial cost center; plan for this when budgeting. Our 21TB migration taught us that egress charges can dramatically exceed your initial estimates. We saved thousands of dollars by identifying GCS buckets containing temporary compliance logs with auto-expiry policies and choosing to let them expire in GCP rather than migrating unnecessary data. Plan for these costs early and audit your data to ensure migration is necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Database Replication Lag Is Your Biggest Enemy
&lt;/h2&gt;

&lt;p&gt;One of the major blockers encountered was replication lag during promotion drills, especially under active write-heavy workloads. Our MySQL master-slave setup experienced significant lag during peak traffic. The solution was implementing iptables-based rules at the OS level to block application write traffic, allowing replication to catch up safely before cutover. This gave us a clean buffer for promotion without the risk of in-flight transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Storage Behavior Differs Dramatically Between Clouds
&lt;/h2&gt;

&lt;p&gt;Lazy loading of EBS volumes made autoscaling unreliable for time-sensitive indexing. Our Apache Solr migration revealed that AWS EBS volumes suffer from lazy loading, meaning data wasn't instantly accessible upon instance boot-up. In GCP, persistent volumes are mounted seamlessly with boot disks, enabling autoscaling. In AWS, we had to abandon autoscaling for Solr and use scheduled start/stop scripts instead. Factor in rebuild times and whether AWS Fast Snapshot Restore fits your budget.&lt;/p&gt;

&lt;p&gt;The IOPS and throughput planning complexity were another major difference. GCP used to take care of IOPS and throughput by choosing SSD disks, and increasing the disk size if more IOPS and throughput are required. But in AWS, we had to plan accordingly as per the usage, especially for databases. EBS gp3 volumes require explicit IOPS and throughput provisioning separate from storage capacity, meaning our database performance tuning became a multi-dimensional optimization problem rather than GCP's simpler disk-size-based scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Load Balancer Architectures Require Complete Rethinking
&lt;/h2&gt;

&lt;p&gt;The differences between GCP's global HTTPS Load Balancer and AWS Application Load Balancer go beyond simple configuration. GCP's URL maps allowed expressive, path-based routing across services. AWS required translating these into listener rules and target groups, often resulting in more granular configurations. We moved from static public IPs to CNAME-based routing, requiring DNS strategy adjustments and SSL certificate management changes through AWS Certificate Manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Security Models Force Architectural Changes
&lt;/h2&gt;

&lt;p&gt;All EC2 instances (except load balancers) were placed in private subnets for enhanced security. We had to implement bastion host access, update CORS headers for CloudFront integration, and create explicit firewall rules using iptables to control MySQL access during migration. AWS's security group model required translating GCP firewall rules while adding WAF integration for DDoS protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Network Restrictions Create Unexpected Blockers
&lt;/h2&gt;

&lt;p&gt;AWS restricts outbound SMTP traffic on port 25 by default to prevent abuse. This is not the case in GCP, so ensure to factor this into your cutover timeline if you're migrating mail servers. Our Postfix mail servers required explicit AWS Support requests to open port 25, adding weeks to our timeline.&lt;/p&gt;

&lt;p&gt;Beyond port restrictions, we discovered that our maximum utilized servers with low configurations couldn't be placed on burstable VM instances like t3 due to CPU credit limitations and network throttling. High-traffic applications that ran smoothly on GCP's custom CPU/RAM configurations suffered performance degradation when mapped to AWS burstable instances. We had to carefully analyze baseline vs. burst performance patterns and move critical workloads to dedicated instance types like m6i or c6i to avoid throttling during peak loads.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rollback Plans Must Account for Cloud-Specific Behaviors
&lt;/h2&gt;

&lt;p&gt;We implemented iptables-based rules at the OS level to block application write traffic, allowing replication to catch up safely before cutover. Our rollback strategy included controlled write freezes and hybrid MongoDB nodes that acted as bridges between cloud environments. The key was testing promotion and demotion scenarios multiple times, not just hoping data backups would suffice.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Monitoring Blind Spots Emerge in Cloud Transitions
&lt;/h2&gt;

&lt;p&gt;We experienced monitoring gaps during the most critical phases when existing tools didn't translate to the new environment. Setting up CloudWatch, maintaining Nagios compatibility, and ensuring Grafana dashboards worked across both environments simultaneously was crucial. Establish baseline performance metrics in both clouds and create real-time visibility before cutover day.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Post-Migration Optimization Is Where Real Value Emerges
&lt;/h2&gt;

&lt;p&gt;This migration tested our nerves and processes, but ultimately, it left us with better observability, tighter security, and an infrastructure we could proudly call production-grade. After successful cutover, we rightsized EC2 instances using historical metrics, implemented Savings Plans for reserved workloads, and enabled S3 lifecycle policies. The migration wasn't just about changing providers, it forced us to modernize our entire infrastructure approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality of Production Cutovers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tech.cyrilsebastian.com/gcp-to-aws-migration-part-1-architecture-data-transfer-and-infrastructure-setup" rel="noopener noreferrer"&gt;No plan survives contact without flexibility&lt;/a&gt;. Our experience proved that even with meticulous planning, live production environments will surprise you. We faced MySQL promotion lags, discovered hardcoded configurations requiring emergency patches, and dealt with Solr performance issues under load. The key was having a flexible team ready to improvise while maintaining strict rollback readiness.&lt;/p&gt;

&lt;p&gt;The most important lesson? Migration is more than lift-and-shift; it's evolve or expire. Successful cloud migration requires embracing architectural differences rather than fighting them. Plan thoroughly, test relentlessly, and be prepared to adapt quickly when reality differs from your runbook.&lt;/p&gt;

&lt;p&gt;Your cloud migration is an opportunity to build better infrastructure, not just move existing problems to a new provider. Approach it as a chance to evolve your entire operational model, and you'll emerge stronger on the other side.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>gcp</category>
      <category>devops</category>
      <category>awschallenge</category>
    </item>
    <item>
      <title>In Part 2 of my GCP to AWS migration blog series, I’m sharing the _unfiltered reality_—the hiccups, missed assumptions, and how we stabilized production in real time.</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Mon, 09 Jun 2025 08:25:17 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/in-part-2-of-my-gcp-to-aws-migration-blog-series-im-sharing-the-unfiltered-reality-the-hiccups-2cd8</link>
      <guid>https://dev.to/cyrilsebastian/in-part-2-of-my-gcp-to-aws-migration-blog-series-im-sharing-the-unfiltered-reality-the-hiccups-2cd8</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l" class="crayons-story__hidden-navigation-link"&gt;GCP to AWS Migration – Part 2: Real Cutover, Issues &amp;amp; Recovery&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/cyrilsebastian" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F2190542%2F25259711-53e6-4b72-a4a6-9e60ecda484b.jpeg" alt="cyrilsebastian profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/cyrilsebastian" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Cyril Sebastian
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Cyril Sebastian
                
              
              &lt;div id="story-author-preview-content-2564936" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/cyrilsebastian" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F2190542%2F25259711-53e6-4b72-a4a6-9e60ecda484b.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Cyril Sebastian&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 5 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l" id="article-link-2564936"&gt;
          GCP to AWS Migration – Part 2: Real Cutover, Issues &amp;amp; Recovery
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/awschallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;awschallenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>aws</category>
      <category>gcp</category>
      <category>devops</category>
      <category>awschallenge</category>
    </item>
    <item>
      <title>Migrated from #GCP to #AWS recently and wrote about it. It's battle-tested architecture + transfer optimizations. Part 1: https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-1-architecture-data-transfer-infrastructure-setup-oi7</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Fri, 06 Jun 2025 05:18:58 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/migrated-from-gcp-to-aws-recently-and-wrote-about-it-its-battle-tested-architecture--4b9l</link>
      <guid>https://dev.to/cyrilsebastian/migrated-from-gcp-to-aws-recently-and-wrote-about-it-its-battle-tested-architecture--4b9l</guid>
      <description></description>
    </item>
    <item>
      <title>GCP to AWS Migration – Part 2: Real Cutover, Issues &amp; Recovery</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Thu, 05 Jun 2025 08:28:12 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l</link>
      <guid>https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-2-real-cutover-issues-recovery-e5l</guid>
      <description>&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;Start of Part 2: The Real Cutover &amp;amp; Beyond&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://tech.cyrilsebastian.com/gcp-to-aws-migration-part-1-architecture-data-transfer-and-infrastructure-setup" rel="noopener noreferrer"&gt;&lt;strong&gt;While Part 1 laid the architectural and data groundwork&lt;/strong&gt;&lt;/a&gt;, Part 2 is where the real-world complexity kicked in.&lt;/p&gt;

&lt;p&gt;We faced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Database promotions that didnt go as rehearsed,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lazy-loaded Solr indexes fighting with EBS latency,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hardcoded GCP configs in the dark corners of our stack,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And the high-stakes pressure of a real-time production cutover.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Part 1 was planning and theory, &lt;strong&gt;Part 2 was execution and improvisation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lets dive into the live switch, the challenges we didnt see coming, and how we turned them into lessons and long-term wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Phase 4: Application &amp;amp; Infrastructure Layer Adaptation
&lt;/h2&gt;

&lt;p&gt;As part of the migration, significant adjustments were required in both the application configuration and infrastructure setup to align with AWS's architecture and security practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Changes &amp;amp; Adaptations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Private Networking &amp;amp; Bastion Access&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CORS &amp;amp; S3 Policy Updates&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application Configuration Updates&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal DNS Transition&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Static Asset Delivery via CloudFront&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security Hardening with WAF&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Firewall Rules: Securing AWS MySQL from Legacy Sources
&lt;/h3&gt;

&lt;p&gt;To ensure &lt;strong&gt;controlled access to the new MySQL server in AWS&lt;/strong&gt; , we hardened the instance using explicit &lt;code&gt;iptables&lt;/code&gt; rules. These rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blocked direct MySQL access&lt;/strong&gt; from legacy or untrusted subnets (e.g., GCP App subnets)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allowed SSH access&lt;/strong&gt; only from trusted bastion/admin IPs during the migration window&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FIREWALL RULES FLOW:

[Blocked Sources] ──❌──┐
                        │
10.AAA.0.0/22     ──────┤
10.BBB.248.0/21   ──────┤
                        ├─── DROP:3306 ───┐
                        │                 │
[Allowed Sources] ──✅──┤                 ▼
                        │         ┌─────────────────┐
10.AAA.0.4/32     ──────┤         │ 10.BBB.CCC.223  │
10.BBB.248.158/32 ──────┤         │  MySQL Server   │
10.BBB.251.107/32 ──────┼─ACCEPT──│     (AWS)       │
10.BBB.253.9/32   ──────┤  :22    │                 │
                        │         └─────────────────┘

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Legend:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://10.AAA" rel="noopener noreferrer"&gt;&lt;code&gt;10.AAA&lt;/code&gt;&lt;/a&gt;&lt;code&gt;.x.x&lt;/code&gt; = Source network (GCP)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;10.BBB.CCC.223&lt;/code&gt; = Target MySQL server in AWS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IPs like &lt;code&gt;10.BBB.248.158&lt;/code&gt; = Bastion or trusted admin IPs allowed for SSH&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;This rule-based approach gave us an &lt;strong&gt;extra layer of protection&lt;/strong&gt; beyond AWS Security Groups during the critical migration phase.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🌐 Load Balancer Differences: GCP vs AWS
&lt;/h2&gt;

&lt;p&gt;During the migration, we encountered significant differences in how load balancing is handled between GCP and AWS. This required architectural adjustments and deeper routing, SSL, and compute scaling planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  📊 Comparison Overview
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GCP HTTPS Load Balancer&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;AWS Application Load Balancer (ALB)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global by default&lt;/td&gt;
&lt;td&gt;Regional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TLS/SSL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wildcard SSL was uploaded to GCP&lt;/td&gt;
&lt;td&gt;Managed manually via AWS Certificate Manager (ACM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;URL Maps&lt;/td&gt;
&lt;td&gt;Target Groups with Listener Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static Public IP&lt;/td&gt;
&lt;td&gt;CNAME with DNS-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global Load Balancer → MIG (Managed Instance Groups)&lt;/td&gt;
&lt;td&gt;ALB → Target Group → ASG (Auto Scaling Group)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  🧩 Key Migration Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Static IP vs DNS Routing&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Routing Mechanism Differences&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSL/TLS Certificates&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application-Specific Custom Rules&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📮 Special Case: Postfix &amp;amp; Port 25 Restrictions
&lt;/h3&gt;

&lt;p&gt;To migrate our &lt;strong&gt;Postfix mail servers&lt;/strong&gt; that use port 25 for SMTP, we had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Submit an &lt;strong&gt;explicit request to AWS Support&lt;/strong&gt; for &lt;strong&gt;port 25 to be opened&lt;/strong&gt; (outbound) on our AWS account in the specific region.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This was a prerequisite for creating a &lt;strong&gt;Network Load Balancer (NLB)&lt;/strong&gt; that could pass traffic directly to the Postfix instances.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : AWS restricts outbound SMTP traffic on port 25 by default to prevent abuse. This is not the case in GCP, so ensure to factor this into your cutover timeline if you're migrating mail servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 Phase 5: Apache Solr Migration
&lt;/h2&gt;

&lt;p&gt;Apache Solr powered our platform's search functionality with complex indexing and fast response times. Migrating it to AWS introduced both architectural and operational complexities.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛠 Migration Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AMI Creation Was Non-Trivial&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
We created custom AMIs for Solr nodes with large EBS volumes. However, this surfaced two key challenges:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No AWS FSR&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
AWS &lt;strong&gt;Fast Snapshot Restore (FSR)&lt;/strong&gt; could have helpedbut was ruled out due to budget constraints. Without FSR, we observed delayed volume readiness post-launch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Index Rebuild from Source DB&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
Post-migration, we &lt;strong&gt;rebuilt Solr indexes&lt;/strong&gt; from source data stored in MongoDB and MySQL, ensuring consistency and avoiding partial data issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Master-Slave Architecture&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
We finalized a &lt;strong&gt;standalone Solr master-slave setup&lt;/strong&gt; on EC2 after a dedicated PoC. This provided better control compared to GCP's managed instance groups.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🏗 GCP vs AWS Deployment Model
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GCP MIGs&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;AWS EC2 Standalone&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Solr slaves ran in &lt;strong&gt;Managed Instance Groups&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Solr nodes deployed on &lt;strong&gt;standalone EC2s&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume Attachment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Persistent volumes mounted with boot disk&lt;/td&gt;
&lt;td&gt;EBS volumes suffered from &lt;strong&gt;lazy loading&lt;/strong&gt;, slowing boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autoscaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully autoscaled Solr slaves based on demand&lt;/td&gt;
&lt;td&gt;Autoscaling impractical due to volume readiness delays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On-demand scaling saved costs&lt;/td&gt;
&lt;td&gt;Used &lt;strong&gt;EC2 scheduling&lt;/strong&gt; (shutdown/startup) to control spend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ⚡ Operational Decision: No Autoscaling for Solr in AWS
&lt;/h3&gt;

&lt;p&gt;In GCP, autoscaling Solr slaves was seamless—new instances booted with attached volumes and joined the cluster dynamically.&lt;/p&gt;

&lt;p&gt;However, in AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lazy loading of EBS volumes&lt;/strong&gt; made autoscaling &lt;strong&gt;unreliable for time-sensitive indexing&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kept EC2 nodes in a &lt;strong&gt;fixed topology&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Used &lt;strong&gt;scheduled start/stop scripts&lt;/strong&gt; (via cron) to manage uptime during peak/off-peak hours&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;p&gt;Solr migrations need deep consideration of disk behavior in AWS. If you're not using FSR, &lt;strong&gt;do not assume volume availability equals data availability&lt;/strong&gt;. Factor in rebuild times, cost impact, and whether autoscaling truly benefits your workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛑 The Cutover Weekend
&lt;/h2&gt;

&lt;p&gt;We declared a &lt;strong&gt;deployment freeze 7 days before&lt;/strong&gt; the migration to maintain stability and reduce last-minute surprises.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Cutover Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TTL reduced to &lt;strong&gt;60 seconds&lt;/strong&gt; to allow quick DNS propagation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final &lt;strong&gt;S3 and database sync&lt;/strong&gt; performed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Checksums validated&lt;/strong&gt; for critical data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Route 53&lt;/strong&gt; routing policies configured to mimic GCPs internal DNS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CloudWatch&lt;/strong&gt; , Nagios, and Grafana set up for monitoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final &lt;strong&gt;fallback snapshot&lt;/strong&gt; captured.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A comprehensive &lt;strong&gt;cutover runbook&lt;/strong&gt; was prepared with clear task ownership and escalation paths.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🕒 Cutover Timeline
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time Slot&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hour 1&lt;/td&gt;
&lt;td&gt;Final S3 + DB sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour 2–3&lt;/td&gt;
&lt;td&gt;DB failover and validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour 4&lt;/td&gt;
&lt;td&gt;DNS switch from GCP to Route 53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour 5–6&lt;/td&gt;
&lt;td&gt;Traffic validation + rollback readiness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  😮 Unexpected Issues (and What We Did)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MySQL master switch had lag&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Improved replica promotion playbook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hardcoded GCP configs found&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Emergency patching of ENV &amp;amp; redeploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solr slow to boot under load&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporarily pre-warmed EC2 nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🚀 Post-Migration Optimizations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rightsized EC2 instances&lt;/strong&gt; using historical metrics&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Committed to &lt;strong&gt;Savings Plans&lt;/strong&gt; for reserved workloads&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enabled and tuned &lt;strong&gt;S3 lifecycle policies&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set up &lt;strong&gt;automated AMI rotations&lt;/strong&gt; and &lt;strong&gt;DB snapshots&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 &lt;strong&gt;End of Part 2: Final Thoughts &amp;amp; Whats Next&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This journey from GCP to AWS wasnt just about swapping clouds, it was a &lt;strong&gt;masterclass in operational resilience&lt;/strong&gt; , &lt;strong&gt;cross-team coordination&lt;/strong&gt; , and &lt;strong&gt;cloud-native rethinking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We learned that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No plan survives contact without flexibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Owning your infrastructure also means owning your edge cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Migration is more than lift-and-shift, it's evolve or expire.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Smooth seas never made a skilled sailor.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Franklin D. Roosevelt&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This migration tested our nerves and processes, but ultimately, it left us with better observability, tighter security, and an infrastructure we could proudly call production-grade.&lt;/p&gt;




&lt;p&gt;🔗 &lt;strong&gt;If this helped or resonated with you, connect with me on&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt;. Let's learn and grow together.&lt;/p&gt;

&lt;p&gt;👉 Stay tuned for more behind-the-scenes write-ups and system design breakdowns.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>gcp</category>
      <category>devops</category>
      <category>awschallenge</category>
    </item>
    <item>
      <title>GCP to AWS Migration – Part 1: Architecture, Data Transfer &amp; Infrastructure Setup</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Tue, 03 Jun 2025 19:16:24 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-1-architecture-data-transfer-infrastructure-setup-oi7</link>
      <guid>https://dev.to/cyrilsebastian/gcp-to-aws-migration-part-1-architecture-data-transfer-infrastructure-setup-oi7</guid>
      <description>&lt;p&gt;Migrating a production-grade, high-traffic application from Google Cloud to AWS is no small feat. Here's how our DevOps and engineering teams executed.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧭 Why We Migrated: Business Drivers Behind the Move
&lt;/h2&gt;

&lt;p&gt;Our platform, serving millions of daily users, was running smoothly on GCP. However, evolving business goals, pricing considerations, and long-term cloud ecosystem alignment led us to migrate to AWS.&lt;/p&gt;

&lt;p&gt;Key components of our GCP-based stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web Tier&lt;/strong&gt; : Next.js frontend + Django backend&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Databases&lt;/strong&gt; : MongoDB replica sets, MySQL clusters&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asynchronous Services&lt;/strong&gt; : Redis, RabbitMQ&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; : Apache Solr for full-text search&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; : GCP Compute Engine VMs, managed instance groups, HTTPS Load Balancer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt; : 21 TB of data in Google Cloud Storage (GCS)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📋 Step 0: Creating a Migration Runbook
&lt;/h2&gt;

&lt;p&gt;We treated this as a mission-critical project. Our runbook included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stakeholders&lt;/strong&gt; : CTO, DevOps Lead, Database Architect, Application Owners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timeline&lt;/strong&gt; : 8 weeks from planning to cutover&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Phases&lt;/strong&gt; : Network Setup Data Migration Database Sync Application Migration Cutover&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rollback Plan&lt;/strong&gt; : Prepared and rehearsed with timelines for failback&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠 Infrastructure Mapping: GCP vs AWS
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenges in Mapping:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GCP allows &lt;em&gt;custom CPU and RAM&lt;/em&gt;, AWS uses fixed instance types (t3, m6i, r6g, etc)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IOPS differences between GCP SSDs and AWS EBS gp3 required tuning&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost model varies significantly (especially egress from GCP)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We used &lt;a href="https://calculator.aws.amazon.com/" rel="noopener noreferrer"&gt;AWS Pricing Calculator&lt;/a&gt; and &lt;a href="https://cloud.google.com/products/calculator" rel="noopener noreferrer"&gt;GCP Pricing Calculator&lt;/a&gt; to simulate monthly billing and select cost-optimized instance types.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 Phase 1: AWS Network Infrastructure Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;AWS Network Infrastructure (ap-south-1)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            ┌──────────────────────┐
                            │    GCP / DC VPC      │
                            └────────┬─────────────┘
                                     │
                           ┌─────────▼─────────┐
                           │ Site-to-Site VPN  │
                           └─────────┬─────────┘
                                     │
                           ┌─────────▼─────────┐
                           │      VPC          │
                           │  (ap-south-1)     │
                           └─────────┬─────────┘
                                     │
          ┌──────────────────────────┼─────────────────────────────┐
          │                          │                             │
┌─────────▼─────────┐     ┌──────────▼──────────┐       ┌──────────▼──────────┐
│ Public Subnet AZ1 │     │ Public Subnet AZ2   │       │ Public Subnet AZ3   │
│ - Bastion Host    │     │ - NAT Gateway       │       │ - Internet Gateway  │
└─────────┬─────────┘     └──────────┬──────────┘       └──────────┬──────────┘
          │                          │                             │
          ▼                          ▼                             ▼
┌────────────────┐        ┌────────────────┐              ┌────────────────┐
│Private Subnet 1│        │Private Subnet 2│              │Private Subnet 3│
│App / DB Tier   │        │App / DB Tier   │              │App / DB Tier   │
└────────────────┘        └────────────────┘              └────────────────┘

                 Security Groups + NACLs as per GCP mapping
                 VPC Flow Logs → CloudWatch Logs

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧩 Components Breakdown
&lt;/h3&gt;

&lt;p&gt;| Component | Purpose |&lt;br&gt;
| &lt;strong&gt;3 AZs&lt;/strong&gt; | High availability and fault tolerance |&lt;br&gt;
| &lt;strong&gt;Public Subnets&lt;/strong&gt; | Bastion, NAT, IGW for ingress/egress |&lt;br&gt;
| &lt;strong&gt;Private Subnets&lt;/strong&gt; | Isolated app and DB tiers |&lt;br&gt;
| &lt;strong&gt;VPN&lt;/strong&gt; | Secure hybrid GCPAWS connectivity |&lt;br&gt;
| &lt;strong&gt;Security&lt;/strong&gt; | Security Groups + NACLs derived from GCP firewall rules |&lt;br&gt;
| &lt;strong&gt;Monitoring&lt;/strong&gt; | VPC Flow Logs + CloudWatch Metrics for visibility |&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Phase 2: Data Migration (GCS S3)
&lt;/h2&gt;

&lt;p&gt;We migrated over &lt;strong&gt;21 TB of user-generated and application asset data&lt;/strong&gt; from Google Cloud Storage (GCS) to Amazon S3. Given the scale, this phase required surgical precision in planning, execution, and cost control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools &amp;amp; Techniques Used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/datasync/latest/userguide/what-is-datasync.html" rel="noopener noreferrer"&gt;&lt;strong&gt;AWS DataSync&lt;/strong&gt;&lt;/a&gt;:&lt;br&gt;&lt;br&gt;
Chosen for its efficiency, security, and ability to handle large-scale object transfers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Service Account HMAC Credentials&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
Used for secure bucket-to-bucket authentication between GCP and AWS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Phased Sync Strategy&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: We carefully validated &lt;strong&gt;checksums and object counts&lt;/strong&gt; after each sync phase to ensure data integrity and avoid overwriting unchanged files.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  💡 Smart Optimization Decisions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Selective Data Migration&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delta Awareness&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Post-Migration S3 Tuning
&lt;/h3&gt;

&lt;p&gt;After the bulk migration was completed, we fine-tuned our S3 environment for &lt;strong&gt;cost optimization, data hygiene&lt;/strong&gt; , and &lt;strong&gt;long-term sustainability&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lifecycle Policies Implemented&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic archival of infrequently accessed data to &lt;strong&gt;S3 Glacier&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Expiry rules for:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configured &lt;strong&gt;S3 Incomplete Multipart Upload Aborts&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Volume Data Complexity&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
Even though we had tools for the job, coordinating syncs across staging, pre-prod, and production environments required careful orchestration and monitoring.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Egress and DTO Costs&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
Data Transfer Out from GCP was a &lt;strong&gt;hidden but substantial cost center&lt;/strong&gt; plan ahead for this when budgeting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;S3 Behavior Is Not GCS&lt;/strong&gt; :&lt;br&gt;&lt;br&gt;
We had to adjust application logic and IAM policies post-migration to align with &lt;strong&gt;S3 object handling, access policies, and permissions model&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🗄 Phase 3: Database Migration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;MongoDB Migration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Migrating MongoDB from GCP to AWS was one of the most sensitive components of the move due to its role in powering real-time operations and user sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Strategy&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replica Set Initialization&lt;/strong&gt; : Set up MongoDB replica sets on AWS EC2 instances to mirror the topology running in GCP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Oplog-Based Sync&lt;/strong&gt; : Enabled &lt;strong&gt;oplog-based replication&lt;/strong&gt; between AWS and GCP MongoDB nodes to ensure near real-time data synchronization without full data dumps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hybrid Node Integration&lt;/strong&gt; : Deployed a &lt;strong&gt;MongoDB node in AWS&lt;/strong&gt; , directly connected to the &lt;strong&gt;GCP replica set&lt;/strong&gt; , acting as a bridge before full cutover.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;iptables for Controlled Access&lt;/strong&gt; : Used &lt;strong&gt;iptables rules&lt;/strong&gt; to restrict write access during the sync period. This allowed &lt;strong&gt;inter-DB synchronization traffic only&lt;/strong&gt; , blocking application-level writes and ensuring data consistency before switchover.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failover Testing&lt;/strong&gt; : Conducted multiple &lt;strong&gt;failover and promotion drills&lt;/strong&gt; to validate readiness, with rollback plans in place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt; : Setting up a hybrid node and controlling access at the OS level allowed us to minimize data drift and test production-grade failovers without service disruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;MySQL Migration&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The MySQL component required careful orchestration to ensure transactional consistency and minimal downtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Approach&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Master-Slave Topology&lt;/strong&gt; : Established a classic &lt;strong&gt;master-slave setup&lt;/strong&gt; on AWS EC2 instances to replicate data from the GCP-hosted MySQL master.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replication Lag Challenges&lt;/strong&gt; : One of the &lt;strong&gt;major blockers&lt;/strong&gt; encountered was &lt;strong&gt;replication lag&lt;/strong&gt; during promotion drills, especially under active write-heavy workloads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Controlled Write Freeze&lt;/strong&gt; : We implemented &lt;strong&gt;iptables-based rules&lt;/strong&gt; at the OS level to &lt;strong&gt;block application write traffic&lt;/strong&gt; , allowing replication to catch up safely before cutover.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Promotion Strategy&lt;/strong&gt; :&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt; : Blocking writes via iptables provided a clean buffer for promotion without the risk of in-flight transactions, making the cutover smooth and predictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;End of Part 1: Setting the Stage for Migration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Youve seen how we architected an AWS environment from scratch, replicated critical systems like MongoDB and MySQL, and seamlessly migrated over &lt;strong&gt;21 TB&lt;/strong&gt; of assets from GCP to S3all while optimizing for cost, security, and scalability.&lt;/p&gt;

&lt;p&gt;But this was just the calm before the storm.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Give me six hours to chop down a tree and I will spend the first four sharpening the axe."&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Abraham Lincoln&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We were well-prepared. But would the systemsand the teamhold up during &lt;strong&gt;live cutover&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2: The Real Cutover &amp;amp; Beyond&lt;/strong&gt; , well step into the fire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What went wrong,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What we had to patch live,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And what we did to walk away from it stronger.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Don't miss it. &lt;strong&gt;Follow me on&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/sebastiancyril/" rel="noopener noreferrer"&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/a&gt; for more deep-dive case studies and real-world DevOps/CloudOps stories like this.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>gcp</category>
      <category>awschallenge</category>
      <category>devops</category>
    </item>
    <item>
      <title>Akamai Staging vs. Production: How to Set Up Environments Efficiently</title>
      <dc:creator>Cyril Sebastian</dc:creator>
      <pubDate>Thu, 26 Dec 2024 12:39:14 +0000</pubDate>
      <link>https://dev.to/cyrilsebastian/akamai-staging-vs-production-how-to-set-up-environments-efficiently-39e1</link>
      <guid>https://dev.to/cyrilsebastian/akamai-staging-vs-production-how-to-set-up-environments-efficiently-39e1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Content Delivery Networks (CDNs)&lt;/strong&gt; have become indispensable in modern web applications to ensure faster load times, enhanced security, and improved global performance. Among the leading CDN providers, &lt;strong&gt;Akamai&lt;/strong&gt; stands out with its robust platform and mature features.&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%2Fzuw5de5nn9w6018nbrth.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%2Fzuw5de5nn9w6018nbrth.png" alt="Image description" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article covers the basics of testing rules in a staging environment and deploying changes to the production environment after setting up rules in Akamai properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use a CDN?
&lt;/h2&gt;

&lt;p&gt;A CDN is a distributed network of servers that caches content closer to the end users. Here are the key reasons to use a CDN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced Latency&lt;/strong&gt; : By serving requests from the nearest edge servers, CDNs significantly decrease load times.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt; : Seamlessly handles high traffic during peak usage by distributing the load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enhanced Security&lt;/strong&gt; : Protects against DDoS attacks, provides Web Application Firewalls (WAF), and mitigates other security threats.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost Optimization&lt;/strong&gt; : Reduces the burden on origin servers by caching static assets and efficiently managing traffic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Practices for Routing Requests in Akamai.
&lt;/h2&gt;

&lt;p&gt;When configuring Akamai, follow these best practices for optimal results:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Appropriate Cache Keys&lt;/strong&gt; : Ensure caching is configured for specific query strings, cookies, or headers to serve accurate content.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Implement Cache Invalidation Policies&lt;/strong&gt; : Use Akamais Content Purge to remove outdated content.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; akamai purge &lt;span class="nt"&gt;--edgerc&lt;/span&gt; ~/.edgerc &lt;span class="nt"&gt;--section&lt;/span&gt; default invalidate &lt;span class="s2"&gt;"www.example.com/path1"&lt;/span&gt; &lt;span class="s2"&gt;"www.example.com/path2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable Gzip or Brotli Compression&lt;/strong&gt; : Compress text-based assets (e.g., HTML, CSS, JS) to reduce bandwidth usage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strategic Traffic Routing&lt;/strong&gt; : Utilize geo-blocking and geo-routing to enhance performance and minimize latency by routing users to the nearest server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor Cache Hit/Miss Ratios&lt;/strong&gt; : Use the Akamais dashboard or logs to analyze and improve caching efficiency.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setting Up Akamai Staging and Production Environments
&lt;/h2&gt;

&lt;p&gt;Akamai allows testing configurations in a staging environment before deploying them live. Here's how to set it up:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Staging Environment Setup&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The staging environment mimics production, ensuring safe testing. Follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Determine the Staging URL&lt;/strong&gt; : Add the Akamai -staging suffix to your Edge Hostname:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;example.com.edgesuite-staging.net
example.com.edgekey-staging.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Perform DNS Lookup&lt;/strong&gt; : Use &lt;code&gt;dig&lt;/code&gt; commands to resolve IPv4 and IPv6 addresses from staging Edge Hostname:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short example.com.edgesuite-staging.net  
dig +short example.com.edgesuite-staging.net AAAA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt; &lt;code&gt;/etc/hosts&lt;/code&gt;: Map the resolved IP address to the staging URL. Prefer IPv6 if IPv4 causes issues:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;2600:XXXX:X:X::XXXX:XXXX example.com.edgesuite-staging.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test Staging with&lt;/strong&gt; &lt;code&gt;curl&lt;/code&gt;: Confirm the request routes to Akamais staging servers:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; curl &lt;span class="nt"&gt;-vvv&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'User-Agent: Safari: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-cache-on"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-check-cacheable"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-akamai-internal: true"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-cache-remote-on"&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-get-cache-key"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-get-extracted-values"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-get-ssl-client-session-id"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-get-true-cache-key"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-serial-no"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: akamai-x-get-request-id"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Pragma: X-Akamai-CacheTrack"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="s2"&gt;"https://example.com.edgesuite-staging.net"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;2. Verifying Akamai Requests&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Response Headers&lt;/strong&gt; : Check for Akamai-specific headers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Cache: TCP_HIT&lt;/code&gt; (served from cache)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Cache: TCP_MISS&lt;/code&gt; (fetched from origin server)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Akamai-Staging: ESSL&lt;/code&gt; (indicates staging environment)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Browser Developer Tools&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open DevTools (Ctrl+Shift+I or Cmd+Option+I).&lt;/li&gt;
&lt;li&gt;Inspect response headers under the &lt;strong&gt;Network&lt;/strong&gt; tab for Akamai-specific details.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Deploying to Production&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Once testing is complete, promote the configuration to production via Akamai's property manager. Activating the changes ensures they are live without downtime.&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%2Fsf8q3fr9atrmd57uakhm.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%2Fsf8q3fr9atrmd57uakhm.png" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Akamai's staging and production environments simplify the process of testing and deploying secure, high-performing web applications. By adhering to best practices like optimizing caching, compressing assets, and leveraging Akamai's robust tools, you can ensure your web application delivers a seamless experience to users globally.&lt;/p&gt;

</description>
      <category>akamai</category>
      <category>cdn</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
