<?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: Huỳnh Lê Nhất Nghĩa</title>
    <description>The latest articles on DEV Community by Huỳnh Lê Nhất Nghĩa (@nghiadaulau).</description>
    <link>https://dev.to/nghiadaulau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1175626%2Fb7a7c231-e0f8-4785-88f4-798a38f3722e.jpg</url>
      <title>DEV Community: Huỳnh Lê Nhất Nghĩa</title>
      <link>https://dev.to/nghiadaulau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nghiadaulau"/>
    <language>en</language>
    <item>
      <title>Migrating &amp; Upgrading GitLab 15.6.1 to 19.2: New Box, Going Private, Cutover Behind Production</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Sat, 25 Jul 2026 13:01:49 +0000</pubDate>
      <link>https://dev.to/aws-builders/migrating-upgrading-gitlab-1561-to-192-new-box-going-private-cutover-behind-production-4mee</link>
      <guid>https://dev.to/aws-builders/migrating-upgrading-gitlab-1561-to-192-new-box-going-private-cutover-behind-production-4mee</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every company name, domain, IP, account ID, instance ID, and fingerprint in this article has been replaced with fictional values for illustration. The company is called &lt;strong&gt;Acme&lt;/strong&gt; (an ecommerce marketplace), the domain is &lt;strong&gt;acme.io&lt;/strong&gt;, and the platform codename is &lt;strong&gt;core&lt;/strong&gt;. Public IPs use the documentation range &lt;code&gt;203.0.113.0/24&lt;/code&gt;, private IPs have had their ranges changed, and account IDs are &lt;code&gt;1111...&lt;/code&gt;, &lt;code&gt;2222...&lt;/code&gt;. The structure and the approach are real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article recounts one night of migrating &lt;strong&gt;GitLab EE 15.6.1 to 19.2&lt;/strong&gt;. Not just a version bump. I also moved GitLab from public to &lt;strong&gt;private&lt;/strong&gt; (reachable only over VPN and PrivateLink), under one hard constraint:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't touch the old box.&lt;/strong&gt; The old box served production right up until the DNS flip. Everything read-only (backup, reading the host key), no config changes, no restarts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The end result: the new box &lt;code&gt;gitlab.acme.io&lt;/code&gt; runs 19.2, fully private, with ArgoCD, the runner, and VPN all able to reach it, and rollback is a single DNS record. The road there had many colors.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Context and constraints
&lt;/h2&gt;

&lt;p&gt;The starting state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old GitLab&lt;/strong&gt;: EE 15.6.1 omnibus, running on one EC2 (&lt;code&gt;i-0old...&lt;/code&gt;, private &lt;code&gt;10.10.5.6&lt;/code&gt;) in the &lt;strong&gt;Dev-core &lt;code&gt;111111111111&lt;/code&gt;&lt;/strong&gt; account, VPC &lt;code&gt;10.10.0.0/16&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Public via &lt;strong&gt;Cloudflare (proxied) then a Classic ELB (internet-facing) then the old box on ports 80/22&lt;/strong&gt;. TLS terminated at the ELB with an ACM &lt;code&gt;*.acme.io&lt;/code&gt; cert. Git over SSH (&lt;code&gt;git@gitlab.acme.io&lt;/code&gt;) went straight to ELB:22 (Cloudflare doesn't proxy port 22), so there was already an internal Route53 &lt;strong&gt;private zone&lt;/strong&gt; pointing &lt;code&gt;gitlab.acme.io&lt;/code&gt; at the ELB to bypass Cloudflare for SSH.&lt;/li&gt;
&lt;li&gt;Main consumers: &lt;strong&gt;ArgoCD&lt;/strong&gt; (~570 Applications, cloning over SSH), the &lt;strong&gt;GitLab Runner&lt;/strong&gt; (docker-autoscaler), and developers connecting through &lt;strong&gt;4 OpenVPN boxes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There was no new EE license key. The first question: install CE or EE? An EE backup &lt;strong&gt;can only be restored onto the same EE version&lt;/strong&gt;, so I had to build a new &lt;strong&gt;EE 15.6.1&lt;/strong&gt; box, restore, and only then step-upgrade to 19.2. No shortcut.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architecture: before and after
&lt;/h2&gt;

&lt;p&gt;Before the cutover, everything funneled through Cloudflare and the public ELB. After the cutover, there is no route to the internet, cross-account traffic goes through &lt;strong&gt;PrivateLink&lt;/strong&gt; at a &lt;strong&gt;network hub&lt;/strong&gt;, and the Route53 private zone decides who reaches it by which path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0rb95px3ug63smf1p0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx0rb95px3ug63smf1p0h.png" alt="GitLab 19.2 private architecture: consumers reach it via PrivateLink and Transit Gateway, split-horizon DNS" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key thing to remember: &lt;strong&gt;the new box has no public IP&lt;/strong&gt;, and sits in the dev VPC (&lt;code&gt;10.20.0.0/16&lt;/code&gt;). Consumers in other accounts (prod, ArgoCD) &lt;strong&gt;can't route to it directly&lt;/strong&gt;. Everything goes through a &lt;strong&gt;VPC Endpoint Service&lt;/strong&gt; at the &lt;strong&gt;network hub account &lt;code&gt;333333333333&lt;/code&gt;&lt;/strong&gt;, and spokes reach the hub over a &lt;strong&gt;Transit Gateway&lt;/strong&gt;. This is exactly the model another private service in the org already used, so I cloned it instead of inventing something new.&lt;/p&gt;

&lt;p&gt;Resource names (changed):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New box&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;i-0new...&lt;/code&gt;, &lt;code&gt;10.20.12.34&lt;/code&gt;, EE upgraded to 19.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal NLB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gitlab-priv-nlb&lt;/code&gt; (443 TLS ACM, 22 TCP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC Endpoint Service&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vpce-svc-0abc...&lt;/code&gt; (allow principal: hub account)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface endpoint (hub)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vpce-0def...&lt;/code&gt;, IPs &lt;code&gt;10.40.10.11&lt;/code&gt;, &lt;code&gt;.20.22&lt;/code&gt;, &lt;code&gt;.30.33&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private zone&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;acme.io&lt;/code&gt; (hub) and a &lt;code&gt;gitlab.acme.io&lt;/code&gt; override per VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Phase 1: backup without touching the old box
&lt;/h2&gt;

&lt;p&gt;The old box is private, no direct SSH, controlled through &lt;strong&gt;SSM Run Command&lt;/strong&gt;. The first backup died for two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(a) The SSM timeout killed the process.&lt;/strong&gt; An omnibus backup includes &lt;code&gt;artifacts&lt;/code&gt; by default. This repo has &lt;strong&gt;14GB of artifacts&lt;/strong&gt;. &lt;code&gt;gitlab-backup create&lt;/code&gt; ran for 40 minutes and then SSM &lt;code&gt;RunShellScript&lt;/code&gt; hit its &lt;code&gt;timeout-seconds&lt;/code&gt; and killed the process, leaving a half-finished backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(b) A stale PID lock.&lt;/strong&gt; The next run reported &lt;code&gt;There is a backup and restore task in progress&lt;/code&gt;, suggesting I remove &lt;code&gt;backup_restore.pid&lt;/code&gt;. The process killed by SSM never got to clean up the PID, so every subsequent backup was blocked.&lt;/p&gt;

&lt;p&gt;The fix: clear the dead lock, run the backup &lt;strong&gt;detached with &lt;code&gt;systemd-run&lt;/code&gt;&lt;/strong&gt; so it's immune to the SSM timeout, and &lt;strong&gt;skip artifacts&lt;/strong&gt; (14GB — accepting the loss of old artifacts; what matters is code, DB, and the repos on the new box).&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) confirm there is NO real process, then remove the dead lock&lt;/span&gt;
ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"rake gitlab:backup"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt;      &lt;span class="c"&gt;# empty&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /opt/gitlab/embedded/service/gitlab-rails/tmp/backup_restore.pid

&lt;span class="c"&gt;# 2) write a backup + upload script, run it as a transient systemd unit&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/root/gl-migrate-backup.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;SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
BUCKET=acme-gitlab-migrate-transfer
gitlab-backup create STRATEGY=copy SKIP=artifacts CRON=1 || { echo "BACKUP_FAILED rc=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="sh"&gt;"; exit 1; }
TAR=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; /var/opt/gitlab/backups/&lt;span class="k"&gt;*&lt;/span&gt;_gitlab_backup.tar | &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="sh"&gt;
cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab.rb /var/opt/gitlab/backups/
aws s3 cp "&lt;/span&gt;&lt;span class="nv"&gt;$TAR&lt;/span&gt;&lt;span class="sh"&gt;" "s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="sh"&gt;/migrate/" --only-show-errors &amp;amp;&amp;amp; echo UP_TAR_OK
aws s3 cp /var/opt/gitlab/backups/gitlab-secrets.json "s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="sh"&gt;/migrate/" &amp;amp;&amp;amp; echo UP_SECRETS_OK
aws s3 cp /var/opt/gitlab/backups/gitlab.rb "s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="sh"&gt;/migrate/" &amp;amp;&amp;amp; echo UP_RB_OK
echo "MIGRATE_BACKUP_DONE &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;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;SCRIPT
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /root/gl-migrate-backup.sh
systemd-run &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gl-migrate-backup &lt;span class="nt"&gt;--collect&lt;/span&gt; /root/gl-migrate-backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemd-run&lt;/code&gt; detaches the process entirely from SSM's process tree. The SSM command returns immediately, and the backup keeps running under &lt;code&gt;systemd&lt;/code&gt;. Follow it with &lt;code&gt;journalctl -u gl-migrate-backup&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99j0ucp1ntwnyq5pts5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99j0ucp1ntwnyq5pts5d.png" alt="GitLab backup running detached via systemd-run, followed with journalctl" width="780" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three things went up to S3: the 15G tarball, &lt;code&gt;gitlab-secrets.json&lt;/code&gt; (mandatory for decrypting CI variables and 2FA), and &lt;code&gt;gitlab.rb&lt;/code&gt;. The "don't touch the old box" constraint still held, because I only read data and deleted lock/temp files in the backup directory — not GitLab data, and no config changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Phase 2: restore and step-upgrade — this is the time sink
&lt;/h2&gt;

&lt;p&gt;The new box was installed with &lt;strong&gt;EE 15.6.1&lt;/strong&gt; to match the backup version (a hard requirement of &lt;code&gt;gitlab-backup restore&lt;/code&gt;). Verify the match before restoring, then load the old box's secrets, stop puma and sidekiq, and restore.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgmiyj1olx5khcs003gd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgmiyj1olx5khcs003gd8.png" alt="Restoring PostgreSQL with a benign pg_trgm warning, then verifying the data came in fully" width="780" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two red &lt;code&gt;ERROR: must be owner of extension pg_trgm&lt;/code&gt; lines scare newcomers, but they're benign: the extension already exists, restore just couldn't set the owner. The data still came in fully. The &lt;code&gt;skipped restore ... .wiki.bundle doesn't exist&lt;/code&gt; lines are because those projects never created a wiki. It's precisely these warnings that make &lt;code&gt;gitlab-backup restore&lt;/code&gt; return &lt;strong&gt;exit code 1&lt;/strong&gt; even though the restore finished — don't let a script bail there.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Bugs hit during the step-upgrade
&lt;/h3&gt;

&lt;p&gt;The restore only took ten minutes. The real time sink is the &lt;strong&gt;step-upgrade from 15.6.1 to 19.2&lt;/strong&gt;, and that's where most of the bugs live. Logging each one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 1: required-stops, no leapfrogging.&lt;/strong&gt; GitLab forces you to stop at specific intermediate versions. The path comes from the official upgrade-path tool, roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15.6.1 -&amp;gt; 15.11.13 -&amp;gt; 16.0.10 -&amp;gt; 16.1.8 -&amp;gt; 16.3.9 -&amp;gt; 16.7.10
       -&amp;gt; 16.11.10 -&amp;gt; 17.1.8 -&amp;gt; 17.3.7 -&amp;gt; 17.5.5 -&amp;gt; 17.8.7 -&amp;gt; 17.11.6
       -&amp;gt; 18.0.x -&amp;gt; 18.2.x -&amp;gt; ... -&amp;gt; 19.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jumping straight there makes the reconfigure die. Each hop has to install exactly that version, &lt;code&gt;reconfigure&lt;/code&gt;, wait for migrations to finish, and only then move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 2: batched background migrations hang and block the next hop.&lt;/strong&gt; This is the number-one time demon. After each upgrade, GitLab queues a batch of background migrations that run gradually through Sidekiq (backfilling columns, changing data types, adding indexes on large tables). With a DB of 71k merge requests, 113k pipelines, and 128k notes, these crawl. If you upgrade again before they're &lt;code&gt;finished&lt;/code&gt;, the next hop errors out and forces you back to wait. Check them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitlab-psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"select job_class_name, status from batched_background_migrations
                where status not in (3,6);"&lt;/span&gt;   &lt;span class="c"&gt;# 3=finished, 6=finalized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Case 3: major PostgreSQL upgrades.&lt;/strong&gt; GitLab bundles Postgres. Around 16.x you have to move PG 13 to 14, and around 17.x/18.x up to PG 16. Run &lt;code&gt;gitlab-ctl pg-upgrade&lt;/code&gt;. The trap: &lt;code&gt;pg-upgrade&lt;/code&gt; needs &lt;strong&gt;double the disk space&lt;/strong&gt; of the current data (it copies to a new cluster). A nearly-full disk fails midway — extremely annoying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 4: old settings in &lt;code&gt;gitlab.rb&lt;/code&gt; break reconfigure.&lt;/strong&gt; Across several majors, some keys in &lt;code&gt;gitlab.rb&lt;/code&gt; were renamed or removed. Reconfigure stops when it hits an unknown key. You have to read the &lt;code&gt;reconfigure&lt;/code&gt; log, remove the deprecated key, and re-run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 5: the runner registration token was retired (16.0+).&lt;/strong&gt; From 16.0, the old &lt;code&gt;registration_token&lt;/code&gt; style of registering runners is disabled by default, replaced by the new runner authentication token (&lt;code&gt;glrt-...&lt;/code&gt;). The old runner loses its connection until you create a new token. I hit exactly this when reattaching the runner (section 7.7).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case 6: reconfigure times out on heavy migrations.&lt;/strong&gt; Some migrations (adding an index on &lt;code&gt;ci_builds&lt;/code&gt;, backfilling a large column) take tens of minutes. &lt;code&gt;reconfigure&lt;/code&gt; waits for migrations by default, and if it runs inside SSM it hits the SSM timeout again, just like the backup. Same lesson repeated: &lt;strong&gt;run the whole upgrade chain detached&lt;/strong&gt; (&lt;code&gt;systemd-run&lt;/code&gt; or &lt;code&gt;tmux&lt;/code&gt;), not inside SSM's process tree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp07nb26ti6ttiesydnt2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp07nb26ti6ttiesydnt2.png" alt="Step-upgrade across required-stops, forcing batched background migrations and pg-upgrade" width="780" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. How to speed up the upgrade
&lt;/h3&gt;

&lt;p&gt;Left to run "naturally", the upgrade chain can take a whole day, mostly sitting and waiting on background migrations. What I did to shorten it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Force batched background migrations to run now, don't wait for Sidekiq to trickle.&lt;/strong&gt; Instead of letting them crawl, finalize each hanging migration manually:&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;# list active migrations&lt;/span&gt;
gitlab-rails runner &lt;span class="s2"&gt;"Gitlab::Database::BackgroundMigration::BatchedMigration.active &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  .each { |m| puts &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;#{m.job_class_name} #{m.table_name}.#{m.column_name}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; }"&lt;/span&gt;

&lt;span class="c"&gt;# finalize synchronously (runs to completion now, instead of waiting forever)&lt;/span&gt;
gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,ci_builds,id,&lt;span class="s1"&gt;'[["id"],["id_convert_to_bigint"]]'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Temporarily upsize the box during the upgrade.&lt;/strong&gt; Migrations are CPU- and IO-heavy. Switch to a larger instance type (more vCPU, RAM, high-IOPS gp3 EBS) during the upgrade night, then downsize afterward. Index build times drop noticeably.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Give resources to Sidekiq, starve Puma.&lt;/strong&gt; The migration box serves no traffic, so raise Sidekiq concurrency and turn off almost all of Puma so Sidekiq chews through migrations faster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/gitlab/gitlab.rb  (only during the upgrade phase)&lt;/span&gt;
&lt;span class="n"&gt;sidekiq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'max_concurrency'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="n"&gt;puma&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'worker_processes'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Tune Postgres for bulk work.&lt;/strong&gt; Temporarily raise &lt;code&gt;maintenance_work_mem&lt;/code&gt; and &lt;code&gt;max_wal_size&lt;/code&gt; for faster index builds, and restore the defaults when done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Pre-download packages for every hop up front.&lt;/strong&gt; Don't sit waiting on downloads between stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get download gitlab-ee&lt;span class="o"&gt;=&lt;/span&gt;15.11.13-ee.0 gitlab-ee&lt;span class="o"&gt;=&lt;/span&gt;16.0.10-ee.0 gitlab-ee&lt;span class="o"&gt;=&lt;/span&gt;16.3.9-ee.0 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Disable cron jobs and integrations during the upgrade&lt;/strong&gt; so Sidekiq doesn't fight for workers with pipeline schedules, webhooks, and mirrors.&lt;/p&gt;

&lt;p&gt;The end of the chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cat /opt/gitlab/embedded/service/gitlab-rails/VERSION
19.2.0-ee
$ gitlab-rails runner 'puts %Q[projects=#{Project.count} users=#{User.count} MR=#{MergeRequest.count}]'
projects=458  users=192  MR=71854
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Phase 3: going private with NLB and PrivateLink
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to get wrong. The initial plan was an &lt;strong&gt;internal ALB (HTTP/443)&lt;/strong&gt; in the dev VPC. Wrong, because of two things I found when looking at how ArgoCD actually talks to GitLab:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD clones over SSH&lt;/strong&gt; (&lt;code&gt;git@gitlab.acme.io&lt;/code&gt;), not HTTPS. An ALB is L7 HTTP and can't carry port 22.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account has to go over PrivateLink&lt;/strong&gt;, and a &lt;strong&gt;VPC Endpoint Service only sits in front of an NLB&lt;/strong&gt;, not an ALB.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So I switched to an &lt;strong&gt;NLB&lt;/strong&gt; with two listeners: &lt;code&gt;443 TLS&lt;/code&gt; (terminated with the ACM &lt;code&gt;*.acme.io&lt;/code&gt; cert, forwarding &lt;code&gt;TCP:80&lt;/code&gt; to GitLab's nginx) and &lt;code&gt;22 TCP&lt;/code&gt; (passthrough to the box's sshd).&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;# target groups: HTTPS (TLS terminated at NLB -&amp;gt; box:80) and SSH (TCP -&amp;gt; box:22)&lt;/span&gt;
aws elbv2 create-target-group &lt;span class="nt"&gt;--name&lt;/span&gt; gitlab-priv-https-tg &lt;span class="nt"&gt;--protocol&lt;/span&gt; TCP &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-dev &lt;span class="nt"&gt;--target-type&lt;/span&gt; ip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-protocol&lt;/span&gt; HTTP &lt;span class="nt"&gt;--health-check-path&lt;/span&gt; /users/sign_in &lt;span class="nt"&gt;--matcher&lt;/span&gt; &lt;span class="nv"&gt;HttpCode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200
aws elbv2 create-target-group &lt;span class="nt"&gt;--name&lt;/span&gt; gitlab-priv-ssh-tg &lt;span class="nt"&gt;--protocol&lt;/span&gt; TCP &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-dev &lt;span class="nt"&gt;--target-type&lt;/span&gt; ip &lt;span class="nt"&gt;--health-check-protocol&lt;/span&gt; TCP
aws elbv2 register-targets &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; &lt;span class="nv"&gt;$TGH&lt;/span&gt; &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.20.12.34,Port&lt;span class="o"&gt;=&lt;/span&gt;80
aws elbv2 register-targets &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; &lt;span class="nv"&gt;$TGS&lt;/span&gt; &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.20.12.34,Port&lt;span class="o"&gt;=&lt;/span&gt;22

&lt;span class="c"&gt;# internal NLB + listeners&lt;/span&gt;
aws elbv2 create-load-balancer &lt;span class="nt"&gt;--name&lt;/span&gt; gitlab-priv-nlb &lt;span class="nt"&gt;--type&lt;/span&gt; network &lt;span class="nt"&gt;--scheme&lt;/span&gt; internal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnets&lt;/span&gt; subnet-1a subnet-1b subnet-1c
aws elbv2 create-listener &lt;span class="nt"&gt;--load-balancer-arn&lt;/span&gt; &lt;span class="nv"&gt;$NLB&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; TLS &lt;span class="nt"&gt;--port&lt;/span&gt; 443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificates&lt;/span&gt; &lt;span class="nv"&gt;CertificateArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$ACM&lt;/span&gt; &lt;span class="nt"&gt;--ssl-policy&lt;/span&gt; ELBSecurityPolicy-TLS13-1-2-2021-06 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--default-actions&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;forward,TargetGroupArn&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TGH&lt;/span&gt;
aws elbv2 create-listener &lt;span class="nt"&gt;--load-balancer-arn&lt;/span&gt; &lt;span class="nv"&gt;$NLB&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; TCP &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--default-actions&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;forward,TargetGroupArn&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TGS&lt;/span&gt;

&lt;span class="c"&gt;# a VPC Endpoint Service in front of the NLB, so the hub account can create an interface endpoint&lt;/span&gt;
&lt;span class="nv"&gt;SVC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 create-vpc-endpoint-service-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-load-balancer-arns&lt;/span&gt; &lt;span class="nv"&gt;$NLB&lt;/span&gt; &lt;span class="nt"&gt;--no-acceptance-required&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'ServiceConfiguration.ServiceId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
aws ec2 modify-vpc-endpoint-service-permissions &lt;span class="nt"&gt;--service-id&lt;/span&gt; &lt;span class="nv"&gt;$SVC&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-allowed-principals&lt;/span&gt; arn:aws:iam::333333333333:root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the &lt;strong&gt;network hub (&lt;code&gt;333333333333&lt;/code&gt;)&lt;/strong&gt;, create an &lt;strong&gt;interface endpoint&lt;/strong&gt; in the shared VPC (&lt;code&gt;10.40.x&lt;/code&gt;), with a security group for 22 and 443 from the internal ranges. Test from inside the VPC (the NLB is private, so it can only be tested from within), with the right SNI and Host &lt;code&gt;gitlab.acme.io&lt;/code&gt;: &lt;code&gt;https=200&lt;/code&gt;, &lt;code&gt;ssl=0&lt;/code&gt;, and the banner &lt;code&gt;SSH-2.0-OpenSSH&lt;/code&gt;. Cross-account from the prod VPN and from a pod in the ArgoCD cluster to the hub endpoint also worked on both 443 and 22. The PrivateLink tunnel is internal, so there's no need to open up the dev VPC's route segmentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Phase 4: cutover with a Route53 private zone (split-horizon)
&lt;/h2&gt;

&lt;p&gt;The cutover doesn't touch Cloudflare (Cloudflare can't reach the private endpoint). The flip point is the &lt;strong&gt;Route53 private zone&lt;/strong&gt; that every VPC uses to resolve &lt;code&gt;gitlab.acme.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem: &lt;strong&gt;the dev VPN is in the same VPC as the NLB (&lt;code&gt;10.20&lt;/code&gt;) but has no route to the hub (&lt;code&gt;10.40&lt;/code&gt;)&lt;/strong&gt;. So we need &lt;strong&gt;split-horizon&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The shared (hub) zone &lt;code&gt;gitlab.acme.io&lt;/code&gt; points at the &lt;strong&gt;hub endpoint&lt;/strong&gt;, for the prod VPN, ArgoCD, and other accounts.&lt;/li&gt;
&lt;li&gt;A separate override zone for the dev VPC points &lt;code&gt;gitlab.acme.io&lt;/code&gt; straight at the &lt;strong&gt;NLB&lt;/strong&gt;, for the dev VPN in the same VPC.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# override zone for the DEV VPC -&amp;gt; NLB (alias works because the record is in the SAME account as the NLB)&lt;/span&gt;
&lt;span class="nv"&gt;Z&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws route53 create-hosted-zone &lt;span class="nt"&gt;--name&lt;/span&gt; gitlab.acme.io &lt;span class="nt"&gt;--caller-reference&lt;/span&gt; dev-1 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--hosted-zone-config&lt;/span&gt; &lt;span class="nv"&gt;PrivateZone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--vpc&lt;/span&gt; &lt;span class="nv"&gt;VPCRegion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-southeast-1,VPCId&lt;span class="o"&gt;=&lt;/span&gt;vpc-dev &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'HostedZone.Id'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
aws route53 change-resource-record-sets &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; &lt;span class="nv"&gt;$Z&lt;/span&gt; &lt;span class="nt"&gt;--change-batch&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Changes":[{"Action":"UPSERT","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"A",
    "AliasTarget":{"HostedZoneId":"'&lt;/span&gt;&lt;span class="nv"&gt;$NLB_ZONE&lt;/span&gt;&lt;span class="s1"&gt;'","DNSName":"gitlab-priv-nlb-...elb.amazonaws.com.","EvaluateTargetHealth":false}}}]}'&lt;/span&gt;

&lt;span class="c"&gt;# hub zone: change CNAME(old ELB) INTO an A alias -&amp;gt; hub endpoint&lt;/span&gt;
aws route53 change-resource-record-sets &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; &lt;span class="nv"&gt;$HUB_ZONE&lt;/span&gt; &lt;span class="nt"&gt;--change-batch&lt;/span&gt; &lt;span class="s1"&gt;'{
  "Changes":[
   {"Action":"DELETE","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"CNAME","TTL":300,
     "ResourceRecords":[{"Value":"acme-gitlab-legacy-lb-...elb.amazonaws.com."}]}},
   {"Action":"CREATE","ResourceRecordSet":{"Name":"gitlab.acme.io.","Type":"A",
     "AliasTarget":{"HostedZoneId":"'&lt;/span&gt;&lt;span class="nv"&gt;$VPCE_ZONE&lt;/span&gt;&lt;span class="s1"&gt;'","DNSName":"vpce-0def-...vpce.amazonaws.com.","EvaluateTargetHealth":false}}}
  ]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rollback is pointing the hub zone's record back at the old ELB. The old box and old ELB are untouched, running in parallel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz66puao0a7hxwkvb42gz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz66puao0a7hxwkvb42gz.png" alt="Verifying the cutover from the dev VPN and prod VPN, flushing negative cache, adding a VPN route" width="780" height="260"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Seven little demons after the cutover
&lt;/h2&gt;

&lt;p&gt;This part is fun. Every bug below happened for real, in between 4 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1. SSM timeout and stale PID lock
&lt;/h3&gt;

&lt;p&gt;Already covered in Phase 1. Lesson: long tasks go detached with &lt;code&gt;systemd-run&lt;/code&gt;, not in SSM's process tree. And always &lt;code&gt;SKIP=artifacts&lt;/code&gt; if object storage isn't used.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2. SSH host key mismatch, ArgoCD frozen
&lt;/h3&gt;

&lt;p&gt;Right after the cutover, ArgoCD reported &lt;code&gt;ssh: handshake failed: knownhosts: key mismatch&lt;/code&gt;. The new box generated its own new host key, different from the old box, so ArgoCD's &lt;code&gt;known_hosts&lt;/code&gt; (pinning the old key) didn't match. The correct approach for a migration is to &lt;strong&gt;keep the host identity&lt;/strong&gt;: copy the old box's host key onto the new box and restart sshd. After that, &lt;code&gt;ssh-keyscan&lt;/code&gt; returns the same old fingerprint, and nothing has to change on the client side.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3. Empty &lt;code&gt;authorized_keys&lt;/code&gt; after restore, publickey auth fails
&lt;/h3&gt;

&lt;p&gt;Host key matched, and ArgoCD then reported &lt;code&gt;unable to authenticate, attempted methods [none publickey]&lt;/code&gt;. The new box uses the &lt;code&gt;authorized_keys&lt;/code&gt; file for SSH auth, but after the restore that file was 0 bytes. &lt;code&gt;gitlab-backup restore&lt;/code&gt; restores the DB (which has 111 SSH keys) but doesn't regenerate the file itself. Fix it with a single rake task: &lt;code&gt;gitlab-rake gitlab:shell:setup&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8xk7bf4n64oc5blbfbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa8xk7bf4n64oc5blbfbm.png" alt="Fixing ArgoCD: copy the old box's host key, rebuild authorized_keys, git ls-remote works for real" width="780" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7.4. NLB cross-zone not enabled, 2/3 endpoint IPs can't reach port 22
&lt;/h3&gt;

&lt;p&gt;The client resolves &lt;code&gt;gitlab.acme.io&lt;/code&gt; to 3 endpoint IPs (round-robin). Testing showed only 1/3 IPs could open port 22. The target box is in a single AZ, and the NLB has &lt;strong&gt;cross-zone disabled&lt;/strong&gt; by default, so an NLB node in another AZ has no target and the connect fails — hit or miss. One-line fix: enable &lt;code&gt;load_balancing.cross_zone.enabled=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5tmjtc5mb6y9n0v4qx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5tmjtc5mb6y9n0v4qx6.png" alt="NLB cross-zone: before enabling, 2/3 IPs have port 22 closed; after, all 3 are open" width="780" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7.5. A cross-account Route53 alias to a VPC endpoint won't resolve
&lt;/h3&gt;

&lt;p&gt;The override zone for the prod VPN (in the prod account) aliased to the hub endpoint (in the hub account), and &lt;code&gt;dig&lt;/code&gt; returned empty. Meanwhile the hub zone (same account as the endpoint) resolved the alias fine. The cause: &lt;strong&gt;a Route53 alias to a VPC endpoint only resolves when the record is in the same account as the endpoint&lt;/strong&gt;. Cross-account, use an A record with the endpoint's real IPs instead of an alias.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.6. DNS negative caching
&lt;/h3&gt;

&lt;p&gt;After changing the record, &lt;code&gt;dig&lt;/code&gt; on the prod box still returned empty, because the earlier failed-alias lookup had been negatively cached in &lt;code&gt;systemd-resolved&lt;/code&gt;. Query the Amazon resolver &lt;code&gt;169.254.169.253&lt;/code&gt; directly to check, then &lt;code&gt;resolvectl flush-caches&lt;/code&gt;. When a DNS change doesn't show up, suspect negative cache before you suspect Route53.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.7. VPN missing a route push, devs report "connected but still can't get in"
&lt;/h3&gt;

&lt;p&gt;Devs reported that after connecting to &lt;code&gt;vpn-core.acme.io&lt;/code&gt; they still couldn't open GitLab. This one is an OpenVPN Access Server in the prod VPC. Looking at its config: it pushed DNS correctly (the VPC resolver, resolving to &lt;code&gt;10.40.x&lt;/code&gt;), but the pushed route list had &lt;code&gt;10.60&lt;/code&gt;, &lt;code&gt;10.10&lt;/code&gt;, and a pile of Cloudflare ranges (&lt;code&gt;104.x&lt;/code&gt;, &lt;code&gt;172.64.x&lt;/code&gt;, leftovers from when GitLab was public), yet was &lt;strong&gt;missing &lt;code&gt;10.40.0.0/16&lt;/code&gt;&lt;/strong&gt; (the hub endpoint's network). The client resolved to &lt;code&gt;10.40.x&lt;/code&gt; but had no route there, so traffic went out the default gateway. Push the extra route and have clients reconnect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sacli &lt;span class="nt"&gt;--key&lt;/span&gt; &lt;span class="s2"&gt;"vpn.server.routing.private_network.23"&lt;/span&gt; &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"10.40.0.0/16"&lt;/span&gt; ConfigPut
sacli start   &lt;span class="c"&gt;# clients disconnect + reconnect to pick up the new route&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sweep all 4 VPN boxes while you're at it — one of them also had its primary DNS set to &lt;code&gt;8.8.8.8&lt;/code&gt; (public, can't resolve the private zone); fix it back to the VPC resolver.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Tip: a debug DaemonSet for instant network probing
&lt;/h2&gt;

&lt;p&gt;Throughout the cutover, the most-repeated action was: "from cluster X, can I resolve &lt;code&gt;gitlab.acme.io&lt;/code&gt;, and can I reach &lt;code&gt;:443&lt;/code&gt; and &lt;code&gt;:22&lt;/code&gt;?". Running &lt;code&gt;kubectl run --image=netshoot&lt;/code&gt; each time is slow — it pulls the image and waits to schedule. Instead, pre-place a &lt;strong&gt;netshoot DaemonSet&lt;/strong&gt; on every node, so there's already a pod to &lt;code&gt;exec&lt;/code&gt; into, no waiting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DaemonSet&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netshoot-debug&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infra-debug&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;netshoot-debug&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;netshoot-debug&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tolerations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Exists&lt;/span&gt;          &lt;span class="c1"&gt;# run even on tainted nodes&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netshoot&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nicolaka/netshoot:latest&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;infinity"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;10m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;32Mi&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;100m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;128Mi&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Probe instantly from any node:&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="nv"&gt;POD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; infra-debug get pod &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;netshoot-debug &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--field-selector&lt;/span&gt; spec.nodeName&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$NODE&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].metadata.name}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; infra-debug &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$POD&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
  getent ahostsv4 gitlab.acme.io
  nc -z -w3 gitlab.acme.io 443 &amp;amp;&amp;amp; echo 443_OK
  nc -z -w3 gitlab.acme.io 22  &amp;amp;&amp;amp; echo 22_OK
  ssh-keyscan -t ed25519 gitlab.acme.io 2&amp;gt;/dev/null | ssh-keygen -lf -
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A debug DaemonSet is a double-edged sword — netshoot carries a full network toolkit. Put it in its own namespace, cap its resources, and &lt;strong&gt;remove it when done&lt;/strong&gt; (&lt;code&gt;kubectl delete ds netshoot-debug -n infra-debug&lt;/code&gt;). Don't leave it living in prod forever. During the cutover it's also worth enabling &lt;strong&gt;GuardDuty Runtime Monitoring&lt;/strong&gt; (the DaemonSet agent) to have eyes on unusual egress while you're opening and closing network paths repeatedly.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Results and lessons
&lt;/h2&gt;

&lt;p&gt;Acceptance: all three consumer groups reached the new, private box.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;443&lt;/th&gt;
&lt;th&gt;22 (git SSH)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dev VPN&lt;/td&gt;
&lt;td&gt;override zone to NLB (same VPC)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prod VPN&lt;/td&gt;
&lt;td&gt;override zone to hub endpoint&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArgoCD&lt;/td&gt;
&lt;td&gt;hub zone to hub endpoint&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git ls-remote&lt;/code&gt; works for real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runner v2&lt;/td&gt;
&lt;td&gt;NLB directly&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;online&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things that stuck:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't upgrade the new box to the target version too early.&lt;/strong&gt; A cross-version backup can't be restored or merged. If the old box keeps producing commits and MRs after the backup, you can't incrementally sync between 15.6.1 and 19.2. Shrink the window between the last backup and the cutover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PrivateLink, not peering.&lt;/strong&gt; Reaching a private service cross-account should go through a VPC Endpoint Service at the hub — it doesn't break segmentation and needs no direct route into the target VPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NLB for GitLab, not ALB&lt;/strong&gt;, because you need SSH port 22 too, and because the endpoint service only sits in front of an NLB. Remember to enable cross-zone if the target is in a single AZ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the host identity when migrating&lt;/strong&gt; (copy &lt;code&gt;/etc/ssh/ssh_host_*&lt;/code&gt;) so clients don't hit a key mismatch. And always run &lt;code&gt;gitlab-rake gitlab:shell:setup&lt;/code&gt; after a restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Route53 alias to a VPC endpoint only works same-account.&lt;/strong&gt; Cross-account, use an A record with IPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a DNS change doesn't show up, suspect negative cache&lt;/strong&gt; (&lt;code&gt;resolvectl flush-caches&lt;/code&gt;) before suspecting Route53.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A network cutover comes with a VPN route cutover.&lt;/strong&gt; When a service moves from public (Cloudflare ranges) to private (new internal ranges), the VPN has to push the new ranges, otherwise it's "connected but still can't get in".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The biggest time sink wasn't the cutover — it was the step-upgrade chain and the pile of batched background migrations in the middle. Prep a strong box, force migrations to run now instead of waiting, and do everything detached: that's how you cut a day down to a night. And the old box didn't get a single config line touched until it was shut off.&lt;/p&gt;




&lt;h2&gt;
  
  
  Come contribute
&lt;/h2&gt;

&lt;p&gt;Synapse is open source (Apache-2.0): &lt;strong&gt;&lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;github.com/KKloudTarus/synapse-ce&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Star it if the "AI proposes, Go and a second model confirm" direction is one you want to follow.&lt;/li&gt;
&lt;li&gt;Run it against your own LLM endpoint and tell me what happened: which model refutes well, which one likes to make things up. Feedback from a real model is the most useful kind.&lt;/li&gt;
&lt;li&gt;The verifier's adversarial prompt, the &lt;code&gt;driver&lt;/code&gt; vocabulary, the consensus threshold: it all lives in &lt;code&gt;internal/usecase/fptriage&lt;/code&gt;, and it's short and easy to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you turn this on in your own repo, drop a comment with how many false positives the AI held back on the first run, and whether it ever came close to refuting something that turned out to be real. I'm curious.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>gitlab</category>
      <category>git</category>
      <category>security</category>
    </item>
    <item>
      <title>A security merge gate is only as good as its false positive rate</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:06:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/a-security-merge-gate-is-only-as-good-as-its-false-positive-rate-4d1p</link>
      <guid>https://dev.to/aws-builders/a-security-merge-gate-is-only-as-good-as-its-false-positive-rate-4d1p</guid>
      <description>&lt;p&gt;A hard gate on merge is the right instinct. Security findings are cheapest when they interrupt a change before it becomes shared state, before other branches build on it, before it ships. Blocking the merge is the correct place for that class of control.&lt;/p&gt;

&lt;p&gt;But there is a failure mode that is worse than having no gate at all: a gate that blocks too eagerly. The first time it flags something that is obviously fine, people start looking for the bypass flag. The second time, they add it to their muscle memory. After that the gate is theater, and the one real finding it catches next quarter gets waved through with everything else.&lt;/p&gt;

&lt;p&gt;So the interesting engineering problem is not "can we block a merge on a security finding." That part is easy. The problem is staying accurate enough that nobody wants to route around you.&lt;/p&gt;

&lt;p&gt;Here is how we think about it on Synapse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F700yo8bjti8l7q2mkgmt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F700yo8bjti8l7q2mkgmt.png" alt="The merge gate: request-changes on a secret-hygiene finding, then approve and merge once it is fixed" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The gate blocked a real pull request until an embedded-credential git URL was rejected, then let it through.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate has to be explainable
&lt;/h2&gt;

&lt;p&gt;A finding that blocks a merge carries four things by construction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what failed (a rule id and a CWE),&lt;/li&gt;
&lt;li&gt;why it matters (a short rationale),&lt;/li&gt;
&lt;li&gt;the evidence it used,&lt;/li&gt;
&lt;li&gt;and the exact change that makes it pass (a compliant example).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a dependency finding the evidence is the scanner and the advisory database version. For a static analysis finding it is the matched pattern or the data flow, at a specific file and line. The evidence is hash chained, so a finding cannot be quietly edited after the fact and still pass the report gate.&lt;/p&gt;

&lt;p&gt;The goal is that a developer never has to guess why the gate fired or argue with a black box. If the gate cannot tell you what to change, it has not earned the right to block you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precision is a release gate for the rules, not just for your code
&lt;/h2&gt;

&lt;p&gt;This is the part most tools get wrong, and it is the part we care about most.&lt;/p&gt;

&lt;p&gt;Our deterministic detectors (dependency scanning, pattern static analysis, taint, secrets, misconfiguration) publish findings with no model in the path. Same input, same finding, every time. There is no AI triage step softening the output. That is a feature, because it makes the gate reproducible and auditable. It is also a constraint, because it means a false positive prone rule is not a nuisance. It is a bug in the tool.&lt;/p&gt;

&lt;p&gt;A concrete example. We were reviewing a community pull request that added a rule pack for Jupyter and IPython notebooks. Fifteen new rules, clean build, all tests green. Two of the rules looked completely reasonable on paper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One flagged &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; markup saved in a notebook's output cell. Reasonable sounding, since script markup in output is an XSS smell.&lt;/li&gt;
&lt;li&gt;One flagged local filesystem paths like &lt;code&gt;/home/alice/...&lt;/code&gt; saved in a traceback, since those can disclose environment structure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both would have been a disaster on real notebooks.&lt;/p&gt;

&lt;p&gt;Plotly, Bokeh, ipywidgets, and Altair all embed &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags in their normal chart output. Every interactive plot in every data science notebook would have tripped the first rule. And every saved Python exception contains &lt;code&gt;site-packages&lt;/code&gt; frames under a home directory, so the second rule would fire on essentially any notebook that ever caught an error.&lt;/p&gt;

&lt;p&gt;Neither rule is wrong in theory. Both are wrong in practice, because they cry wolf on ordinary code. So we blocked the merge until they were tightened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The script rule now requires a real injection signal in the markup, like an event handler attribute, &lt;code&gt;document.write(&lt;/code&gt;, or &lt;code&gt;eval(&lt;/code&gt;, not just the presence of a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;The traceback rule now excludes &lt;code&gt;site-packages&lt;/code&gt;, &lt;code&gt;dist-packages&lt;/code&gt;, and virtualenv frames, and fires only on a genuine project path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp10wu71w2gk7upg9ci83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp10wu71w2gk7upg9ci83.png" alt="The tightened rules plus regression tests: Plotly and Bokeh output no longer flagged, real injection and real project paths still flagged" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The rule count stayed the same. The false positive rate went to near zero, with regression tests to keep it there.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bar we hold is simple: if a rule fires on ordinary, safe code, it does not ship. Precision is a gate the rules have to pass before they are allowed to gate anyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI generated code and the shared blind spot
&lt;/h2&gt;

&lt;p&gt;A growing share of pull requests are agent generated now, and they read clean at a glance. The dangerous case is not the code that looks sloppy. It is the code where the same agent also wrote the tests, so the suite inherits the exact blind spot the implementation has. Everything is green, and the vulnerable pattern sails through.&lt;/p&gt;

&lt;p&gt;This is why the gate is pattern and data flow based, and deliberately independent of the test suite rather than derived from it. If the tests missed it because they shared the author's assumptions, the gate still does not. A gate that trusts the tests is only as good as whoever wrote the tests, which increasingly is the same model that wrote the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the model propose, do not let it decide
&lt;/h2&gt;

&lt;p&gt;We do use models. We just do not let them block a merge on their own opinion.&lt;/p&gt;

&lt;p&gt;Any analytical claim, whether it is reachability, a risk narrative, or a suspected exploit, is a proposal. It cannot gate anything until a separate, deterministic verifier seals a verdict over a threshold. A confident but wrong model output never becomes a merge blocker, because it never gets to be the one deciding.&lt;/p&gt;

&lt;p&gt;For dependency findings, reachability and taint analysis decide whether a vulnerable symbol is actually reachable in your code before the finding escalates. "It exists somewhere in the tree" is not automatically a hard stop. That alone removes a large slice of the noise that makes teams resent dependency scanners.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate stays green because it is boring
&lt;/h2&gt;

&lt;p&gt;None of this works if the checks are flaky. The detectors are plain, deterministic Go: build with and without CGO, vet, and a fast test suite that runs on every change. Reproducible checks are what let you trust a red result enough to block on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5h3ddl16atsfkj6jyiof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5h3ddl16atsfkj6jyiof.png" alt="Build, vet, and the project test suite all passing" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Deterministic checks: same input, same result, fast enough to run on every pull request.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet part
&lt;/h2&gt;

&lt;p&gt;Catching real issues is table stakes. Any scanner can produce findings. The actual engineering work is in the negative space: not firing on the Plotly chart, not firing on the site-packages traceback, not blocking a merge over a vulnerable function nobody calls.&lt;/p&gt;

&lt;p&gt;A gate earns trust by being right, being specific about the fix, and being quiet when there is nothing wrong. Get that part right and people stop trying to route around it, because there is nothing to route around. Get it wrong and it does not matter how good your detection is, because the gate everyone silences is the same as no gate at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Come contribute
&lt;/h2&gt;

&lt;p&gt;Synapse is open source (Apache-2.0): &lt;strong&gt;&lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;github.com/KKloudTarus/synapse-ce&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Star it if the "AI proposes, Go and a second model confirm" direction is one you want to follow.&lt;/li&gt;
&lt;li&gt;Run it against your own LLM endpoint and tell me what happened: which model refutes well, which one likes to make things up. Feedback from a real model is the most useful kind.&lt;/li&gt;
&lt;li&gt;The verifier's adversarial prompt, the &lt;code&gt;driver&lt;/code&gt; vocabulary, the consensus threshold: it all lives in &lt;code&gt;internal/usecase/fptriage&lt;/code&gt;, and it's short and easy to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you turn this on in your own repo, drop a comment with how many false positives the AI held back on the first run, and whether it ever came close to refuting something that turned out to be real. I'm curious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>contributorswanted</category>
      <category>go</category>
    </item>
    <item>
      <title>Let an AI clear out your false positives without letting it hide a real bug</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:07:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/let-an-ai-clear-out-your-false-positives-without-letting-it-hide-a-real-bug-1akl</link>
      <guid>https://dev.to/aws-builders/let-an-ai-clear-out-your-false-positives-without-letting-it-hide-a-real-bug-1akl</guid>
      <description>&lt;p&gt;Last time I wrote about wiring up a security gate that blocks merges in CI with &lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;Synapse&lt;/a&gt;. Someone left a comment that hit the exact sore spot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The gate is the easy part. Two weeks in, the team turns it off because it's red over false positives every single time."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yep. A gate that cries wolf too often dies on its own. People start hitting "merge anyway," and by the time a real vulnerability shows up, nobody's looking anymore.&lt;/p&gt;

&lt;p&gt;The obvious 2020s fix is to hand the whole pile to an LLM and say "delete the junk." Except: the moment a security tool lets an AI &lt;em&gt;delete&lt;/em&gt; findings, you've just built a brand new vulnerability. The model guesses wrong once, a real SQL injection quietly drops off the report, and nobody's the wiser.&lt;/p&gt;

&lt;p&gt;So Synapse does it differently. The AI is allowed to &lt;em&gt;propose&lt;/em&gt; that a finding is a false positive. It is never allowed to confirm that on its own, and it is never allowed to delete anything. This whole post is a real run. Terminal output at the bottom, nothing faked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the actual "gate" is
&lt;/h2&gt;

&lt;p&gt;Three rules, and all three exist so you can trust the output:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The model only proposes.&lt;/strong&gt; It doesn't return prose, and it doesn't return a delete command. It returns exactly one typed verdict: &lt;code&gt;verdict&lt;/code&gt; is one of &lt;code&gt;refuted | sound | uncertain&lt;/code&gt;, &lt;code&gt;driver&lt;/code&gt; is a closed snake_case token (not a sentence), and &lt;code&gt;confidence&lt;/code&gt; is 0–100. That grammar is validated in Go. The model can hallucinate a whole paragraph of reasoning and none of it reaches the report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A second, &lt;em&gt;different&lt;/em&gt; model has to agree.&lt;/strong&gt; When the first model proposes "refuted" with confidence ≥ 75, a separate verifier model gets called to assess the same finding independently. The refutation only stands if the verifier also says "refuted" at ≥ 75. And the verifier is prompted adversarially. Its job is to &lt;em&gt;stop a real bug from being waved away&lt;/em&gt;. This is the CLI version of the rule Synapse enforces on the server: a claim is only confirmed by a &lt;em&gt;different&lt;/em&gt; reviewer's sealed verdict. No confirming your own work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Nothing gets deleted.&lt;/strong&gt; A "refuted" finding is retain-and-mark: it stays in the report, it stays evidence-sealed, it still shows up in the compliance table. It's just exempt from the gate's exit code. The worst a wrong verdict can do is let one finding skip &lt;code&gt;--fail-on&lt;/code&gt;. It can never make a finding disappear.&lt;/p&gt;

&lt;p&gt;One more thing, because it matters for the cost: this is the &lt;em&gt;second&lt;/em&gt; layer. Before any model runs, a deterministic scope classifier already strips the obvious noise: test files, fixtures, that sort of thing. The model only gets the harder calls: is this sink actually attacker-controlled? Is that interpolated value a constant or user input? Model calls cost tokens, so they only run on production-scope, first-party source findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on
&lt;/h2&gt;

&lt;p&gt;It's opt-in. Four environment variables:&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;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_FP_TRIAGE_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;                  &lt;span class="c"&gt;# turn on AI false-positive triage&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_LLM_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:20128/v1  &lt;span class="c"&gt;# any OpenAI-compatible endpoint&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_FP_TRIAGE_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;proposer-model&amp;gt;        &lt;span class="c"&gt;# a cheap/fast model to propose&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_VERIFIER_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;different-model&amp;gt;         &lt;span class="c"&gt;# a DIFFERENT model to verify (two-model consensus)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SYNAPSE_LLM_BASE_URL&lt;/code&gt; is a plain OpenAI-style endpoint (&lt;code&gt;/v1/chat/completions&lt;/code&gt;). Run it locally with Ollama or vLLM, or point it at OpenAI. Doesn't matter. Add &lt;code&gt;SYNAPSE_LLM_API_KEY&lt;/code&gt; if yours needs a key.&lt;/li&gt;
&lt;li&gt;If you &lt;em&gt;don't&lt;/em&gt; set &lt;code&gt;SYNAPSE_VERIFIER_MODEL&lt;/code&gt; (or you set it to the same model), you get single-model mode. It still works, but the real gate is the second model being &lt;em&gt;different&lt;/em&gt;. Set it to the proposer and Synapse ignores it. That's not a verifier.&lt;/li&gt;
&lt;li&gt;The bar is 75, the same threshold every other AI claim in Synapse has to clear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then you run the exact same scan command as always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;synapse-cli scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I built a small service with &lt;strong&gt;both real bugs and deliberate false positives&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;payments/store.py&lt;/code&gt;: a SQL query built with &lt;code&gt;db.session.execute(f"... {account_id}")&lt;/code&gt; where &lt;code&gt;account_id&lt;/code&gt; comes straight from &lt;code&gt;request.args&lt;/code&gt;. That's a &lt;strong&gt;real SQL injection&lt;/strong&gt;. Same file has a &lt;code&gt;hashlib.md5(...)&lt;/code&gt; call used as a &lt;strong&gt;cache key for a static asset&lt;/strong&gt;. The weak-hash rule flags it, but it has nothing to do with security.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payments/settings.py&lt;/code&gt;: a hardcoded AWS access key. &lt;strong&gt;Real&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reports/ledger.py&lt;/code&gt;: another f-string SQL query, but the interpolated value is &lt;code&gt;PERIOD&lt;/code&gt;, a &lt;strong&gt;server-side constant&lt;/strong&gt;, not user input. The pattern matcher flags every f-string SQL, so this is a textbook &lt;strong&gt;high-severity false positive&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Before: no AI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;synapse-cli scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;span class="go"&gt;
Synapse SCA dogfood – payments-service
  vulnerabilities: 0
  findings (promoted): 7
    critical  risk  0.00  Hardcoded AWS access key id (payments/settings.py:4)
    high      risk  0.00  SQL execution uses dynamic string construction (payments/store.py:14)
    high      risk  0.00  Python SQLAlchemy/raw SQL uses dynamic string construction (payments/store.py:14)
    medium    risk  0.00  Weak hash: MD5 (payments/store.py:21)
    high      risk  0.00  SQL execution uses dynamic string construction (reports/ledger.py:13)
    high      risk  0.00  Python SQLAlchemy/raw SQL uses dynamic string construction (reports/ledger.py:13)
    high      risk  0.00  AWS access key ID (payments/settings.py:4)

synapse-cli: 6 finding(s) at or above high
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gate is red over &lt;strong&gt;6 findings at or above high&lt;/strong&gt;. Two of them (the SQL in &lt;code&gt;reports/ledger.py&lt;/code&gt;) are false alarms you'd have to triage by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  After: AI on, two models
&lt;/h3&gt;

&lt;p&gt;The proposer and the verifier are &lt;strong&gt;two different models&lt;/strong&gt; on my endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_FP_TRIAGE_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_FP_TRIAGE_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-5.4-mini      &lt;span class="c"&gt;# proposer&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SYNAPSE_VERIFIER_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gpt-5.4            &lt;span class="c"&gt;# verifier (a DIFFERENT model)&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;synapse-cli scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;span class="go"&gt;
synapse-cli: AI false-positive triage (gpt-5.4-mini, verified by gpt-5.4): critiqued 7 finding(s), 3 suspected false positive(s) held back from the gate

Synapse SCA dogfood – payments-service
&lt;/span&gt;&lt;span class="gp"&gt;  findings (promoted): 7            #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;- still 7, nothing deleted
&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;  compliance: Synapse AppSec Baseline v1.0 – 3/7 controls passing
    [FAIL] SAB-INJ-1  No injection weaknesses ...
           - SQL ... (payments/store.py:14)
&lt;/span&gt;&lt;span class="gp"&gt;           - SQL ... (reports/ledger.py:13)     #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;- the FP still shows &lt;span class="k"&gt;in &lt;/span&gt;compliance
&lt;span class="c"&gt;    ...
&lt;/span&gt;&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;synapse-cli: 2 suspected false positive(s) at or above high held back from the gate by AI triage (reported with a verdict;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;not deleted&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;synapse-cli: 4 finding(s) at or above high
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="go"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read those lines closely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;critiqued 7 finding(s), 3 suspected false positive(s) held back&lt;/code&gt;: the AI looked at 7 findings and proposed 3 as false.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2 ... at or above high held back ... (reported with a verdict; not deleted)&lt;/code&gt;: two of those were high-severity, held back from the gate but &lt;strong&gt;not deleted&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4 finding(s) at or above high&lt;/code&gt;: the 4 real bugs still count. Gate is still &lt;strong&gt;red&lt;/strong&gt; (&lt;code&gt;exit 1&lt;/code&gt;). The AI couldn't bury a real bug.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;findings (promoted): 7&lt;/code&gt; is unchanged, and the false positives are still sitting in the compliance table. Nothing left the report.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verdict by verdict
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. With &lt;code&gt;--json&lt;/code&gt;, every finding carries the AI's verdict next to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[critical] Hardcoded AWS access key id (settings.py:4)
    verdict=sound    confidence=98   suspected_fp=false   driver=hardcoded_secret
[high]     SQL execution dynamic string (store.py:14)
    verdict=sound    confidence=99   suspected_fp=false   driver=confirmed_by_review
[high]     SQLAlchemy raw SQL dynamic (store.py:14)
    verdict=sound    confidence=99   suspected_fp=false   driver=dynamic_string_sql
[high]     AWS access key ID (settings.py:4)
    verdict=sound    confidence=99   suspected_fp=false   driver=literal
[medium]   Weak hash: MD5 (store.py:21)
    verdict=refuted  confidence=96   suspected_fp=true    driver=unspecified_refutation
[high]     SQL execution dynamic string (ledger.py:13)
    verdict=refuted  confidence=99   suspected_fp=true    driver=constant_literal
[high]     SQLAlchemy raw SQL dynamic (ledger.py:13)
    verdict=refuted  confidence=98   suspected_fp=true    driver=constant_literal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the &lt;code&gt;driver&lt;/code&gt; column. For &lt;code&gt;reports/ledger.py&lt;/code&gt; the model worked out on its own that the interpolated value is a &lt;code&gt;constant_literal&lt;/code&gt;, a constant, not input. For &lt;code&gt;store.py&lt;/code&gt; it's &lt;code&gt;sound&lt;/code&gt;: real bug, left alone. And &lt;code&gt;suspected_fp=true&lt;/code&gt; is the only thing that exempts a finding from the gate. The key detail: because I configured a &lt;em&gt;different&lt;/em&gt; verifier (the log literally says &lt;code&gt;verified by gpt-5.4&lt;/code&gt;), a finding only gets &lt;code&gt;suspected_fp=true&lt;/code&gt; when &lt;strong&gt;both&lt;/strong&gt; models refute it at ≥ 75. Drop the verifier and these exact three refutations still have to clear a second model to stand.&lt;/p&gt;

&lt;p&gt;End result: you're left with 4 real problems to fix instead of 6-with-2-fake mixed in. And more importantly, not one real bug got dimmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But what if the AI is wrong?"
&lt;/h2&gt;

&lt;p&gt;Right question. The whole design is built around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI wrongly refutes a real bug?&lt;/strong&gt; The verifier is a different model, prompted to push back, and it has to agree before the refutation counts. If the verifier disagrees, is unsure, or errors out, the finding &lt;strong&gt;still counts toward the gate&lt;/strong&gt; (fail-safe). Even if both models are wrong, the finding is still sitting in the report with its verdict attached. You can see it, it didn't vanish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model times out / endpoint dies?&lt;/strong&gt; That critique becomes "uncertain" and the finding gates normally. Triage is best-effort and &lt;strong&gt;never fails the scan&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model tries to smuggle prose into the report?&lt;/strong&gt; &lt;code&gt;driver&lt;/code&gt; is forced into a closed token grammar, &lt;code&gt;verdict&lt;/code&gt; has to be from a closed vocabulary, anything else is rejected. No LLM gets prose into the report path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put another way: the worst a bad verdict can do is exempt one finding from the exit code while it stays visible in the report. Not hide a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into CI
&lt;/h2&gt;

&lt;p&gt;Nothing about the workflow from the last post changes. Add four environment variables to the scan step and you're done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synapse scan (SARIF + gate on high)&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_FP_TRIAGE_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_BASE_URL }}&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LLM_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_FP_TRIAGE_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-proposer-model"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_VERIFIER_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-verifier-model"&lt;/span&gt;   &lt;span class="c1"&gt;# different from the proposer&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;synapse-cli scan . --sarif --fail-on high &amp;gt; synapse.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate stays as deterministic as before. The pile of false alarms just stops eroding the team's trust in it. And since nothing gets deleted, the SARIF you push to Code scanning is still complete; the false positives are only marked, not dropped.&lt;/p&gt;

&lt;p&gt;One honest caveat: this is a noise filter, not a replacement for a human. It gets you looking at the 4 real problems instead of 6 muddled ones. You still have to fix those 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come contribute
&lt;/h2&gt;

&lt;p&gt;Synapse is open source (Apache-2.0): &lt;strong&gt;&lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;github.com/KKloudTarus/synapse-ce&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Star it if the "AI proposes, Go and a second model confirm" direction is one you want to follow.&lt;/li&gt;
&lt;li&gt;Run it against your own LLM endpoint and tell me what happened: which model refutes well, which one likes to make things up. Feedback from a real model is the most useful kind.&lt;/li&gt;
&lt;li&gt;The verifier's adversarial prompt, the &lt;code&gt;driver&lt;/code&gt; vocabulary, the consensus threshold: it all lives in &lt;code&gt;internal/usecase/fptriage&lt;/code&gt;, and it's short and easy to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you turn this on in your own repo, drop a comment with how many false positives the AI held back on the first run, and whether it ever came close to refuting something that turned out to be real. I'm curious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>contributorswanted</category>
      <category>go</category>
    </item>
    <item>
      <title>Synapse Feature Demo: Deterministic Software Composition Analysis in Action</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:49:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/synapse-feature-demo-deterministic-software-composition-analysis-in-action-el6</link>
      <guid>https://dev.to/aws-builders/synapse-feature-demo-deterministic-software-composition-analysis-in-action-el6</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I've just added a short demo video to the Synapse project to showcase how the platform performs deterministic Software Composition Analysis (SCA) from start to finish.&lt;/p&gt;

&lt;p&gt;In this demo, you'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Importing a project for analysis&lt;/li&gt;
&lt;li&gt;SBOM generation&lt;/li&gt;
&lt;li&gt;Dependency and vulnerability analysis&lt;/li&gt;
&lt;li&gt;Evidence collection&lt;/li&gt;
&lt;li&gt;Findings presented through the web interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of Synapse is to provide a deterministic-first approach to application security, where every finding is backed by reproducible evidence rather than opaque scanning results.&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User experience&lt;/li&gt;
&lt;li&gt;Scan workflow&lt;/li&gt;
&lt;li&gt;Dashboard design&lt;/li&gt;
&lt;li&gt;Missing capabilities&lt;/li&gt;
&lt;li&gt;Features you'd like to see next&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're interested in application security, DevSecOps, or open-source security tooling, your feedback would be greatly appreciated.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;https://github.com/KKloudTarus/synapse-ce&lt;/a&gt;&lt;br&gt;
Youtube: &lt;a href="https://www.youtube.com/watch?v=rUdt52em2QQ" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=rUdt52em2QQ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contributions are always welcome! Whether you're interested in backend development, frontend improvements, documentation, parser support, or security research, feel free to open an issue or submit a pull request.&lt;/p&gt;

&lt;p&gt;Thank you for taking the time to watch the demo, and I hope it gives you a better idea of where Synapse is heading.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>contributorswanted</category>
    </item>
    <item>
      <title>Block the merge when a PR ships a vulnerability: a CI security gate with Synapse</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Wed, 08 Jul 2026 16:07:09 +0000</pubDate>
      <link>https://dev.to/aws-builders/block-the-merge-when-a-pr-ships-a-vulnerability-a-ci-security-gate-with-synapse-33bn</link>
      <guid>https://dev.to/aws-builders/block-the-merge-when-a-pr-ships-a-vulnerability-a-ci-security-gate-with-synapse-33bn</guid>
      <description>&lt;p&gt;I have a simple rule when I work with a team: if a vulnerability makes it into &lt;code&gt;main&lt;/code&gt;, the process already failed somewhere upstream. The right place to catch it is on the pull request, before anyone clicks merge.&lt;/p&gt;

&lt;p&gt;This post is how I built exactly that gate with &lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;Synapse&lt;/a&gt;, an open source security scanner written in Go. The end result: open a PR that introduces a vulnerability and CI goes red, a comment lands on the PR telling you what to fix and how, the results show up in the GitHub Code scanning tab, and the merge button stays locked until the issue is dealt with.&lt;/p&gt;

&lt;p&gt;Everything below actually runs. There is a public demo repo linked at the end so you can see it, not just read about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why another tool
&lt;/h2&gt;

&lt;p&gt;Fair question. Trivy, Grype and OSV-Scanner are all good and I still use them.&lt;/p&gt;

&lt;p&gt;What is different about Synapse is not "it finds more CVEs". Detection is basically a commodity now, since every tool pulls from the same handful of data sources. The difference is the governance layer around a finding: a hash chained chain of custody so results cannot be edited quietly, a separation between who proposes and who confirms for the AI assisted parts, and a report path with no LLM in it. In short, it is built for the case where you have to prove a result, not just print one.&lt;/p&gt;

&lt;p&gt;But this post is not about philosophy. It is about one concrete job: blocking the merge.&lt;/p&gt;

&lt;p&gt;One thing I like: &lt;code&gt;synapse-cli&lt;/code&gt; runs with no database and no server. It is a single static Go binary. It shells out to &lt;code&gt;syft&lt;/code&gt; to build the SBOM and to &lt;code&gt;grype&lt;/code&gt; as one detection source, and both of those are single static binaries too. That is the same footprint you already know from running Trivy in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;Three binaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;synapse-cli&lt;/code&gt; (built from source, see the workflow below)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;syft&lt;/code&gt; (builds the SBOM)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grype&lt;/code&gt; (one offline detection source)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No Postgres, no external service. If the runner has network access it also uses OSV.dev as a second source. If you are air gapped, add the &lt;code&gt;--offline&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;The gate itself is one flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;synapse-cli scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns a non zero exit code if there is any finding at or above high, and the important part is that it counts across every kind: dependency vulnerabilities (SCA), issues in your own source code (SAST), hardcoded secrets, and misconfigurations in Dockerfiles, Kubernetes, Helm and Terraform. A leaked API key in the code blocks the merge just like a CVE does.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;--sarif&lt;/code&gt; to emit a SARIF 2.1.0 report for the GitHub Code scanning tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;synapse-cli scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--sarif&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; synapse.sarif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SARIF is written to stdout before the gate sets the exit code, so the file is complete even when the command returns 1. A single run both annotates the PR and blocks the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full workflow
&lt;/h2&gt;

&lt;p&gt;Here is the &lt;code&gt;.github/workflows/synapse.yml&lt;/code&gt; I use in the demo repo. It runs the scan, gates on high, uploads SARIF, and posts a remediation report on the PR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synapse Security Scan&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;   &lt;span class="c1"&gt;# to upload SARIF&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;      &lt;span class="c1"&gt;# to comment on the PR&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;go-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.26.4'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install syft + grype&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p "$HOME/.local/bin"&lt;/span&gt;
          &lt;span class="s"&gt;echo "$HOME/.local/bin" &amp;gt;&amp;gt; "$GITHUB_PATH"&lt;/span&gt;
          &lt;span class="s"&gt;curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh  | sh -s -- -b "$HOME/.local/bin"&lt;/span&gt;
          &lt;span class="s"&gt;curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b "$HOME/.local/bin"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build synapse-cli&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GOTOOLCHAIN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git clone --depth 1 https://github.com/KKloudTarus/synapse-ce.git /tmp/synapse-ce&lt;/span&gt;
          &lt;span class="s"&gt;cd /tmp/synapse-ce&lt;/span&gt;
          &lt;span class="s"&gt;go build -o "$HOME/.local/bin/synapse-cli" ./cmd/synapse-cli&lt;/span&gt;

      &lt;span class="c1"&gt;# The gate. The JSON result feeds both the exit code and the PR report.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synapse scan (gate on high)&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scan&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_MISCONFIG_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_SAST_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_SECRET_SCAN_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;synapse-cli scan . --json --fail-on high &amp;gt; synapse.json&lt;/span&gt;

      &lt;span class="c1"&gt;# SARIF for the Code scanning tab. The gate already ran, so ignore the exit here.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Synapse scan (SARIF for code scanning)&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_MISCONFIG_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_SAST_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
          &lt;span class="na"&gt;SYNAPSE_SECRET_SCAN_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;synapse-cli scan . --sarif &amp;gt; synapse.sarif || &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF to code scanning&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;synapse.sarif&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post report on the PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always() &amp;amp;&amp;amp; github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
          &lt;span class="na"&gt;PR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;GATE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.scan.outcome }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;crit=$(jq '[.findings[]|select(.Severity=="critical")]|length' synapse.json)&lt;/span&gt;
          &lt;span class="s"&gt;high=$(jq '[.findings[]|select(.Severity=="high")]|length' synapse.json)&lt;/span&gt;
          &lt;span class="s"&gt;med=$(jq '[.findings[]|select(.Severity=="medium")]|length' synapse.json)&lt;/span&gt;
          &lt;span class="s"&gt;low=$(jq '[.findings[]|select(.Severity=="low" or .Severity=="info")]|length' synapse.json)&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;echo "## Synapse security scan"&lt;/span&gt;
            &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;if [ "$GATE" = "failure" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "**Result: FAILED.** Findings at or above high severity. The merge is blocked until they are resolved."&lt;/span&gt;
            &lt;span class="s"&gt;else&lt;/span&gt;
              &lt;span class="s"&gt;echo "**Result: PASSED.** No findings at or above high severity."&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;echo "| Severity | Count |"&lt;/span&gt;
            &lt;span class="s"&gt;echo "|---|---|"&lt;/span&gt;
            &lt;span class="s"&gt;echo "| Critical | $crit |"&lt;/span&gt;
            &lt;span class="s"&gt;echo "| High | $high |"&lt;/span&gt;
            &lt;span class="s"&gt;echo "| Medium | $med |"&lt;/span&gt;
            &lt;span class="s"&gt;echo "| Low | $low |"&lt;/span&gt;
            &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;if [ "$(jq '[.vulnerabilities[].Component]|unique|length' synapse.json)" -gt 0 ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "### Dependencies to upgrade"&lt;/span&gt;
              &lt;span class="s"&gt;echo&lt;/span&gt;
              &lt;span class="s"&gt;echo "Edit the manifest and bump each package to at least the version in the \"Upgrade to\" column."&lt;/span&gt;
              &lt;span class="s"&gt;echo&lt;/span&gt;
              &lt;span class="s"&gt;echo "| Package | Current | Upgrade to | CVEs | Top severity |"&lt;/span&gt;
              &lt;span class="s"&gt;echo "|---|---|---|---|---|"&lt;/span&gt;
              &lt;span class="s"&gt;jq -r '&lt;/span&gt;
                &lt;span class="s"&gt;.vulnerabilities | group_by(.Component)&lt;/span&gt;
                &lt;span class="s"&gt;| map({comp:.[0].Component, ver:.[0].Version,&lt;/span&gt;
                       &lt;span class="s"&gt;fix:((map(.FixedVersion)|map(select(.!=null and .!=""))|max) // "no fix yet"),&lt;/span&gt;
                       &lt;span class="s"&gt;n:length,&lt;/span&gt;
                       &lt;span class="s"&gt;sev:(if any(.[];.Severity=="critical") then "critical"&lt;/span&gt;
                            &lt;span class="s"&gt;elif any(.[];.Severity=="high") then "high"&lt;/span&gt;
                            &lt;span class="s"&gt;elif any(.[];.Severity=="medium") then "medium" else "low" end)})&lt;/span&gt;
                &lt;span class="s"&gt;| sort_by(.n) | reverse&lt;/span&gt;
                &lt;span class="s"&gt;| .[] | "| \(.comp) | \(.ver) | &amp;gt;= \(.fix) | \(.n) | \(.sev) |"' synapse.json&lt;/span&gt;
              &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;if [ "$(jq '[.findings[]|select(.Kind!="sca")]|length' synapse.json)" -gt 0 ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "### Code and config"&lt;/span&gt;
              &lt;span class="s"&gt;echo&lt;/span&gt;
              &lt;span class="s"&gt;jq -r '.findings[] | select(.Kind != "sca")&lt;/span&gt;
                &lt;span class="s"&gt;| "- **\(.Title)** [\(.Severity)] `\(.DedupKey|split(":")[1])`\n  \(.Description|split("\n")[0])"' synapse.json&lt;/span&gt;
              &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;echo "_Per line annotations live in the repository Code scanning tab._"&lt;/span&gt;
          &lt;span class="s"&gt;} &amp;gt; comment.md&lt;/span&gt;
          &lt;span class="s"&gt;gh pr comment "$PR" --edit-last --body-file comment.md || gh pr comment "$PR" --body-file comment.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The gate step (&lt;code&gt;id: scan&lt;/code&gt;) has no &lt;code&gt;continue-on-error&lt;/code&gt;, so when it fails the whole job fails. That is what turns CI red.&lt;/li&gt;
&lt;li&gt;The upload and comment steps use &lt;code&gt;if: always()&lt;/code&gt;, so they still run after the gate fails. That is how you get both the merge block and the annotations plus the comment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security-events: write&lt;/code&gt; is required for &lt;code&gt;upload-sarif&lt;/code&gt;, and &lt;code&gt;pull-requests: write&lt;/code&gt; is required to post the comment.&lt;/li&gt;
&lt;li&gt;The report is built from &lt;code&gt;synapse-cli scan --json&lt;/code&gt;, which carries the fixed version, EPSS, CVSS and a remediation description per finding. That is what lets the comment say "upgrade to X" and "here is how to fix it", not just "there is a problem".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actually blocking the merge
&lt;/h2&gt;

&lt;p&gt;The CLI only produces a pass or fail signal. Blocking the merge is GitHub branch protection using that check as a required status.&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;# require the "scan" check to pass before merging into main&lt;/span&gt;
&lt;span class="c"&gt;# enforce_admins=true so even an admin cannot bypass it&lt;/span&gt;
gh api &lt;span class="nt"&gt;-X&lt;/span&gt; PUT repos/OWNER/REPO/branches/main/protection &lt;span class="nt"&gt;--input&lt;/span&gt; - &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;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "required_status_checks": { "strict": false, "contexts": ["scan"] },
  "enforce_admins": true,
  "required_pull_request_reviews": null,
  "restrictions": null
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, any PR where the &lt;code&gt;scan&lt;/code&gt; job fails cannot be merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;I set up a demo repo: a clean &lt;code&gt;main&lt;/code&gt; (CI green), then a PR that adds a "payments service" with three deliberate problems. A Dockerfile that runs as root and pulls a script from an external URL, a function that uses MD5 to build a token, and a &lt;code&gt;requirements.txt&lt;/code&gt; pinned to old Django, PyYAML and requests full of CVEs.&lt;/p&gt;

&lt;p&gt;That PR produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gate &lt;strong&gt;FAILED&lt;/strong&gt;: findings at and above high, so the job went red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;51 alerts&lt;/strong&gt; in the Code scanning tab, each with a file location: most on &lt;code&gt;requirements.txt&lt;/code&gt;, several on &lt;code&gt;Dockerfile&lt;/code&gt;, and one on &lt;code&gt;app/payments.py:6&lt;/code&gt; where the MD5 call is.&lt;/li&gt;
&lt;li&gt;A single PR comment that reads like a real report:

&lt;ul&gt;
&lt;li&gt;a severity summary table&lt;/li&gt;
&lt;li&gt;a "Dependencies to upgrade" table (for example &lt;code&gt;django 2.2.0&lt;/code&gt; needs &lt;code&gt;&amp;gt;= 5.2.8&lt;/code&gt;, &lt;code&gt;requests 2.19.0&lt;/code&gt; needs &lt;code&gt;&amp;gt;= 2.33.0&lt;/code&gt;), with a collapsible list of every CVE and its fixed version sorted by severity and EPSS&lt;/li&gt;
&lt;li&gt;a "Code and config" section with each issue as &lt;code&gt;file:line&lt;/code&gt;, its rule id, and a one line remediation ("MD5 is broken, use SHA-256 or a KDF", "add a non-root USER before the entrypoint", and so on)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The merge blocked. I ran &lt;code&gt;gh pr merge&lt;/code&gt; for real and GitHub answered: &lt;code&gt;the base branch policy prohibits the merge&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One run caught all four kinds: vulnerable dependencies, weak source code, and an unsafe Docker config, all rolled into a single pass or fail decision, with the fix spelled out on the PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SARIF gotcha worth knowing
&lt;/h2&gt;

&lt;p&gt;This is the part I most wanted to share, because it only showed up when I ran the real thing.&lt;/p&gt;

&lt;p&gt;On the first run the SARIF upload failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code Scanning could not process the submitted SARIF file:
expected a physical location
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns out GitHub code scanning rejects the entire file if any result has a location that carries only a &lt;code&gt;logicalLocation&lt;/code&gt; and no &lt;code&gt;physicalLocation&lt;/code&gt;. The SCA findings were pointing at a "module" (something like &lt;code&gt;django@2.2.0&lt;/code&gt;) with no concrete file, so they were all rejected.&lt;/p&gt;

&lt;p&gt;The correct fix, which is also what Trivy does, is to point each SCA finding at the manifest that declares it, for example &lt;code&gt;requirements.txt&lt;/code&gt;. If the manifest is unknown, leave the result with no location at all, since GitHub still accepts that as a repo level alert. Never emit a location that is logical only.&lt;/p&gt;

&lt;p&gt;The lesson if you generate SARIF yourself: every result you want GitHub to annotate on a line needs a &lt;code&gt;physicalLocation&lt;/code&gt;, and &lt;code&gt;artifactLocation.uri&lt;/code&gt; must be a repo relative path with no leading slash. This is handled inside &lt;code&gt;synapse-cli&lt;/code&gt; now, but if you write your own exporter, keep it in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares to Trivy
&lt;/h2&gt;

&lt;p&gt;I ran both side by side on the same large real repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unique CVEs: Synapse 261, Trivy 239, with 235 in common. The four CVEs only Trivy reported were all one package where the two tools resolved a different version, not a database gap.&lt;/li&gt;
&lt;li&gt;License detection: Synapse attached a license to 1443 packages, Trivy to 1394.&lt;/li&gt;
&lt;li&gt;Misconfig: Trivy has more low severity rules in raw count (270 to 208), but both now cover Dockerfile, Kubernetes, Helm and Terraform.&lt;/li&gt;
&lt;li&gt;Plus SAST and secret scanning, which &lt;code&gt;trivy fs&lt;/code&gt; does not do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short version: detection is at parity with a few areas ahead, and Synapse adds the governance layer that the plain scanners do not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come and contribute
&lt;/h2&gt;

&lt;p&gt;Synapse is open source under Apache-2.0 at &lt;strong&gt;&lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;github.com/KKloudTarus/synapse-ce&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you find it useful, I would love for you to jump in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop a star if the project looks worth following. It helps other people find it.&lt;/li&gt;
&lt;li&gt;Run it in your own pipeline and open an issue about what breaks or what is missing. Feedback from a real CI run is the most valuable kind.&lt;/li&gt;
&lt;li&gt;Pick up an issue labeled &lt;code&gt;good first issue&lt;/code&gt;, or add a lockfile parser, a SAST or misconfig rule, or a new ecosystem for SCA. The architecture is cleanly layered, so adding a tool does not touch the core.&lt;/li&gt;
&lt;li&gt;There is a &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; and a full docs set to get you started.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can fork the demo repo and play with it right away: &lt;strong&gt;&lt;a href="https://github.com/nghiadaulau/synapse-ci-demo" rel="noopener noreferrer"&gt;github.com/nghiadaulau/synapse-ci-demo&lt;/a&gt;&lt;/strong&gt; (open &lt;a href="https://github.com/nghiadaulau/synapse-ci-demo/pull/1" rel="noopener noreferrer"&gt;PR #1&lt;/a&gt; to see the blocked merge and the report).&lt;/p&gt;

&lt;p&gt;If you wire this gate into your own project, drop a comment below and tell me what it caught on the first run. My guess is there will be a surprise.&lt;/p&gt;

</description>
      <category>security</category>
      <category>contributorswanted</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Introducing Synapse: a deterministic-first, open-source SCA and evidence platform</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Mon, 06 Jul 2026 18:48:33 +0000</pubDate>
      <link>https://dev.to/nghiadaulau/introducing-synapse-a-deterministic-first-open-source-sca-and-evidence-platform-1k0i</link>
      <guid>https://dev.to/nghiadaulau/introducing-synapse-a-deterministic-first-open-source-sca-and-evidence-platform-1k0i</guid>
      <description>&lt;p&gt;We just open-sourced &lt;strong&gt;Synapse&lt;/strong&gt;, a governed control plane for software composition analysis,&lt;br&gt;
recon, evidence, and reporting. It is built for people who have to scan a dependency tree,&lt;br&gt;
prove what they found, and hand over a report that holds up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Site: &lt;a href="https://synapse.kkloudtarus.net/" rel="noopener noreferrer"&gt;https://synapse.kkloudtarus.net/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/KKloudTarus/synapse-ce" rel="noopener noreferrer"&gt;https://github.com/KKloudTarus/synapse-ce&lt;/a&gt; (Apache-2.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why we built it
&lt;/h2&gt;

&lt;p&gt;The usual workflow is fragmented. One tool for the SBOM, another for vulnerabilities, a&lt;br&gt;
spreadsheet for licenses, a folder of screenshots for evidence, and a report you assemble by&lt;br&gt;
hand. Nothing is reproducible, and when a client asks "how do you know this is real," the&lt;br&gt;
answer lives in someone's memory. Adding an LLM that writes your findings only makes that&lt;br&gt;
worse. We wanted the opposite: fast, but provable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Synapse runs the assessment lifecycle behind one control plane, in Go, clean architecture. A&lt;br&gt;
few ideas hold it together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic-first.&lt;/strong&gt; Scanning, matching, license classification, and reporting are pure,
reproducible Go. There is no model in the report path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope-gated execution.&lt;/strong&gt; Every engagement carries a scope and an authorization window,
enforced server-side before any tool runs. Tools run via argument arrays, never a shell string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper-evident evidence.&lt;/strong&gt; Every artifact is hash-chained and append-only. A broken chain
blocks the report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded automation.&lt;/strong&gt; The optional AI layer only ever proposes. A distinct verifier or a
human confirms. The agent can never confirm its own claim.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does today: SBOM across 15+ ecosystems, multi-source vulnerability detection with&lt;br&gt;
risk-based prioritization (KEV, then EPSS, then CVSS), license compliance, reachability, and&lt;br&gt;
deterministic reports in CycloneDX, SPDX, SARIF, and OpenVEX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/KKloudTarus/synapse-ce.git
&lt;span class="nb"&gt;cd &lt;/span&gt;synapse-ce
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; deploy/docker-compose.full.yml up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="c"&gt;# open http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or gate CI on real risk: &lt;code&gt;./bin/synapse-cli scan . --fail-on high&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  We are looking for contributors
&lt;/h2&gt;

&lt;p&gt;Synapse is open source because this kind of tool gets better with more eyes on it. Good places&lt;br&gt;
to jump in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New ecosystems and lockfile parsers&lt;/li&gt;
&lt;li&gt;Detection quality and false-positive reduction&lt;/li&gt;
&lt;li&gt;Reachability and SAST coverage&lt;/li&gt;
&lt;li&gt;Reporting and standards (OpenVEX, CSAF, SARIF)&lt;/li&gt;
&lt;li&gt;The dashboard (Vite, React, Tailwind) and the docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the &lt;a href="https://github.com/KKloudTarus/synapse-ce/tree/main/docs/guide" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://github.com/KKloudTarus/synapse-ce/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING&lt;/a&gt;, run it&lt;br&gt;
locally, then open an issue or send a pull request.&lt;/p&gt;

&lt;p&gt;One note: Synapse is for authorized security testing only. It validates scope data but cannot&lt;br&gt;
verify your legal authorization, so use it only against systems you are allowed to test.&lt;/p&gt;

&lt;p&gt;If it looks useful, a star helps other people find it, and if you want to build with us, open&lt;br&gt;
an issue and say hello.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>contributorswanted</category>
    </item>
    <item>
      <title>Building a Production Serverless URL Shortener on AWS — 21 Articles, Every Test Run for Real</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Tue, 26 May 2026 00:40:26 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-production-serverless-url-shortener-on-aws-21-articles-every-test-run-for-real-5905</link>
      <guid>https://dev.to/aws-builders/building-a-production-serverless-url-shortener-on-aws-21-articles-every-test-run-for-real-5905</guid>
      <description>&lt;p&gt;Most serverless tutorials stop at a hello-world Lambda behind API Gateway. I wanted the opposite: build one real product end to end, run every command on real AWS, and write&lt;br&gt;
down the actual numbers — including the ones that didn't go as planned.&lt;/p&gt;

&lt;p&gt;The result is a 21-article series that builds a &lt;strong&gt;URL shortener with realtime click analytics&lt;/strong&gt;, fully serverless, and takes it to production: auth, multi-tenancy, an event&lt;br&gt;
pipeline, realtime push, observability, CI/CD with canary deploys, a cost breakdown, and a load test. Code:&lt;br&gt;
&lt;a href="https://github.com/nghiadaulau/serverless-url-shortener-aws" rel="noopener noreferrer"&gt;github.com/nghiadaulau/serverless-url-shortener-aws&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       ┌──────── Cognito (JWT) ─────────┐
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Browser ─HTTPS──▶ API Gateway (HTTP API)        │ auth&lt;br&gt;
     │         │     │                           │&lt;br&gt;
POST /links ───┼─────▶ create ──▶ DynamoDB (single-table)&lt;br&gt;
GET /{code} ───┼─────▶ resolve ─┬─▶ 301 redirect&lt;br&gt;
     │         │                └─▶ EventBridge ─▶ SQS(+DLQ) ─▶ aggregator&lt;br&gt;
     │         │                                                  │ count + push&lt;br&gt;
     └─WebSocket───────────────────────────────────◀── realtime dashboard &lt;/p&gt;

&lt;p&gt;Step Functions (link moderation) · X-Ray · CloudWatch alarms&lt;br&gt;
SAM (IaC) · GitHub Actions CI · CodeDeploy canary + rollback&lt;/p&gt;

&lt;p&gt;Nothing here is a server you keep running. Idle cost is effectively zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few findings that surprised me (all measured, not quoted)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory is CPU.&lt;/strong&gt; The same CPU-bound work at 128 MB vs 1769 MB:&lt;/p&gt;

&lt;p&gt;128 MB:  2594 ms&lt;br&gt;
1769 MB:   88 ms   → ~29x faster, and ~2.25x cheaper (memory × billed time)&lt;/p&gt;

&lt;p&gt;At 128 MB a Lambda is CPU-starved, not memory-starved (&lt;code&gt;Max Memory Used&lt;/code&gt; stayed ~82 MB in both). The same lever cut a cold start from &lt;strong&gt;1513 ms to 295 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concurrency ceiling is a quota, not your code.&lt;/strong&gt; Load testing with k6 at ~554 req/s:&lt;/p&gt;

&lt;p&gt;Total requests: 27741&lt;br&gt;
301 (success):   304  (1.1%)&lt;br&gt;
503 (overload):   25  (0.1%)&lt;br&gt;
429 (throttled): ~98.8%&lt;br&gt;
ConcurrentExecutions (max): 10&lt;/p&gt;

&lt;p&gt;This account had a reduced Lambda concurrency limit of 10, and an API Gateway rate throttle I'd set deliberately. So under load the system &lt;em&gt;sheds&lt;/em&gt; traffic in two layers (429&lt;br&gt;
at the gateway, 503 at Lambda) and stays fast for what it serves — graceful degradation, not a crash. The fix isn't optimizing code; it's raising the quotas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency without a framework.&lt;/strong&gt; Click events are delivered at-least-once, so naive counting double-counts. One DynamoDB &lt;code&gt;TransactWriteItems&lt;/code&gt; bumps the counters &lt;em&gt;and&lt;/em&gt;&lt;br&gt;
writes a &lt;code&gt;CLICK#&amp;lt;eventId&amp;gt;&lt;/code&gt; marker with &lt;code&gt;attribute_not_exists&lt;/code&gt; — a duplicate cancels the whole transaction, so it's counted exactly once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bill.&lt;/strong&gt; Building and testing the entire thing — API, database, auth, event bus, queues, realtime, state machine, observability — cost essentially &lt;strong&gt;$0&lt;/strong&gt;, all within&lt;br&gt;
free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the series covers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foundations&lt;/strong&gt; — SAM, the Lambda execution lifecycle, cold starts, arm64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core&lt;/strong&gt; — HTTP API vs REST API, DynamoDB single-table design, GSIs and sparse indexes, conditional writes, atomic counters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; — Cognito + JWT authorizer, multi-tenancy, blocking IDOR at the data layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-driven&lt;/strong&gt; — EventBridge, SQS + DLQ, idempotency, partial batch failure, WebSocket realtime push, Step Functions + the saga pattern&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt; — Powertools + X-Ray, CloudWatch alarms and SLOs, cold-start optimization, IAM least-privilege, throttling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations&lt;/strong&gt; — CI/CD with canary deploys and rollback, a real cost breakdown, load testing with k6, a Well-Architected review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article is grounded in the official AWS docs and ends with cleanup so you never get a surprise bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it
&lt;/h2&gt;

&lt;p&gt;Full series (English): &lt;strong&gt;&lt;a href="https://kkloudtarus.net/en/blog/what-is-serverless-when-to-use" rel="noopener noreferrer"&gt;https://kkloudtarus.net/en/blog/what-is-serverless-when-to-use&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Code: &lt;strong&gt;&lt;a href="https://github.com/nghiadaulau/serverless-url-shortener-aws" rel="noopener noreferrer"&gt;https://github.com/nghiadaulau/serverless-url-shortener-aws&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're learning serverless beyond hello-world, this is the path I wish I'd had. Questions and corrections welcome.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>lambda</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>eBPF From Scratch: from the eBPF VM to writing your own tools (tested on a live Cilium cluster)</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Sun, 24 May 2026 15:08:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/ebpf-from-scratch-from-the-ebpf-vm-to-writing-your-own-tools-tested-on-a-live-cilium-cluster-3373</link>
      <guid>https://dev.to/aws-builders/ebpf-from-scratch-from-the-ebpf-vm-to-writing-your-own-tools-tested-on-a-live-cilium-cluster-3373</guid>
      <description>&lt;p&gt;Right now, on one worker of a Kubernetes cluster I built, &lt;strong&gt;140 eBPF programs are running inside the Linux kernel&lt;/strong&gt; — routing every packet, controlling device access, collecting metrics. Nobody recompiled the kernel. Nobody loaded a module.&lt;/p&gt;

&lt;p&gt;That sentence is the whole reason I wrote &lt;strong&gt;eBPF From Scratch&lt;/strong&gt;: a free, 22-chapter series that takes you from &lt;em&gt;"what even is the eBPF virtual machine"&lt;/em&gt; all the way to &lt;em&gt;writing and loading your own eBPF programs&lt;/em&gt; — in C with libbpf + CO-RE, and from Go with cilium/ebpf.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read it free (English):&lt;/strong&gt; &lt;a href="https://kkloudtarus.net/en/blog/series/ebpf-from-scratch" rel="noopener noreferrer"&gt;https://kkloudtarus.net/en/blog/series/ebpf-from-scratch&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/nghiadaulau/ebpf-from-scratch" rel="noopener noreferrer"&gt;https://github.com/nghiadaulau/ebpf-from-scratch&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything is tested on real hardware&lt;/strong&gt; — a Kubernetes cluster running &lt;strong&gt;kernel 6.17&lt;/strong&gt; and &lt;strong&gt;Cilium 1.19&lt;/strong&gt; (kube-proxy-less, hundreds of BPF programs live) is the lab throughout. No hand-wavy diagrams; we dissect programs that are actually running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounded in official docs&lt;/strong&gt; — ebpf.io, kernel.org, libbpf, cilium. Claims are verified, not vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep-dive, not surface&lt;/strong&gt; — we go down to registers, the verifier's safety proofs, JIT, maps, and the exact lifecycle a program goes through.&lt;/li&gt;
&lt;li&gt;It's also fully bilingual (English + Vietnamese).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick taste — every eBPF concept is something you can &lt;em&gt;see&lt;/em&gt; on a real node:&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;bpftool prog show &lt;span class="nb"&gt;id &lt;/span&gt;2871
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2871: sched_cls  name tail_no_service_ipv4  tag fe7bcb57c001d434  gpl
    xlated 4920B  jited 2778B  memlock 8192B  map_ids 171,631
    btf_id 758
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;xlated&lt;/code&gt; = bytecode after the verifier accepted it. &lt;code&gt;jited&lt;/code&gt; = native machine code. &lt;code&gt;map_ids&lt;/code&gt; = how it keeps state. That's eBPF, not on a slide — running.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll learn (7 parts, 22 chapters)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part I — Foundations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The eBPF Virtual Machine: registers, instruction set, and bytecode&lt;/li&gt;
&lt;li&gt;The Verifier: why eBPF doesn't crash the kernel&lt;/li&gt;
&lt;li&gt;Maps: memory and the bridge to userspace&lt;/li&gt;
&lt;li&gt;Program types and hooks: where you attach, what you see&lt;/li&gt;
&lt;li&gt;BTF and CO-RE: compile once, run on every kernel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part II — Tracing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bpftrace from a one-liner to maps, counting and histograms&lt;/li&gt;
&lt;li&gt;uprobe, USDT, and inspecting a pod from the host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part III — Writing real tools&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;libbpf + CO-RE: writing an eBPF tool yourself (C)&lt;/li&gt;
&lt;li&gt;cilium/ebpf: loading eBPF from Go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part IV — Networking&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XDP: processing packets at the earliest point — writing a firewall&lt;/li&gt;
&lt;li&gt;tc/sched_cls and dissecting a live Cilium datapath&lt;/li&gt;
&lt;li&gt;Writing a tc program yourself: &lt;code&gt;__sk_buff&lt;/code&gt; and the tcx chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part V — Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LSM BPF: enforcing security right inside the kernel&lt;/li&gt;
&lt;li&gt;seccomp-bpf: filtering syscalls in every container&lt;/li&gt;
&lt;li&gt;The Tetragon way: from observe to enforce with &lt;code&gt;bpf_send_signal&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part VI — Observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU profiling with perf_event (the foundation of flame graphs)&lt;/li&gt;
&lt;li&gt;Off-CPU and scheduler latency&lt;/li&gt;
&lt;li&gt;Inside Hubble: from eBPF events to cluster-wide network flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part VII — Putting it together&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case study: a packet through Cilium's eBPF datapath&lt;/li&gt;
&lt;li&gt;Capstone: writing &lt;code&gt;connmon&lt;/code&gt;, a node-wide TCP connection monitor&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;Backend / platform / SRE / security folks who keep hearing "Cilium does that with eBPF" and want to actually understand — and &lt;em&gt;write&lt;/em&gt; — the thing. You'll want to be comfortable on the Linux command line; everything else is built up from zero.&lt;/p&gt;




&lt;p&gt;If you read any of it, I'd genuinely love feedback — what was clear, what wasn't. And if it's useful, a ⭐ on the &lt;a href="https://github.com/nghiadaulau/ebpf-from-scratch" rel="noopener noreferrer"&gt;repo&lt;/a&gt; helps a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here →&lt;/strong&gt; &lt;a href="https://kkloudtarus.net/en/blog/series/ebpf-from-scratch" rel="noopener noreferrer"&gt;https://kkloudtarus.net/en/blog/series/ebpf-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kernel</category>
    </item>
    <item>
      <title>Kubernetes From Scratch: build an HA cluster by hand (no kubeadm), then deep-dive every concept</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Sun, 24 May 2026 15:06:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/kubernetes-from-scratch-build-an-ha-cluster-by-hand-no-kubeadm-then-deep-dive-every-concept-bdl</link>
      <guid>https://dev.to/aws-builders/kubernetes-from-scratch-build-an-ha-cluster-by-hand-no-kubeadm-then-deep-dive-every-concept-bdl</guid>
      <description>&lt;p&gt;Most Kubernetes tutorials start with &lt;code&gt;kubeadm init&lt;/code&gt; or a managed cluster. You get a working cluster and almost no idea &lt;em&gt;why&lt;/em&gt; it works.&lt;/p&gt;

&lt;p&gt;So I did the opposite. &lt;strong&gt;Kubernetes From Scratch&lt;/strong&gt; builds a complete, highly-available cluster &lt;strong&gt;by hand — no kubeadm, no scripts&lt;/strong&gt; — starting from the very first TLS certificate, all the way to a real HA control plane. Then it uses &lt;em&gt;that&lt;/em&gt; cluster as a lab to &lt;strong&gt;deep-dive every concept in Kubernetes&lt;/strong&gt;: 73 chapters.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read it free (English):&lt;/strong&gt; &lt;a href="https://kkloudtarus.net/en/blog/series/kubernetes-from-scratch" rel="noopener noreferrer"&gt;https://kkloudtarus.net/en/blog/series/kubernetes-from-scratch&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Manifests &amp;amp; scripts:&lt;/strong&gt; &lt;a href="https://github.com/nghiadaulau/kubernetes-from-scratch" rel="noopener noreferrer"&gt;https://github.com/nghiadaulau/kubernetes-from-scratch&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You build it by hand.&lt;/strong&gt; cfssl-signed certs, etcd quorum, apiserver/controller-manager/scheduler wired up yourself, kubelet + containerd on the workers, pod networking with the CNI bridge + VPC routes, CoreDNS — then a smoke test of the whole thing running together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then it goes deep.&lt;/strong&gt; Once the cluster is up, every concept gets explained &lt;em&gt;from the inside&lt;/em&gt; and configured by hand — not just "apply this YAML".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested for real&lt;/strong&gt; on AWS EC2 with &lt;strong&gt;Kubernetes v1.36&lt;/strong&gt;, grounded in the official kubernetes.io docs.&lt;/li&gt;
&lt;li&gt;Fully bilingual (English + Vietnamese).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The journey
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 1 — Build the cluster from nothing (chapters 0–17)&lt;/strong&gt;&lt;br&gt;
PKI/TLS and &lt;em&gt;why a cluster needs so many certificates&lt;/em&gt; → etcd (quorum, Raft) → kube-apiserver and the request pipeline → controller-manager &amp;amp; scheduler (control loops, leader election) → HAProxy in front of 3 API servers → containerd/CRI → kubelet → kube-proxy → the Kubernetes network model → wiring pod networking by hand → CoreDNS → a full smoke test → and finally, &lt;strong&gt;the lifecycle of a request: from &lt;code&gt;kubectl apply&lt;/code&gt; to a running pod&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2 — Deep-dive every concept (chapters 18–72)&lt;/strong&gt;&lt;br&gt;
Using the cluster you built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workloads:&lt;/strong&gt; Pod lifecycle, init/sidecar containers, probes, Deployment/StatefulSet/DaemonSet/Job, requests/limits/QoS, PodDisruptionBudgets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling:&lt;/strong&gt; the scheduling framework, affinity/taints/tolerations, topology spread, priority &amp;amp; preemption, node-pressure eviction, HPA/VPA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; volumes, PV/PVC, StorageClass + dynamic provisioning (EBS CSI), VolumeSnapshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; why replace kube-proxy, migrating to &lt;strong&gt;kube-proxy-less Cilium (eBPF)&lt;/strong&gt;, NetworkPolicy, Ingress, Gateway API, LB IPAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; authentication → RBAC → ServiceAccounts &amp;amp; bound tokens, Pod Security Standards, seccomp/AppArmor/capabilities, Secrets hardening&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extending K8s:&lt;/strong&gt; CRDs, admission webhooks, the operator pattern, API aggregation, device plugins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations:&lt;/strong&gt; etcd backup &amp;amp; cert rotation, upgrades &amp;amp; version skew, logging, metrics/traces/APF, CEL admission policy, in-place pod resize, and a full teardown&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;Anyone who can &lt;em&gt;use&lt;/em&gt; Kubernetes but wants to truly &lt;em&gt;understand&lt;/em&gt; it — for the CKA/CKS, for debugging production with confidence, or just to finally kill the "it's magic" feeling. Comfortable-with-Linux is the only prerequisite.&lt;/p&gt;




&lt;p&gt;If this helps you, a ⭐ on the &lt;a href="https://github.com/nghiadaulau/kubernetes-from-scratch" rel="noopener noreferrer"&gt;repo&lt;/a&gt; means a lot — and I'd love to hear which chapter clicked for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start here →&lt;/strong&gt; &lt;a href="https://kkloudtarus.net/en/blog/series/kubernetes-from-scratch" rel="noopener noreferrer"&gt;https://kkloudtarus.net/en/blog/series/kubernetes-from-scratch&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Getting Started with EVS CLI: Manage Amazon Elastic VMware Service from the Command Line</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Wed, 06 Aug 2025 15:06:51 +0000</pubDate>
      <link>https://dev.to/aws-builders/getting-started-with-evs-cli-manage-amazon-elastic-vmware-service-from-the-command-line-5cif</link>
      <guid>https://dev.to/aws-builders/getting-started-with-evs-cli-manage-amazon-elastic-vmware-service-from-the-command-line-5cif</guid>
      <description>&lt;h1&gt;
  
  
  🚀 Getting Started with EVS CLI: Manage Amazon Elastic VMware Service from the Command Line
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon EVS is currently in &lt;strong&gt;public preview&lt;/strong&gt; and subject to change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Amazon Elastic VMware Service (EVS) is a fully managed service that allows you to run &lt;strong&gt;VMware Cloud Foundation (VCF)&lt;/strong&gt; directly on &lt;strong&gt;EC2 bare metal instances&lt;/strong&gt; inside your &lt;strong&gt;Amazon VPC&lt;/strong&gt;, without refactoring workloads or changing operational tools.&lt;/p&gt;

&lt;p&gt;With the launch of the &lt;strong&gt;EVS CLI&lt;/strong&gt;, you now have a powerful command-line interface to automate and manage your EVS infrastructure, ideal for scripting, DevOps pipelines, and quick access tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧰 What is EVS CLI?
&lt;/h2&gt;

&lt;p&gt;The EVS CLI is an AWS CLI-compatible tool to help manage your &lt;strong&gt;Amazon EVS environments&lt;/strong&gt; programmatically. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚧 Create or delete VCF environments&lt;/li&gt;
&lt;li&gt;🧱 Add or remove ESXi hosts&lt;/li&gt;
&lt;li&gt;📡 List VLANs and connected resources&lt;/li&gt;
&lt;li&gt;🏷️ Tag and organize environments&lt;/li&gt;
&lt;li&gt;📥 Retrieve environment information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It complements AWS Console and offers Infrastructure-as-Code (IaC) like capabilities directly in shell environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  📋 Available Commands
&lt;/h2&gt;

&lt;p&gt;Here's a quick overview of commands you can run with &lt;code&gt;evs&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create-environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provision a new VCF environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create-environment-host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add a new host to an existing EVS environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete-environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove an entire EVS environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete-environment-host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Remove a host from a deployed environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-environments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all EVS environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-environment-hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View hosts within an environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-environment-vlans&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View VLAN setup in an environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get-environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch detailed information of an EVS environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tag-resource&lt;/code&gt; / &lt;code&gt;untag-resource&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Manage tags on EVS resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list-tags-for-resource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display tags on a specific resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ⚙️ Creating Your First EVS Environment
&lt;/h2&gt;

&lt;p&gt;The most essential command is &lt;code&gt;create-environment&lt;/code&gt;. It provisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vCenter Server&lt;/li&gt;
&lt;li&gt;NSX Manager&lt;/li&gt;
&lt;li&gt;SDDC Manager&lt;/li&gt;
&lt;li&gt;ESXi hosts (min 4)&lt;/li&gt;
&lt;li&gt;VLANs: Management, vMotion, vSAN, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧪 Sample Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws evs create-environment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--environment-name&lt;/span&gt; &lt;span class="s2"&gt;"prod-vcf-env"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-0a12b345cdef67890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-access-subnet-id&lt;/span&gt; subnet-0123abcd4567ef890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vcf-version&lt;/span&gt; VCF-5.2.1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--terms-accepted&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--license-info&lt;/span&gt; file://license.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--initial-vlans&lt;/span&gt; file://vlans.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hosts&lt;/span&gt; file://hosts.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connectivity-info&lt;/span&gt; file://connectivity.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vcf-hostnames&lt;/span&gt; file://vcf-hostnames.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--site-id&lt;/span&gt; &lt;span class="s2"&gt;"SITE-123456"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 Note: Provisioning takes several hours. Host count must be 4 to 16, and all subnets must reside in a single Availability Zone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🛠️ Example: hosts.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "hostName": "esxi01.mycorp.local",
    "keyName": "evs-key",
    "instanceType": "i4i.metal"
  },
  {
    "hostName": "esxi02.mycorp.local",
    "keyName": "evs-key",
    "instanceType": "i4i.metal"
  },
  ...
]

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  🧹 Cleaning Up Resources
&lt;/h3&gt;

&lt;p&gt;Once you’re done, it’s critical to clean up properly to avoid ongoing charges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete a host:&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;aws evs delete-environment-host \
  --environment-id env-abc123456 \
  --host-name esxi01.mycorp.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ You must decommission the host in SDDC Manager UI before deleting it via CLI.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws evs delete-environment-host \
  --environment-id env-abc123456 \
  --host-name esxi01.mycorp.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delete the entire environment:&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;aws evs delete-environment \
  --environment-id env-abc123456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🛡️ Requirements Summary&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Minimum 4 hosts, maximum 16&lt;/li&gt;
&lt;li&gt;✅ VCF Version: VCF-5.2.1&lt;/li&gt;
&lt;li&gt;✅ Valid VCF and vSAN license keys from Broadcom&lt;/li&gt;
&lt;li&gt;✅ CIDRs for VLAN subnets must not overlap&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ All subnets must be in one Availability Zone&lt;br&gt;
🧠 Why Use EVS CLI?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📦 Infrastructure-as-Code: fits easily into Terraform or CI/CD workflows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚙️ Automation: simplifies provisioning and scaling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🧪 Testing: replicate environments consistently&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔍 Debugging: quick access to environment state&lt;br&gt;
🧩 Additional Tips&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;--generate-cli-skeleton&lt;/code&gt; to scaffold input JSON or YAML files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;--debug&lt;/code&gt; for troubleshooting and verbose logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Secure your credentials using named &lt;code&gt;--profile&lt;/code&gt; or environment variables&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 Final Thoughts&lt;br&gt;
Amazon EVS with CLI support marks a new era for hybrid cloud operations. If your team relies on VMware, this opens a path to modernize workloads with minimal disruption while enjoying the scale and security of AWS.&lt;/p&gt;

&lt;p&gt;Try out the CLI and start building your first EVS environment today.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>news</category>
      <category>cli</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>Amazon Elastic VMware Service (EVS) is Now GA — Run VMware Cloud Foundation Natively in AWS</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Wed, 06 Aug 2025 14:43:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-elastic-vmware-service-evs-is-now-ga-run-vmware-cloud-foundation-natively-in-aws-3c8o</link>
      <guid>https://dev.to/aws-builders/amazon-elastic-vmware-service-evs-is-now-ga-run-vmware-cloud-foundation-natively-in-aws-3c8o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🚀 &lt;strong&gt;Amazon Elastic VMware Service (EVS)&lt;/strong&gt; is now generally available. If you're managing VMware workloads, this new service might be the bridge to the cloud you've been waiting for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🧩 What is Amazon EVS?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Amazon Elastic VMware Service (EVS)&lt;/strong&gt; lets you deploy and run &lt;strong&gt;VMware Cloud Foundation (VCF)&lt;/strong&gt; environments directly on EC2 bare metal instances inside your &lt;strong&gt;Amazon VPC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No replatforming, no IP reassignments, no retraining. Just your existing VMware stack — now in AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Drop-in migration&lt;/strong&gt; — Lift-and-shift workloads without changing IPs or refactoring applications.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Full administrative control&lt;/strong&gt; — Customize and operate your VMware stack just like on-prem.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Self-managed or Partner-operated&lt;/strong&gt; — Choose your ops model based on team capacity.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;License portability&lt;/strong&gt; — Bring your existing VCF licenses to EVS.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Hybrid-ready&lt;/strong&gt; — Extend on-prem networks and workloads with zero disruption.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;AWS-native integration&lt;/strong&gt; — Connect with 200+ AWS services including Lambda, S3, Aurora, Bedrock, and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌍 Available Now in 6 Regions
&lt;/h2&gt;

&lt;p&gt;Amazon EVS is now GA in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🇺🇸 US East (N. Virginia)&lt;/li&gt;
&lt;li&gt;🇺🇸 US East (Ohio)&lt;/li&gt;
&lt;li&gt;🇺🇸 US West (Oregon)&lt;/li&gt;
&lt;li&gt;🇯🇵 Asia Pacific (Tokyo)&lt;/li&gt;
&lt;li&gt;🇩🇪 Europe (Frankfurt)&lt;/li&gt;
&lt;li&gt;🇮🇪 Europe (Ireland)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VCF Version:&lt;/strong&gt; 5.2.1
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instance Type:&lt;/strong&gt; &lt;code&gt;i4i.metal&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Compatible with external storage and backup tools&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🔄 Use Cases to Explore
&lt;/h2&gt;

&lt;p&gt;Here are some hybrid scenarios Amazon EVS enables:&lt;/p&gt;

&lt;h3&gt;
  
  
  🏢 Datacenter Exit Strategy
&lt;/h3&gt;

&lt;p&gt;Migrate entire clusters from on-prem to AWS without rewriting infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  💼 Disaster Recovery &amp;amp; Resilience
&lt;/h3&gt;

&lt;p&gt;Use EVS as your DR site with full VMware feature parity.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙️ Dev/Test Bursting
&lt;/h3&gt;

&lt;p&gt;Spin up VCF environments in AWS for short-term workloads, testing, or isolated dev.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Gradual Modernization
&lt;/h3&gt;

&lt;p&gt;Move VMs to EVS and start layering AWS-native services like S3, RDS, or even Bedrock-based AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧪 Getting Started with EVS
&lt;/h2&gt;

&lt;p&gt;Here's how to spin up your first Amazon EVS environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complete prerequisites&lt;/strong&gt; (VPC, CIDR, IAM, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create your EVS environment&lt;/strong&gt; via:

&lt;ul&gt;
&lt;li&gt;AWS Console&lt;/li&gt;
&lt;li&gt;AWS CLI&lt;/li&gt;
&lt;li&gt;CloudFormation (&lt;code&gt;AWS::EVS::Environment&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customize VCF&lt;/strong&gt; in the familiar vSphere UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect &amp;amp; Migrate&lt;/strong&gt; your workloads&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;📘 &lt;a href="https://docs.aws.amazon.com/evs/latest/userguide/getting-started.html" rel="noopener noreferrer"&gt;Official Docs – Getting Started&lt;/a&gt;&lt;br&gt;&lt;br&gt;
🖥️ &lt;a href="https://console.aws.amazon.com/evs/" rel="noopener noreferrer"&gt;EVS Console Access&lt;/a&gt;&lt;br&gt;&lt;br&gt;
💸 &lt;a href="https://aws.amazon.com/evs/pricing/" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;If you've been looking for a cloud migration path that &lt;strong&gt;doesn't disrupt your VMware expertise, tooling, or licensing&lt;/strong&gt;, Amazon EVS might be your golden ticket.&lt;/p&gt;

&lt;p&gt;You get the &lt;strong&gt;operational consistency&lt;/strong&gt; of VMware + the &lt;strong&gt;scale and innovation&lt;/strong&gt; of AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Try It Today
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;a href="https://aws.amazon.com/evs" rel="noopener noreferrer"&gt;Explore Amazon EVS on AWS&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📩 Thinking about migrating? Let’s talk about your use case!&lt;br&gt;&lt;br&gt;
🛠 Have a hybrid cloud story? Share it in the comments!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading! Follow me for more AWS deep dives and hybrid cloud updates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>news</category>
      <category>evs</category>
      <category>challenge</category>
    </item>
    <item>
      <title>Beyond Auto-Complete: Supercharge Your Workflow with AWS's Newest AI, Amazon Q</title>
      <dc:creator>Huỳnh Lê Nhất Nghĩa</dc:creator>
      <pubDate>Wed, 06 Aug 2025 14:14:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/beyond-auto-complete-supercharge-your-workflow-with-awss-newest-ai-amazon-q-44n2</link>
      <guid>https://dev.to/aws-builders/beyond-auto-complete-supercharge-your-workflow-with-awss-newest-ai-amazon-q-44n2</guid>
      <description>&lt;p&gt;You've probably used AI code assistants. They're great for boilerplate, auto-completing a function, or even writing a unit test. But what if an AI could do more than just write the code you tell it to? What if it could understand your intent, debug multi-step problems, and even upgrade your application's framework for you?&lt;/p&gt;

&lt;p&gt;That's the promise of Amazon Q Developer, AWS's new, supercharged generative AI assistant. It's more than just a chatbot that knows the AWS docs; it's an active participant in your development workflow. If you're an experienced developer looking for the next leap in productivity, this is a tool you need to pay attention to.&lt;/p&gt;

&lt;p&gt;This post isn't just about what Amazon Q is. We'll dive into the specific, powerful features that move beyond simple code generation and show you how to leverage them with concrete examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Code Whisperer to True Collaborator
&lt;/h2&gt;

&lt;p&gt;AWS has had CodeWhisperer, its direct competitor to GitHub Copilot. It's solid for inline code suggestions. But Amazon Q represents a fundamental shift. It's less of a passive "whisperer" and more of an active "collaborator."&lt;/p&gt;

&lt;p&gt;The biggest leap forward is its agentic capabilities. An "agent" in this context is an AI that can perform a series of actions to achieve a goal. Instead of just suggesting a single block of code, Amazon Q can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan your entire project to understand its structure and dependencies.&lt;/li&gt;
&lt;li&gt;Reason about the steps needed to complete a complex task.&lt;/li&gt;
&lt;li&gt;Read files, write new code, and apply diffs on your behalf.&lt;/li&gt;
&lt;li&gt;Run shell commands to test its own work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where things get interesting for seasoned developers. Think about the time you spend on tasks that are more than just writing a single function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Killer Feature #1: The &lt;code&gt;/fix&lt;/code&gt; Command for In-IDE Debugging
&lt;/h2&gt;

&lt;p&gt;Imagine you're running your application and hit a cryptic error message in the console. Your usual workflow might be to copy-paste the error into Google and hunt through Stack Overflow.&lt;/p&gt;

&lt;p&gt;With Amazon Q, you streamline this dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario: A Null Pointer Exception in Java&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's say your test run fails with this common Java error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Exception &lt;span class="k"&gt;in &lt;/span&gt;thread &lt;span class="s2"&gt;"main"&lt;/span&gt; java.lang.NullPointerException
    at com.example.myapp.service.DataProcessor.process&lt;span class="o"&gt;(&lt;/span&gt;DataProcessor.java:25&lt;span class="o"&gt;)&lt;/span&gt;
    at com.example.myapp.Main.main&lt;span class="o"&gt;(&lt;/span&gt;Main.java:10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of leaving your IDE, you open the Amazon Q chat window and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/fix Exception in thread "main" java.lang.NullPointerException at com.example.myapp.service.DataProcessor.process(DataProcessor.java:25)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Q doesn't just look up the error. It analyzes &lt;code&gt;DataProcessor.java&lt;/code&gt; at line 25 within the context of your project. It might see that you're calling a method on an object that wasn't initialized.&lt;/p&gt;

&lt;p&gt;It will then propose a specific, multi-file solution with a complete diff that you can review.&lt;/p&gt;

&lt;p&gt;Q's Proposed Fix (Diff View):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/src/main/java/com/example/myapp/service/DataProcessor.java
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/src/main/java/com/example/myapp/service/DataProcessor.java
&lt;/span&gt;&lt;span class="p"&gt;@@ -22,7 +22,9 @@&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;     public void process(Data data) {
         // The object 'someObject' was not initialized.
&lt;span class="gd"&gt;-        String result = data.getSomeObject().getValue();
&lt;/span&gt;&lt;span class="gi"&gt;+        if (data != null &amp;amp;&amp;amp; data.getSomeObject() != null) {
+            String result = data.getSomeObject().getValue();
+        }
&lt;/span&gt;         System.out.println("Processing: " + result);
     }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a clear, context-aware suggestion that you can accept with one click. It's like pair-programming with someone who has already diagnosed the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Killer Feature #2: Agentic Framework Upgrades
&lt;/h2&gt;

&lt;p&gt;This is a game-changer for dealing with technical debt. Let's say you have a Java 8 application using Spring Boot 2.x and you need to upgrade to Java 17 and Spring Boot 3.x. This is a notoriously complex task.&lt;/p&gt;

&lt;p&gt;With Amazon Q's upgrade agent, you can initiate this from the command line (after installing the Q tools).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command to Start the Upgrade:&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;// Make sure you are &lt;span class="k"&gt;in &lt;/span&gt;your project&lt;span class="s1"&gt;'s root directory
amazon-q-transformation upgrade --from-version 8 --to-version 17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Q will then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Analyze Dependencies: It scans your &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;build.gradle&lt;/code&gt; and identifies all dependencies that need updating (e.g., Spring Boot, Jakarta EE).&lt;/li&gt;
&lt;li&gt;Refactor Code: It systematically refactors code that relies on deprecated APIs. For instance, it knows that &lt;code&gt;javax.persistence.*&lt;/code&gt; needs to be changed to &lt;code&gt;jakarta.persistence.*&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Example: Refactoring javax to jakarta&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old Code (Before Q):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;New Code (After Q's Transformation):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just one of hundreds of changes it can make automatically. It handles the tedious work, leaving you to focus on testing and logic verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Killer Feature #3: Implementing Features with &lt;code&gt;/dev&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's where it gets really powerful for teams. The &lt;code&gt;/dev&lt;/code&gt; command acts as a "workspace agent." You can give it high-level tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario: Add a new API endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you need to add a standard CRUD API for a Product entity. Instead of creating all the files manually, you can instruct Q:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev create a new REST API for a 'Product' resource. It should have full CRUD operations (GET all, GET by ID, POST, PUT, DELETE). Use Spring Boot conventions and create a controller, service, and repository layer. The Product entity has 'id', 'name', and 'price' fields.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Q will then generate a plan and, upon your approval, create the following file structure and content:&lt;/p&gt;

&lt;p&gt;Plan Proposed by Q:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I will perform the following actions:
1.  ✅ Create `src/main/java/com/example/myapp/model/Product.java` with id, name, and price.
2.  ✅ Create `src/main/java/com/example/myapp/repository/ProductRepository.java` extending JpaRepository.
3.  ✅ Create `src/main/java/com/example/myapp/service/ProductService.java` with business logic for CRUD.
4.  ✅ Create `src/main/java/com/example/myapp/controller/ProductController.java` with REST endpoints.
5.  ✅ Suggest a new unit test file `ProductControllerTest.java`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example Code Generated for &lt;code&gt;ProductController.java&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/products"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt; &lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllProducts&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getProductById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;ResponseEntity:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notFound&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="nf"&gt;createProduct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... other methods for PUT and DELETE&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes a high-level developer request and turns it into fully-scaffolded, idiomatic code, saving you a significant amount of setup time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Is It Worth It?
&lt;/h2&gt;

&lt;p&gt;AI assistants are becoming standard, but Amazon Q Developer is making a strong case for being best-in-class. For intermediate and advanced developers, the value isn't in generating simple loops; it's in automating the complex, time-consuming tasks that bog us down.&lt;/p&gt;

&lt;p&gt;Features like &lt;strong&gt;context-aware debugging&lt;/strong&gt; (&lt;code&gt;/fix&lt;/code&gt;), automated framework upgrades, and the ability to &lt;strong&gt;act as a workspace agen&lt;/strong&gt;t (&lt;code&gt;/dev&lt;/code&gt;) are significant productivity multipliers. By understanding your entire codebase and executing multi-step plans, Amazon Q is moving the goalposts for what a developer assistant can be.&lt;/p&gt;

&lt;p&gt;If you're already in the AWS ecosystem, it's a no-brainer to integrate it into your IDE and give it a try. The time you save on just one nasty bug or one framework upgrade could easily justify the investment. The future of development is collaborative, and AWS is making a powerful new collaborator available to all of&lt;/p&gt;

</description>
      <category>aws</category>
      <category>challenge</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
