<?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: Alok Ranjan Daftuar</title>
    <description>The latest articles on DEV Community by Alok Ranjan Daftuar (@aloknecessary).</description>
    <link>https://dev.to/aloknecessary</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%2F3791551%2F62fbfeb5-1fba-4e79-bc4b-780b7ce52748.jpg</url>
      <title>DEV Community: Alok Ranjan Daftuar</title>
      <link>https://dev.to/aloknecessary</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aloknecessary"/>
    <language>en</language>
    <item>
      <title>Why Agent Infrastructure Is Its Own Discipline</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Fri, 18 Sep 2026 05:39:20 +0000</pubDate>
      <link>https://dev.to/aloknecessary/why-agent-infrastructure-is-its-own-discipline-4778</link>
      <guid>https://dev.to/aloknecessary/why-agent-infrastructure-is-its-own-discipline-4778</guid>
      <description>&lt;p&gt;Ask a platform team how they're going to run their first production agent and you'll get a confident answer within ten seconds: containerize it, put it behind an ingress, wire up a Horizontal Pod Autoscaler, done. It's the same playbook that's shipped every stateless service for the last decade, and there's no obvious reason an agent should be different. It accepts a request. It returns a response. It's just a container.&lt;/p&gt;

&lt;p&gt;That answer is wrong — and it's wrong in a way that doesn't show up in a demo. It shows up three weeks into production, when a pod gets killed mid-reasoning because a liveness probe decided a 40-second tool call was a hang, or when the autoscaler adds five replicas because CPU spiked on a single request calling six tools in sequence, or when two "sessions" turn out to share a K8s Service without anyone having reasoned about what that means for isolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shape of an agent request vs. a microservice request
&lt;/h2&gt;

&lt;p&gt;A typical microservice request has a shape platform engineers have spent fifteen years optimizing around: bounded latency, a single unit of compute per request, statelessness between requests, and a clear success/failure signal at the HTTP layer. An agent request breaks all four at once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Standard microservice request:
  client -&amp;gt; service -&amp;gt; [DB/cache lookup] -&amp;gt; response
  Duration: milliseconds to low seconds
  Compute: roughly constant per request
  State: none carried between requests
  Outcome signal: HTTP status code

Agent request:
  client -&amp;gt; agent -&amp;gt; [reason] -&amp;gt; tool call 1 -&amp;gt; [reason] -&amp;gt; tool call 2
        -&amp;gt; [reason] -&amp;gt; tool call N -&amp;gt; [reason] -&amp;gt; response
  Duration: seconds to minutes, highly variable
  Compute: proportional to reasoning depth and tool fan-out
  State: conversation/task context carried across the entire chain
  Outcome signal: HTTP 200 with a semantically wrong answer is common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the one platform teams underestimate most. An agent that loops on a tool, calls the wrong one, or returns a plausible-but-incorrect result will still return a healthy status code. The infrastructure layer has no way to distinguish a correct run from a confidently wrong one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the standard playbook breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Microservice assumption&lt;/th&gt;
&lt;th&gt;Agent reality&lt;/th&gt;
&lt;th&gt;Practical implication&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Liveness = "process is alive and responsive"&lt;/td&gt;
&lt;td&gt;A live agent can be legitimately unresponsive for 30–90 seconds mid-tool-call&lt;/td&gt;
&lt;td&gt;Naive liveness probes kill healthy pods mid-reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU/memory tracks load&lt;/td&gt;
&lt;td&gt;Load tracks reasoning depth and tool fan-out, not CPU&lt;/td&gt;
&lt;td&gt;HPA on CPU/memory over- or under-scales unpredictably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests are independent&lt;/td&gt;
&lt;td&gt;A single task often spans multiple round trips carrying shared context&lt;/td&gt;
&lt;td&gt;Session/state handling needs an explicit design, not an assumption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One replica serves any request&lt;/td&gt;
&lt;td&gt;Tool credentials and context may be tenant-specific&lt;/td&gt;
&lt;td&gt;Routing and isolation boundaries need to be architectural, not incidental&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout = failure&lt;/td&gt;
&lt;td&gt;Timeout at 30s might just mean the agent is still reasoning&lt;/td&gt;
&lt;td&gt;Timeout budgets have to be set per tool-call chain, not per request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The liveness probe problem, concretely
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;periodSeconds: 10&lt;/code&gt; and &lt;code&gt;failureThreshold: 3&lt;/code&gt;, a standard probe kills a pod if &lt;code&gt;/health&lt;/code&gt; doesn't respond within roughly 30 seconds — which is an entirely normal duration for a reasoning step that includes a tool call to a slow downstream API.&lt;/p&gt;

&lt;p&gt;The fix isn't a bigger &lt;code&gt;failureThreshold&lt;/code&gt;. It's separating "the process is alive" from "the process is making forward progress":&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;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;          &lt;span class="c1"&gt;# answers: is the process itself alive?&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;
  &lt;span class="na"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;      &lt;span class="c1"&gt;# ~45s of true unresponsiveness before restart&lt;/span&gt;

&lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ready&lt;/span&gt;            &lt;span class="c1"&gt;# answers: can this pod accept new work right now?&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; should do nothing more than confirm the process's event loop is running — never block on the status of an in-progress tool call. &lt;code&gt;/ready&lt;/code&gt; reflects capacity: a pod mid-reasoning can report itself not-ready for new work without being treated as dead. For a request/response service these two questions have the same answer. For an agent, they routinely don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed in the protocol layer
&lt;/h2&gt;

&lt;p&gt;A large part of the awkwardness in running MCP-based agents on Kubernetes came from the original protocol design, which required persistent, pinned sessions between a client and a specific server instance — the opposite of what horizontally scaled infrastructure wants. That constraint forced teams into sticky routing and shared session stores just to keep a conversation coherent across requests.&lt;/p&gt;

&lt;p&gt;The July 2026 MCP specification revision removed the protocol-level session entirely. Any request can now land on any server instance, and applications that need to carry state across calls do it the way HTTP APIs always have — by minting an explicit handle passed back as an ordinary argument, rather than relying on the transport to remember. That single change removes an entire category of infrastructure workaround (sticky routing, pinned sessions, shared session stores) that used to be treated as unavoidable.&lt;/p&gt;

&lt;p&gt;The second shift is A2A (Agent-to-Agent protocol), which reached v1.0 in early 2026. Where MCP governs how an agent talks to tools and data sources, A2A governs how agents talk to each other — and that distinction matters for infrastructure design. A multi-agent system where agents call each other over A2A has different routing, identity, and isolation requirements than one where a single orchestrator calls tools over MCP. Both patterns are in production today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the cost of getting this wrong compounds quietly
&lt;/h2&gt;

&lt;p&gt;A liveness probe that kills healthy pods doesn't fail loudly — it shows up as an elevated error rate attributed to "model flakiness" or "the tool API being unreliable," and teams spend weeks tuning retry logic against a problem that's actually a probe misconfiguration one layer down. An autoscaler tuned on the wrong signal doesn't fail either — it just runs 30% more replicas than the workload needs, indefinitely, because nobody has a reason to suspect the scaling metric itself.&lt;/p&gt;

&lt;p&gt;Getting the infrastructure layer right doesn't guarantee correct agent behavior, but getting it wrong guarantees you can't tell the difference between an agent that's actually failing and one that's simply being run on infrastructure that wasn't built for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The summary covers the core argument. The full article goes deeper on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The full breakdown of each assumption failure — autoscaling signal, session and state design, isolation boundaries, and timeout budget — with the specific production failure mode each one produces&lt;/li&gt;
&lt;li&gt;Why EKS and AKS don't solve the shape problem by default, and how the implementations diverge (IRSA vs. workload identity federation, ALB Ingress vs. Application Gateway) even when the underlying design goal is identical&lt;/li&gt;
&lt;li&gt;The complete naive vs. corrected deployment YAML side-by-side, with the exact reasoning behind each change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/why-agent-infrastructure-is-its-own-discipline/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=why-agent-infrastructure-is-its-own-discipline" rel="noopener noreferrer"&gt;Why Agent Infrastructure Is Its Own Discipline — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>VPC Peering vs Transit Gateway: Choosing the Right AWS Inter-VPC Connectivity Model</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Tue, 15 Sep 2026 11:35:40 +0000</pubDate>
      <link>https://dev.to/aloknecessary/vpc-peering-vs-transit-gateway-choosing-the-right-aws-inter-vpc-connectivity-model-58i2</link>
      <guid>https://dev.to/aloknecessary/vpc-peering-vs-transit-gateway-choosing-the-right-aws-inter-vpc-connectivity-model-58i2</guid>
      <description>&lt;p&gt;The default assumption most teams start with is that VPC peering is the "simple" option and Transit Gateway is the "enterprise" option you graduate into once you're big enough. That framing is wrong often enough to cause real architectural pain. The actual decision isn't about company size — it's about topology shape.&lt;/p&gt;

&lt;p&gt;Before comparing features, answer one question: does your connectivity requirement look like a &lt;strong&gt;mesh&lt;/strong&gt; or a &lt;strong&gt;hub-and-spoke&lt;/strong&gt;? That single test resolves most of the debate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The math that kills peering at scale
&lt;/h2&gt;

&lt;p&gt;A peering connection is non-transitive — A cannot reach C through B even if both connections exist. For N VPCs in a full mesh, the connections required are &lt;code&gt;N * (N - 1) / 2&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;VPCs&lt;/th&gt;
&lt;th&gt;Peering connections&lt;/th&gt;
&lt;th&gt;TGW attachments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;190&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 10 VPCs you're managing 45 separate connections, each with its own route table entries on both sides. Transit Gateway needs exactly N attachments regardless of how many VPCs you have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up VPC Peering in Terraform
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_peering_connection"&lt;/span&gt; &lt;span class="s2"&gt;"app_to_shared"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;peer_vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared_services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;auto_accept&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"app_to_shared"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;destination_cidr_block&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared_services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cidr_block&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_peering_connection_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc_peering_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_to_shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"shared_to_app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shared_private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;destination_cidr_block&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cidr_block&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_peering_connection_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc_peering_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_to_shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both directions need an explicit route — peering doesn't propagate routes automatically. Every new VPC added to a peered mesh means touching route tables on every existing member.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up Transit Gateway with segmented routing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ec2_transit_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tgw-central-networking"&lt;/span&gt;
  &lt;span class="nx"&gt;default_route_table_association&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"disable"&lt;/span&gt;
  &lt;span class="nx"&gt;default_route_table_propagation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"disable"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ec2_transit_gateway_vpc_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_private_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_private_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;transit_gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ec2_transit_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;disable&lt;/code&gt; pair on both defaults is deliberate — it enables route table segmentation instead of every attachment automatically seeing every other attachment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Route table segmentation: the feature peering can't replicate
&lt;/h2&gt;

&lt;p&gt;A single Transit Gateway can have multiple route tables. Each attachment associates with one, giving you actual network segmentation without touching security groups or NACLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TGW Route Table: "production"
├── app-vpc attachment    (associated)
├── shared-vpc attachment (associated, propagated)
└── sandbox-vpc attachment (NOT associated — isolated by design)

TGW Route Table: "sandbox"
├── sandbox-vpc attachment (associated)
└── shared-vpc attachment  (associated, propagated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pattern most multi-account AWS Organizations setups converge on: one Transit Gateway per region, a small number of route tables representing trust boundaries, and VPCs associated into the appropriate table.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-account via AWS RAM
&lt;/h2&gt;

&lt;p&gt;Transit Gateway solves the multi-account case cleanly through Resource Access Manager — one account owns the TGW, shares it via RAM, and spoke accounts create their own attachments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ram_resource_share"&lt;/span&gt; &lt;span class="s2"&gt;"tgw_share"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tgw-network-share"&lt;/span&gt;
  &lt;span class="nx"&gt;allow_external_principals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ram_resource_association"&lt;/span&gt; &lt;span class="s2"&gt;"tgw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resource_arn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ec2_transit_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;resource_share_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ram_resource_share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tgw_share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ram_principal_association"&lt;/span&gt; &lt;span class="s2"&gt;"spoke_account"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"222233334444"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_share_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ram_resource_share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tgw_share&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Peering works cross-account too, but the N(N-1)/2 problem gets worse because you're also managing acceptance workflows and cross-account IAM permissions for every connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  DNS and security group gotchas
&lt;/h2&gt;

&lt;p&gt;Routing connectivity doesn't automatically mean DNS works across it. For peering, DNS resolution support must be enabled explicitly on both sides — without it, private hosted zone records resolve to public IPs or fail entirely, which looks identical to a routing problem.&lt;/p&gt;

&lt;p&gt;Security group referencing across peering connections is also limited — most cross-account or cross-region setups fall back to CIDR-based rules rather than SG-to-SG references. Transit Gateway doesn't change this constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting in priority order
&lt;/h2&gt;

&lt;p&gt;When "VPC-A can't reach VPC-B" comes in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route table on both sides&lt;/strong&gt; — both directions need explicit routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection/attachment state&lt;/strong&gt; — &lt;code&gt;pending-acceptance&lt;/code&gt; on peering, non-&lt;code&gt;available&lt;/code&gt; TGW attachment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security group and NACL on both ends&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS resolution settings&lt;/strong&gt; — easy to mistake for a routing failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TGW route table association&lt;/strong&gt; — an attachment not associated with the expected route table silently fails&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The full post covers everything above in depth, plus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The DNS resolution strategy for hub-and-spoke setups using Route 53 Resolver endpoints — why it needs to be planned alongside routing, not retrofitted&lt;/li&gt;
&lt;li&gt;The full side-by-side comparison table across all dimensions (routing model, cost, cross-region, segmentation, best fit)&lt;/li&gt;
&lt;li&gt;The migration path from peering to Transit Gateway — running both in parallel, using most-specific-match routing for a controlled cutover without an all-or-nothing switch&lt;/li&gt;
&lt;li&gt;When peering is genuinely the right answer and why you shouldn't add TGW complexity to a two-VPC problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/aws-vpc-peering-vs-transit-gateway/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=aws-vpc-peering-vs-transit-gateway" rel="noopener noreferrer"&gt;VPC Peering vs Transit Gateway — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>cloud</category>
      <category>terraform</category>
    </item>
    <item>
      <title>CI to GitOps Handoff: Argo Image Updater vs. CI-Writes-the-Commit vs. Kargo</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:23:01 +0000</pubDate>
      <link>https://dev.to/aloknecessary/ci-to-gitops-handoff-argo-image-updater-vs-ci-writes-the-commit-vs-kargo-468</link>
      <guid>https://dev.to/aloknecessary/ci-to-gitops-handoff-argo-image-updater-vs-ci-writes-the-commit-vs-kargo-468</guid>
      <description>&lt;p&gt;ArgoCD only reconciles what's in Git. A CI pipeline builds an image, pushes it to a registry — and then what? Something has to turn "a new image exists" into "a commit exists in the GitOps repo referencing it." That handoff is where a surprising number of GitOps implementations quietly go wrong: either it's done manually, or it's automated in a way that reintroduces the exact imperative-deploy risk GitOps was supposed to remove.&lt;/p&gt;

&lt;p&gt;Three approaches cover almost every real setup. The choice isn't just operational preference — it interacts directly with your repo structure and how much promotion-stage enforcement you actually need.&lt;/p&gt;




&lt;h2&gt;
  
  
  The structural difference in one picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Approach 1 — Argo Image Updater
  CI: build → push image ──────────────► registry
                                              │ (polled every 1-2 min)
                                              ▼
                                    Image Updater controller
                                              │ writes tag
                                              ▼
                                     GitOps repo commit ──► ArgoCD syncs

Approach 2 — CI writes the commit
  CI: build → push image ──► registry
       │
       └── same workflow, next job: bump tag, commit, push ──► GitOps repo ──► ArgoCD syncs

Approach 3 — Kargo
  CI: build → push image ──► registry
                                 │
                                 ▼
                            Kargo Warehouse (watches registry)
                                 │
                                 ▼
                          Stage: staging ──(verify)──► Stage: prod
                                 │                          │
                                 ▼                          ▼
                          GitOps repo commit          GitOps repo commit
                          (staging path)               (prod path, only if
                                                         staging verified)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approaches 1 and 2 go straight from "new image" to "commit" with no concept of one environment gating another. Kargo inserts an explicit, enforced step between them. Everything below is detail on why you'd pick one over another.&lt;/p&gt;




&lt;h2&gt;
  
  
  Argo Image Updater
&lt;/h2&gt;

&lt;p&gt;Image Updater runs alongside ArgoCD, polls registries for new tags matching a pattern, and writes the updated tag back into the GitOps repo. CI's job stays exactly "build, test, push image" — no GitOps-repo awareness required.&lt;/p&gt;

&lt;p&gt;Configuration lives as annotations on the Application itself:&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;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;argocd-image-updater.argoproj.io/image-list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout=111122223333.dkr.ecr.us-east-1.amazonaws.com/checkout-service&lt;/span&gt;
  &lt;span class="na"&gt;argocd-image-updater.argoproj.io/checkout.update-strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semver&lt;/span&gt;
  &lt;span class="na"&gt;argocd-image-updater.argoproj.io/checkout.allow-tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;regexp:^v[0-9]+\.[0-9]+\.[0-9]+$'&lt;/span&gt;
  &lt;span class="na"&gt;argocd-image-updater.argoproj.io/write-back-method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
  &lt;span class="na"&gt;argocd-image-updater.argoproj.io/git-branch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;write-back-method: git&lt;/code&gt; setting is the important one. The alternative (&lt;code&gt;argocd&lt;/code&gt;) writes to the Application object only, meaning the running state and the Git repo can silently diverge — undermining the entire premise of GitOps as the source of truth. If you use Image Updater, &lt;code&gt;git&lt;/code&gt; write-back is the only mode consistent with a real GitOps model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; polling-based (1-2 minute delay), and no native concept of promotion stages. Getting a build to flow dev → staging → prod with any gate means bolting that logic on separately — which is exactly the gap Kargo exists to close.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI writes the manifest commit
&lt;/h2&gt;

&lt;p&gt;No separate controller. The CI job itself pushes the commit after a successful build. The auth chain here deserves the same scrutiny as cloud credentials — a long-lived PAT with write access to the GitOps repo is the wrong answer for the same reasons a long-lived AWS access key is wrong.&lt;/p&gt;

&lt;p&gt;The right pattern: a GitHub App scoped to exactly the one GitOps repo, with an installation token minted per workflow run:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-gitops-repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate GitHub App token for GitOps repo&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;app-token&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/create-github-app-token@v1&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;app-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.GITOPS_BOT_APP_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;private-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITOPS_BOT_PRIVATE_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-org&lt;/span&gt;
          &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-team-gitops&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@v6&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;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example-org/checkout-team-gitops&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.app-token.outputs.token }}&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;Bump image tag&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;yq -i '.image.tag = "${{ github.sha }}"' apps/checkout-service/prod/values.yaml&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;Commit and push&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 config user.name "gitops-bot"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "gitops-bot@example.org"&lt;/span&gt;
          &lt;span class="s"&gt;git commit -am "checkout-service: bump to ${{ github.sha }}"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is scoped to one installation (one repo) and expires roughly an hour after issuance — no long-lived credential sitting in a secrets store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; no delay, no extra controller. But CI now has push access to the thing that controls production. A GitHub App token scoped org-wide instead of to one repo turns a compromised CI secret from a one-service incident into an org-wide one. The scoping isn't optional hardening — it's the difference between those two outcomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kargo
&lt;/h2&gt;

&lt;p&gt;Image Updater and CI-writes-the-commit both update one Application at a time with no native concept of "this build must pass staging before prod." Kargo models that as a first-class API.&lt;/p&gt;

&lt;p&gt;Two core resources: a &lt;code&gt;Warehouse&lt;/code&gt; (watches for new artifacts, like Image Updater) and a &lt;code&gt;Stage&lt;/code&gt; (one step in a promotion pipeline). The key field:&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;kargo.akuity.io/v1alpha1&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;Stage&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;prod&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;checkout-team&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;requestedFreight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="pi"&gt;:&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;Warehouse&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;checkout-service&lt;/span&gt;
      &lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# prod can only promote freight that passed through staging&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sources: stages: [staging]&lt;/code&gt; means prod is structurally incapable of promoting an image that hasn't gone through staging — enforced by Kargo, not by convention. Verification gates (smoke tests, analysis runs) attach to each Stage, and Kargo tracks exactly which artifact made it through which Stages.&lt;/p&gt;

&lt;p&gt;The distinction from bolting a manual approval onto Image Updater: the gate is a property of the Stage graph itself, checked by Kargo before it constructs a promotion for the next Stage. Not a step someone has to remember to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; another controller with its own resource model on top of ArgoCD's. Earns its cost specifically when multi-stage promotion with gated, auditable progression is a real requirement — not as a default for every service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Recommended approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single environment, or environments promote independently&lt;/td&gt;
&lt;td&gt;Argo Image Updater — simplest, no CI changes needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple promotion logic already visible in CI (merge to main = staging, tag = prod)&lt;/td&gt;
&lt;td&gt;CI writes the commit — keeps logic in one place, no extra controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard requirement that prod only receives artifacts that passed staging with automated verification&lt;/td&gt;
&lt;td&gt;Kargo — this is the exact problem it's built to solve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated workloads needing an auditable promotion trail as a first-class object&lt;/td&gt;
&lt;td&gt;Kargo — Stage/Freight model gives you that, not something reconstructed from CI logs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't mutually exclusive across a platform — a team running mostly independent services on Image Updater might still put its most compliance-sensitive service through Kargo specifically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The summary covers the shape of each approach and the key trade-offs. The full article goes deeper on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The complete Image Updater Application manifest — &lt;code&gt;image-list&lt;/code&gt;, &lt;code&gt;update-strategy&lt;/code&gt;, &lt;code&gt;allow-tags&lt;/code&gt;, and &lt;code&gt;write-back-method&lt;/code&gt; in context, not just the annotation snippet&lt;/li&gt;
&lt;li&gt;The full two-job GitHub Actions workflow (build + update-gitops-repo), including the step that verifies the App token &lt;em&gt;fails&lt;/em&gt; against repos it shouldn't reach — confirming blast radius, not just that it works&lt;/li&gt;
&lt;li&gt;The complete Kargo &lt;code&gt;Warehouse&lt;/code&gt; + &lt;code&gt;Stage&lt;/code&gt; YAML including the &lt;code&gt;verification&lt;/code&gt; block that wires an &lt;code&gt;AnalysisTemplate&lt;/code&gt; smoke test to the staging Stage — the part that makes "staging verified" mean something concrete&lt;/li&gt;
&lt;li&gt;Why a Matrix generator with a too-broad cluster label selector connects directly to the auth chain here: AppProject destinations as the backstop when the ApplicationSet generates more than intended&lt;/li&gt;
&lt;li&gt;Article 4 preview: secrets management (Sealed Secrets, External Secrets Operator, Vault plugin) — what each actually protects against and what it doesn't&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/ci-to-gitops-handoff/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=ci-to-gitops-handoff" rel="noopener noreferrer"&gt;CI to GitOps Handoff: Argo Image Updater vs. CI-Writes-the-Commit vs. Kargo — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>githubactions</category>
      <category>argocd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Always Encrypt in Transit: The Gap Between TLS Everywhere and Actual Transport Security</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:26:21 +0000</pubDate>
      <link>https://dev.to/aloknecessary/always-encrypt-in-transit-the-gap-between-tls-everywhere-and-actual-transport-security-2mib</link>
      <guid>https://dev.to/aloknecessary/always-encrypt-in-transit-the-gap-between-tls-everywhere-and-actual-transport-security-2mib</guid>
      <description>&lt;p&gt;"We have TLS everywhere." It appears in every architecture review and every compliance questionnaire. The gap is not in the intention — it is in the implementation. Most systems that claim TLS everywhere have TLS at the edge and plaintext everywhere else: the ingress controller to the pod is HTTP, pod-to-pod traffic is unencrypted, and the application-to-database connection string never had &lt;code&gt;Encrypt=True&lt;/code&gt; or &lt;code&gt;sslmode=require&lt;/code&gt; set. None of these gaps appear in architecture diagrams.&lt;/p&gt;

&lt;p&gt;This post maps where TLS is actually absent, why the gaps exist, and the implementation path that closes them — without defaulting to "add a service mesh" as the answer to every transport security question.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four Layers Where TLS Is Actually Absent
&lt;/h2&gt;

&lt;p&gt;Before solutions, the precise inventory of where plaintext exists in systems claiming TLS everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario: "Secure" architecture
  ✅ HTTPS on the load balancer
  ✅ Private subnets, no public IPs
  ✅ Security groups restricting access
  ❌ Ingress Controller → Pod: plaintext HTTP
  ❌ Pod → Pod: plaintext HTTP/gRPC
  ❌ Application → RDS: plaintext TCP
  ❌ Cluster infrastructure certs: expire annually, no automated rotation

What "TLS everywhere" actually means in this architecture:
  TLS on the edge. Plaintext the rest of the way.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 1 — Ingress-to-pod:&lt;/strong&gt; nginx-ingress and Traefik terminate TLS at the edge and forward plain HTTP to backend pods by default. The gap is invisible in architecture diagrams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Pod-to-pod:&lt;/strong&gt; Kubernetes does not encrypt data plane traffic. A compromised pod on a shared node can observe plaintext traffic from other pods on the same node — the VPC boundary is not the relevant perimeter here, the pod boundary is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Application-to-database:&lt;/strong&gt; Enabling TLS on RDS makes the database &lt;em&gt;capable&lt;/em&gt; of TLS connections. It does not enforce them. A connection string without &lt;code&gt;sslmode=require&lt;/code&gt; or &lt;code&gt;Encrypt=True&lt;/code&gt; connects over plaintext regardless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4 — Cluster infrastructure certs:&lt;/strong&gt; kubeadm cluster CA certificates expire after one year by default with no automated renewal. A missed rotation takes down the entire cluster — not an application outage, a cluster-level failure where &lt;code&gt;kubectl&lt;/code&gt; stops working entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Termination Architecture: Where the Decision Gets Made
&lt;/h2&gt;

&lt;p&gt;Where TLS terminates determines which traffic is encrypted. Three patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge termination only&lt;/strong&gt; — TLS terminates at the ingress controller, all internal traffic is plaintext. Legitimate for single-tenant clusters with no regulated data. Not legitimate to call "TLS everywhere."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-encryption&lt;/strong&gt; — the ingress controller terminates external TLS and re-encrypts before forwarding to the pod. Closes the ingress-to-pod gap without a service mesh:&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="c1"&gt;# NGINX Ingress: re-encrypt to backend over TLS&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/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;Ingress&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;secure-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/backend-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTPS"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/ssl-redirect&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;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letsencrypt-prod"&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;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;api.yourdomain.com&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-tls-secret&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.yourdomain.com&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
        &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
        &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service&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;api-service&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8443&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;End-to-end mTLS via service mesh&lt;/strong&gt; — the mesh intercepts all pod-to-pod traffic and wraps it in mTLS. Strongest posture, highest operational overhead. Justified in regulated multi-tenant clusters; disproportionate for smaller service estates.&lt;/p&gt;




&lt;h2&gt;
  
  
  cert-manager: The Implementation That Actually Automates It
&lt;/h2&gt;

&lt;p&gt;cert-manager handles issuance, renewal, and storage in Kubernetes Secrets with zero manual steps in the rotation path. One ClusterIssuer and one Certificate resource per service:&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="c1"&gt;# ClusterIssuer: Let's Encrypt production&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/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;ClusterIssuer&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;letsencrypt-prod&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;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://acme-v02.api.letsencrypt.org/directory&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platform@yourdomain.com&lt;/span&gt;
    &lt;span class="na"&gt;privateKeySecretRef&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;letsencrypt-prod-account-key&lt;/span&gt;
    &lt;span class="na"&gt;solvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# Certificate: 90-day validity, auto-renewed at 60 days remaining&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/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;Certificate&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;api-certificate&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;production&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;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-tls-secret&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2160h&lt;/span&gt;
  &lt;span class="na"&gt;renewBefore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;720h&lt;/span&gt;
  &lt;span class="na"&gt;dnsNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api.yourdomain.com&lt;/span&gt;
  &lt;span class="na"&gt;issuerRef&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;letsencrypt-prod&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;ClusterIssuer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For internal service-to-service certificates, use a self-signed CA issuer with 24-hour validity — cert-manager makes short-lived internal certs operationally trivial, and a 24-hour certificate has a dramatically smaller blast radius than a one-year certificate if the key is compromised:&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="c1"&gt;# Internal service certificate — 24h validity, rotated automatically&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/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;Certificate&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;order-service-cert&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;production&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;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order-service-tls&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;
  &lt;span class="na"&gt;renewBefore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8h&lt;/span&gt;
  &lt;span class="na"&gt;dnsNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;order-service.production.svc.cluster.local&lt;/span&gt;
  &lt;span class="na"&gt;issuerRef&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;internal-ca-issuer&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;ClusterIssuer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Certificate Lifecycle: Where Security Controls Become Outages
&lt;/h2&gt;

&lt;p&gt;The failure mode is consistent: a certificate is issued, configured, and forgotten. The alert that was supposed to fire before expiry never got configured, or fired into a channel nobody watches. The certificate expires. The service goes down. The postmortem recommends "better monitoring" — until the same thing happens eighteen months later with a different certificate.&lt;/p&gt;

&lt;p&gt;Alert on time-to-expiry, not on expiry. A Prometheus alert that fires at 15 days remaining gives the team time to investigate before the outage. An alert that fires when the certificate has already expired is a notification of an ongoing incident:&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;groups&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;certificate-expiry&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CertificateExpiringIn15Days&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;certmanager_certificate_expiration_timestamp_seconds&lt;/span&gt;
        &lt;span class="s"&gt;- time() &amp;lt; (15 * 24 * 3600)&lt;/span&gt;
    &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Certificate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expiring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;soon"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CertificateExpired&lt;/span&gt;
    &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;certmanager_certificate_expiration_timestamp_seconds - time() &amp;lt; 0&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Certificate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;EXPIRED"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both alerts are required. Most teams have only the expired alert — which is a notification of an ongoing incident, not a prevention.&lt;/p&gt;




&lt;h2&gt;
  
  
  mTLS Without a Service Mesh
&lt;/h2&gt;

&lt;p&gt;For fewer than twenty services, cert-manager internal CA plus application-level TLS provides mutual authentication without sidecar injection or a mesh control plane. The application handles the TLS handshake directly using certificates cert-manager issues and rotates automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .NET: present a client certificate and verify the server against the internal CA&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clientCert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X509Certificate2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFromPemFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientCertPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientKeyPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;caCert&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;X509Certificate2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caCertPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientCertificates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientCert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerCertificateCustomValidationCallback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;ChainPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrustMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X509ChainTrustMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomRootTrust&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChainPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomTrustStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caCert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The certificates are mounted from Kubernetes Secrets that cert-manager manages. When cert-manager rotates the Secret, the volume mount is updated in place — no application restart required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;This summary covers the core gaps, termination architecture patterns, cert-manager setup, and the mTLS-without-mesh approach. The full article includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The complete database enforcement gap — &lt;code&gt;rds.force_ssl&lt;/code&gt;, connection string &lt;code&gt;Encrypt=True&lt;/code&gt;, and why enabling TLS on the DB is not the same as enforcing it&lt;/li&gt;
&lt;li&gt;The VPC isolation misconception in full — why private subnets don't substitute for encryption&lt;/li&gt;
&lt;li&gt;The "Kubernetes encrypts cluster traffic" misconception — what it actually encrypts vs. what it doesn't&lt;/li&gt;
&lt;li&gt;Full decision framework mapping every traffic path to the right termination architecture&lt;/li&gt;
&lt;li&gt;When regulated environments (PCI-DSS, HIPAA) make full end-to-end TLS non-negotiable&lt;/li&gt;
&lt;li&gt;Eight key takeaways covering every layer of the in-transit security posture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/always-encrypt-in-transit/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=always-encrypt-in-transit" rel="noopener noreferrer"&gt;Always Encrypt in Transit: The Gap Between TLS Everywhere and Actual Transport Security — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>kubernetes</category>
      <category>cloudnative</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS MGN Architecture: How Continuous Replication Actually Works</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:29:32 +0000</pubDate>
      <link>https://dev.to/aloknecessary/aws-mgn-architecture-how-continuous-replication-actually-works-47f4</link>
      <guid>https://dev.to/aloknecessary/aws-mgn-architecture-how-continuous-replication-actually-works-47f4</guid>
      <description>&lt;p&gt;The first time I ran an AWS MGN migration, I trusted the console's green "Healthy" status without understanding what it actually meant. It took a stalled replication and a confusing lag metric mid-cutover to make me go back and learn the mechanics underneath the dashboard. This post is the breakdown I wish I'd had before that first wave.&lt;/p&gt;

&lt;p&gt;MGN does one job: continuous, block-level replication of a source server's disks to a staging area in your target AWS account, so that when you cut over, you launch a fully synced, bootable EC2 instance instead of doing a one-time data copy and hoping nothing changed since. The distinction that matters is &lt;strong&gt;block-level, not file-level&lt;/strong&gt; — MGN doesn't care about your filesystem or application state, which is what allows it to keep a target in near-real-time sync with a live, running source server throughout the migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four components, in order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The replication agent&lt;/strong&gt; — installed directly on the source server, hooks into the OS's disk I/O path at the kernel level. On Linux this means a kernel-level block device reader; on Windows, a filter driver. This low-level hook is why a kernel version mismatch on the source isn't a minor footnote — it's a direct threat to replication itself.&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;python3 ./aws-replication-installer-init.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--aws-access-key-id&lt;/span&gt; &amp;lt;access-key-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--aws-secret-access-key&lt;/span&gt; &amp;lt;secret-access-key&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--no-prompt&lt;/code&gt; flag matters for scripted wave installations — without it, the installer pauses for disk selection confirmation and silently stalls automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The staging area subnet&lt;/strong&gt; — a designated subnet in the target AWS account where MGN provisions its replication infrastructure. Needs outbound connectivity and enough IOPS headroom on EBS to keep up with the source's write rate. Undersizing this subnet or placing it behind restrictive route tables is a common cause of replication that starts but never stabilizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The replication server and EBS volumes&lt;/strong&gt; — for each source server, MGN provisions a temporary EC2 instance in the staging subnet whose only job is to receive the block-level stream and write it to EBS volumes that mirror the source's disk layout. These EBS volumes are the actual replica — they're what gets snapshotted at cutover.&lt;/p&gt;

&lt;p&gt;The cost model people get wrong: you're paying for these replication servers and EBS volumes for the &lt;strong&gt;entire duration&lt;/strong&gt; of the migration project, not just at cutover. A wave that sits in "replicating, not yet cut over" for six weeks accumulates staging infrastructure cost for all six weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Continuous sync and the lag metric&lt;/strong&gt; — once initial replication completes, MGN switches to continuous incremental sync: every write on the source is captured and streamed as it happens. The metric to watch is &lt;strong&gt;replication lag&lt;/strong&gt; — how far behind the target EBS volumes are from the live source. Lag that climbs rather than staying flat is a leading indicator of a cutover that will either take much longer than expected or launch a target instance further behind the source than your rollback tolerance allows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws mgn describe-source-servers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'items[*].{Server:sourceServerID, LagDuration:dataReplicationInfo.lagDuration, State:dataReplicationInfo.dataReplicationState}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this as a scriptable pre-cutover gate rather than eyeballing a color indicator in the console.&lt;/p&gt;




&lt;h2&gt;
  
  
  What cutover actually triggers
&lt;/h2&gt;

&lt;p&gt;Cutover is not a data operation — the data is already synced. What it actually does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes a final snapshot of the target EBS volumes at the moment of cutover&lt;/li&gt;
&lt;li&gt;Launches a new EC2 instance from that snapshot using the launch template you've configured&lt;/li&gt;
&lt;li&gt;Runs the MGN post-launch conversion process — driver injection, network configuration, and OS-level changes needed to make a disk image that ran on a different hypervisor boot correctly under AWS's Nitro hypervisor&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 is where kernel driver failures happen. The conversion process needs to inject or activate the correct network driver (&lt;code&gt;ena&lt;/code&gt;) for the instance to have connectivity after launch. If the source server's kernel doesn't have the module available or the boot configuration doesn't reference it correctly, the instance can come up without network connectivity — or fail to boot entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Launch settings most people under-invest in
&lt;/h2&gt;

&lt;p&gt;Every source server has its own launch template in MGN. Worth setting deliberately before cutover, not left at defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target instance type&lt;/strong&gt; — set explicitly for anything performance-sensitive rather than trusting MGN's right-size recommendation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet and security groups&lt;/strong&gt; — should be decided by your VPC CIDR plan, not chosen ad hoc at cutover time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM instance profile&lt;/strong&gt; — doesn't carry over from the source server; must be set in the launch template explicitly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test launch&lt;/strong&gt; — MGN supports launching a test instance from current replication state without ending replication. Always run at least one test launch before the real cutover. Skipping this is the single most common way teams discover a launch-time problem during the actual cutover window instead of during a safe rehearsal&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Replication settings that matter under load
&lt;/h2&gt;

&lt;p&gt;Three settings worth understanding rather than leaving on defaults for source servers with high write throughput:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth throttling&lt;/strong&gt; — MGN doesn't cap replication bandwidth by default, which can compete with production traffic on a busy source server. Set a maximum throughput per source server for anything actively serving production load during the replication window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging volume type&lt;/strong&gt; — the default EBS volume type for staging can itself become the bottleneck for high-IOPS sources, showing up as climbing lag even when network bandwidth is fine. Rule this in or out separately from the network-bandwidth cause when diagnosing a server that won't stabilize&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point-in-time (PIT) snapshots&lt;/strong&gt; — MGN can retain a rolling window of snapshots of the staging volumes, giving you a recovery point earlier than "right now" if you need to launch from a known-good state. Configure this under &lt;strong&gt;Replication settings → Point-in-time snapshots&lt;/strong&gt; in the MGN console before replication starts — snapshots only accumulate from the point the setting is enabled&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why continuous replication beats snapshot-and-copy
&lt;/h2&gt;

&lt;p&gt;The snapshot approach has an unavoidable trade-off: the longer the gap between snapshot and cutover, the more source-side changes are missing from the target. Closing that gap means either accepting data loss or taking the source offline for the copy window.&lt;/p&gt;

&lt;p&gt;MGN's continuous model removes that trade-off entirely. The target stays within seconds of the source right up until cutover, and the source never has to go offline for the migration itself — only for the brief cutover window when traffic is actually redirected. For anything with a real availability requirement, this is the entire reason to use MGN over a manual export/import process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The summary covers the core mechanics. The full article goes deeper on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The troubleshooting priority order for "these are definitely connected but nothing works" tickets — route tables, attachment state, security layers, DNS — and why checking in that sequence resolves the large majority of cases without reasoning about the full topology from scratch&lt;/li&gt;
&lt;li&gt;The specific kernel driver failure mode that surfaces when migrating Azure-sourced Ubuntu images to AWS, why it manifests the way it does, and the fix&lt;/li&gt;
&lt;li&gt;The full cost model for staging infrastructure across a multi-week wave, with the specific line items to flag before the wave kicks off&lt;/li&gt;
&lt;li&gt;CIDR planning considerations for the staging subnet relative to the rest of the target VPC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/aws-mgn-architecture-continuous-replication/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=aws-mgn-architecture-continuous-replication" rel="noopener noreferrer"&gt;AWS MGN Architecture: How Continuous Replication Actually Works — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloud</category>
      <category>aws</category>
      <category>migration</category>
    </item>
    <item>
      <title>Enforcing Modular Monolith Boundaries in .NET: NDepend, Parallel Pipelines, and the Architecture That Holds</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Mon, 31 Aug 2026 06:03:32 +0000</pubDate>
      <link>https://dev.to/aloknecessary/enforcing-modular-monolith-boundaries-in-net-ndepend-parallel-pipelines-and-the-architecture-37e1</link>
      <guid>https://dev.to/aloknecessary/enforcing-modular-monolith-boundaries-in-net-ndepend-parallel-pipelines-and-the-architecture-37e1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A modular monolith without enforcement is not an architecture — it is a monolith with good intentions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most teams skip the modular monolith and jump straight to microservices. The ones that do attempt a modular monolith rely on convention — "don't cross module boundaries" — which fails the moment deadlines hit.&lt;/p&gt;

&lt;p&gt;The difference between a well-structured modular monolith and a mess is whether boundaries are maintained by &lt;strong&gt;tooling&lt;/strong&gt; or by convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution Structure
&lt;/h2&gt;

&lt;p&gt;Each module is a pair of .NET projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/Modules/
  Orders/
    YourApp.Orders/              ← internal: domain, application, infrastructure
    YourApp.Orders.Contracts/    ← public: DTOs, interfaces, events
  Payments/
    YourApp.Payments/
    YourApp.Payments.Contracts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule&lt;/strong&gt;: modules may only reference each other's &lt;code&gt;*.Contracts&lt;/code&gt; projects. The compiler enforces this physically — no project reference means no type access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Layers of Enforcement
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compiler&lt;/strong&gt; — project references prevent cross-module type access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NetArchTest&lt;/strong&gt; — architecture tests fail the build on namespace-level violations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NDepend CQLinq&lt;/strong&gt; — catches dependency cycles and coupling the compiler can't see&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality Gates&lt;/strong&gt; — block PRs that introduce &lt;em&gt;new&lt;/em&gt; boundary violations&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Module-Scoped Data
&lt;/h2&gt;

&lt;p&gt;Each module owns a dedicated &lt;code&gt;DbContext&lt;/code&gt; with a schema prefix (&lt;code&gt;orders.*&lt;/code&gt;, &lt;code&gt;payments.*&lt;/code&gt;). No module queries another module's tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Module Communication
&lt;/h2&gt;

&lt;p&gt;Modules communicate via MediatR in-process events. Orders publishes &lt;code&gt;OrderPlaced&lt;/code&gt;; Payments subscribes — without Orders knowing Payments exists.&lt;/p&gt;

&lt;p&gt;This is also the extraction seam: when you eventually extract a module into a service, MediatR becomes a message broker. The event contract stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Orders&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Payments&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Inventory&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module's tests run in parallel. CI time scales with the slowest module, not the total count.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Extraction Path
&lt;/h2&gt;

&lt;p&gt;When a module genuinely needs independence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add outbox table → publish to real broker&lt;/li&gt;
&lt;li&gt;Replace MediatR handlers with broker consumers&lt;/li&gt;
&lt;li&gt;Deploy module as separate service&lt;/li&gt;
&lt;li&gt;Publish &lt;code&gt;*.Contracts&lt;/code&gt; as NuGet package&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The boundary was already clean. Extraction is a deployment change, not a redesign.&lt;/p&gt;




&lt;p&gt;The full post covers NDepend CQLinq rule examples, Quality Gate configuration, GitHub Actions pipeline YAML, test isolation patterns, and a production checklist.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://aloknecessary.in/blogs/modular-monolith-dotnet-ndepend/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=modular-monolith-dotnet" rel="noopener noreferrer"&gt;Read the complete implementation guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>AWS VPC Networking Fundamentals: VPCs, Subnets, CIDR, Route Tables, IGW, and NAT Gateways</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Fri, 28 Aug 2026 03:49:00 +0000</pubDate>
      <link>https://dev.to/aloknecessary/aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways-19h1</link>
      <guid>https://dev.to/aloknecessary/aws-vpc-networking-fundamentals-vpcs-subnets-cidr-route-tables-igw-and-nat-gateways-19h1</guid>
      <description>&lt;p&gt;If you've provisioned a VPC from a Terraform module without fully internalising what each piece is doing, that's fine — right up until something breaks. An instance that should be reachable isn't. A private instance can't pull a package update. And you're left checking five different resources with no clear mental model of how they connect.&lt;/p&gt;

&lt;p&gt;This post builds that mental model from the ground up. Not just definitions — the &lt;em&gt;why&lt;/em&gt; behind each piece, so troubleshooting becomes deduction instead of guesswork.&lt;/p&gt;




&lt;h2&gt;
  
  
  CIDR math you actually need
&lt;/h2&gt;

&lt;p&gt;A CIDR block is &lt;code&gt;IP address / prefix length&lt;/code&gt;. The prefix length fixes the network portion; the remaining bits are your host space.&lt;/p&gt;

&lt;p&gt;Formula: &lt;code&gt;2^(32 - prefix) = total addresses&lt;/code&gt;. AWS reserves 5 per subnet (network address, VPC router, DNS, reserved, broadcast).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CIDR&lt;/th&gt;
&lt;th&gt;Total addresses&lt;/th&gt;
&lt;th&gt;Usable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/16&lt;/td&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;65,531&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/20&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;4,091&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/24&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/28&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To reverse-engineer a prefix from a required host count: round up to the next power of two, subtract the exponent from 32. Need 300 hosts? Next power of two is 512 (2⁹), so prefix = 32 - 9 = &lt;code&gt;/23&lt;/code&gt;. Run this before sizing any subnet that will host an autoscaling group or EKS node group.&lt;/p&gt;

&lt;p&gt;Start with &lt;code&gt;/16&lt;/code&gt; for the VPC itself. VPC CIDR is difficult to resize after the fact — once you have subnets, peering connections, or Transit Gateway attachments built against it, renumbering becomes a migration project. &lt;code&gt;/16&lt;/code&gt; costs nothing up front and avoids that corner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subnet allocation: carving up the VPC
&lt;/h2&gt;

&lt;p&gt;A practical three-AZ production layout from &lt;code&gt;10.0.0.0/16&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;Tier&lt;/th&gt;
&lt;th&gt;AZ-a&lt;/th&gt;
&lt;th&gt;AZ-b&lt;/th&gt;
&lt;th&gt;AZ-c&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;10.0.0.0/24&lt;/td&gt;
&lt;td&gt;10.0.1.0/24&lt;/td&gt;
&lt;td&gt;10.0.2.0/24&lt;/td&gt;
&lt;td&gt;/24&lt;/td&gt;
&lt;td&gt;ALB, NAT gateway, bastion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private/app&lt;/td&gt;
&lt;td&gt;10.0.16.0/20&lt;/td&gt;
&lt;td&gt;10.0.32.0/20&lt;/td&gt;
&lt;td&gt;10.0.48.0/20&lt;/td&gt;
&lt;td&gt;/20&lt;/td&gt;
&lt;td&gt;EKS nodes, ECS, EC2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;10.0.64.0/24&lt;/td&gt;
&lt;td&gt;10.0.65.0/24&lt;/td&gt;
&lt;td&gt;10.0.66.0/24&lt;/td&gt;
&lt;td&gt;/24&lt;/td&gt;
&lt;td&gt;RDS, ElastiCache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved&lt;/td&gt;
&lt;td&gt;10.0.128.0/17&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;/17&lt;/td&gt;
&lt;td&gt;Future tiers, Transit Gateway, VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The jump from &lt;code&gt;/24&lt;/code&gt; in the public tier to &lt;code&gt;/20&lt;/code&gt; in the app tier is intentional. ALBs and NAT gateways consume very few IPs; the app tier is where consumption scales with autoscaling groups, rolling deployments, and pod density.&lt;/p&gt;

&lt;p&gt;For EKS specifically: with the VPC CNI, every pod can consume an ENI-backed IP. IP exhaustion is one of the most common EKS production incidents. &lt;code&gt;/20&lt;/code&gt; per AZ for worker subnets is the standard starting point.&lt;/p&gt;

&lt;p&gt;The deliberate gaps between tiers (0–2, then 16–48, then 64–66) leave room to insert new tiers later without renumbering anything already deployed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Route tables: the actual decision maker
&lt;/h2&gt;

&lt;p&gt;A subnet is "public" or "private" because of its route table — not any inherent property of the subnet itself. The table is a list of &lt;code&gt;destination → target&lt;/code&gt; rules evaluated by &lt;strong&gt;most specific match&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every route table gets an implicit &lt;code&gt;local&lt;/code&gt; route for the full VPC CIDR — this can't be removed, and it's what lets every subnet reach every other subnet inside the VPC by default.&lt;/p&gt;

&lt;p&gt;A public subnet route table in Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rtb-public"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route"&lt;/span&gt; &lt;span class="s2"&gt;"public_internet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;destination_cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
  &lt;span class="nx"&gt;gateway_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public_a"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_az_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating the route table does nothing on its own — the association step is what binds it to a subnet and makes routing take effect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Internet Gateway and the three conditions for inbound access
&lt;/h2&gt;

&lt;p&gt;An IGW is horizontally scaled, redundant, and AZ-agnostic — one per VPC, no capacity to configure. It does two things: 1:1 NAT between public and private IPs (the public IP mapping lives at the IGW, not on the instance — which is why &lt;code&gt;ip addr&lt;/code&gt; on an EC2 instance never shows its public IP), and serves as a route table target.&lt;/p&gt;

&lt;p&gt;All three of these must be true simultaneously for inbound internet access to work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The instance has a public or Elastic IP on its ENI.&lt;/li&gt;
&lt;li&gt;The subnet's route table has &lt;code&gt;0.0.0.0/0 → IGW&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Both the security group and the NACL allow the inbound traffic on that port.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any one missing produces the same symptom: a silent timeout with no obvious pointer to the actual cause. This is where most "why can't I reach my instance" tickets originate.&lt;/p&gt;




&lt;h2&gt;
  
  
  NAT Gateway: outbound only
&lt;/h2&gt;

&lt;p&gt;A NAT gateway lives in a specific subnet in a specific AZ, performs source NAT for private instances, and has real hourly and per-GB cost. The packet walk for a private instance at &lt;code&gt;10.0.2.15&lt;/code&gt; requesting a public registry:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Private subnet route table: &lt;code&gt;0.0.0.0/0 → nat-0abc...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;NAT gateway rewrites source to its own Elastic IP + ephemeral port&lt;/li&gt;
&lt;li&gt;NAT gateway's public subnet route table: &lt;code&gt;0.0.0.0/0 → IGW&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;IGW performs its own separate 1:1 NAT translation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two distinct NAT translations — easy to collapse into one mental step, but they're separate resources doing separate jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it lives in the public subnet:&lt;/strong&gt; the NAT gateway needs its own route to the IGW, so it must sit in a subnet whose route table already points to the IGW. The private subnet's route table then points &lt;code&gt;0.0.0.0/0&lt;/code&gt; at the NAT gateway. Two different route tables, two different subnets, one resource bridging them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HA pattern:&lt;/strong&gt; one NAT gateway per AZ, each AZ's private subnet routing to the NAT gateway in its own AZ. One NAT gateway for the whole VPC is cheaper but creates a single point of failure — if that AZ has an outage, every private subnet in every other AZ loses outbound internet access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost trap worth auditing:&lt;/strong&gt; traffic to S3 and DynamoDB from private subnets doesn't need to go through NAT at all if you use VPC Gateway Endpoints. Routing S3 traffic through NAT is billed per GB with no benefit over a free Gateway Endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The summary covers the core mental model. The full article goes deeper on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The reverse-engineering formula for subnet sizing applied to autoscaling groups and EKS node groups, with the specific IP exhaustion failure mode explained&lt;/li&gt;
&lt;li&gt;The full route table example with VPC peering and S3 Gateway Endpoint entries, and why most-specific-match matters for overlapping routes&lt;/li&gt;
&lt;li&gt;IGW statelessness and why NACLs require explicit ephemeral port rules (&lt;code&gt;1024–65535&lt;/code&gt;) that security groups handle automatically&lt;/li&gt;
&lt;li&gt;NAT Gateway connection tracking limits: 55,000 concurrent connections per unique destination, &lt;code&gt;PortAllocationErrors&lt;/code&gt; in CloudWatch as the signal, and when to reconsider architecture vs. adding more NAT gateways&lt;/li&gt;
&lt;li&gt;The full security group vs. NACL comparison — stateful vs. stateless evaluation, allow-only vs. allow-and-deny, and why the default NACL and default security group behave differently out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/aws-vpc-networking-fundamentals/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=aws-vpc-networking-fundamentals" rel="noopener noreferrer"&gt;AWS VPC Networking Fundamentals — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>vpc</category>
      <category>terraform</category>
      <category>aws</category>
    </item>
    <item>
      <title>Microservices by Default: The Organizational Constraints Nobody Puts in the Architecture Diagram</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:29:39 +0000</pubDate>
      <link>https://dev.to/aloknecessary/microservices-by-default-the-organizational-constraints-nobody-puts-in-the-architecture-diagram-258l</link>
      <guid>https://dev.to/aloknecessary/microservices-by-default-the-organizational-constraints-nobody-puts-in-the-architecture-diagram-258l</guid>
      <description>&lt;p&gt;"We're moving to microservices" has been the architectural ambition of engineering teams for a decade. More than 40% of organisations now report regretting at least some of those decisions. One team's consolidation back to a monolith: response times improved 13x, AWS costs dropped from $18K to $2.4K/month, deployment time fell from 45 minutes to 6 minutes.&lt;/p&gt;

&lt;p&gt;These are not teams that failed at microservices. They implemented them correctly — and discovered the operational premium was not proportional to the benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Distributed System Tax
&lt;/h2&gt;

&lt;p&gt;Every network boundary introduces failure modes that don't exist in-process: retries, circuit breakers, idempotency, distributed tracing, timeout budgets, schema versioning. This tax consumes 30–50% of engineering capacity once you cross the process boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What you pay per service boundary:

Network:      Retries, circuit breakers, timeouts, idempotency keys
Observability: Distributed tracing, per-service dashboards, N on-call rotations
Deployment:   N pipelines, N rollback procedures, compatibility windows
Data:         No foreign keys, eventual consistency, data duplication, sagas

Multiply by number of services. Then ask: does the benefit justify this?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conway's Law Is a Constraint, Not a Suggestion
&lt;/h2&gt;

&lt;p&gt;Microservices deliver their promise only when the team structure supports end-to-end ownership without cross-team coordination for routine changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer:  "We're splitting into order-service, inventory-service,
             and payment-service."
Architect:  "Which team owns each?"
Developer:  "The same team. Us."
Architect:  "So you're paying the distributed system tax for three
             services, with no autonomy dividend, because one team
             owns all three."
Developer:  "..."
Architect:  "How many engineers do you have?"
Developer:  "Seven."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inverse Conway Maneuver: restructure teams first. The architecture follows. Decomposing architecture first and hoping teams catch up is how seven engineers end up debugging a distributed trace across five services for a bug that would have been a five-line stack trace in a monolith.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Microservices Are Premature
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer than three independent teams&lt;/strong&gt; — communication overhead of service boundaries exceeds coordination overhead of a shared codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domains still evolving&lt;/strong&gt; — wrong service boundaries require data migration and interface deprecation; wrong module boundaries are a refactoring task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary bottleneck isn't deployment frequency&lt;/strong&gt; — microservices won't fix slow tests, hiring, or infrastructure cost&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When Microservices ARE Worth It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Multiple autonomous teams with clear domain ownership&lt;/li&gt;
&lt;li&gt;Genuinely divergent scaling requirements (&amp;gt;5x difference between components)&lt;/li&gt;
&lt;li&gt;Compliance isolation requirements (PCI-DSS, HIPAA)&lt;/li&gt;
&lt;li&gt;High deployment frequency at scale (dozens of deploys/day across teams)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Modular Monolith — The Default That Gets Skipped
&lt;/h2&gt;

&lt;p&gt;A single deployable with enforced module boundaries aligned to business domains. Not a monolith with good intentions — a monolith where boundaries are enforced by build tooling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/src/modules
  /orders/api          ← public interface (what other modules may call)
  /orders/domain       ← internal (not accessible from other modules)
  /payments/api
  /payments/domain
  /inventory/api
  /inventory/domain

Rule: orders may only import from payments/api
      Enforced by ArchUnit, NDepend, or equivalent

Benefit: when you extract payments into a service,
         the boundary is already clean. The API is already defined.
         The migration is a deployment change, not a redesign.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Decomposition Decision Framework
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: What specific problem is decomposition solving?
Step 2: Do the organisational prerequisites exist?
        - Team exists to own the service end-to-end?
        - Team can deploy without coordinating with others?
        - Service interface is stable?
Step 3: Use the strangler fig — extract one bounded context at a time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Warning Signs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You have a distributed monolith:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Services deployed together in fixed order&lt;/li&gt;
&lt;li&gt;Changes to Service A always require changes to Service B&lt;/li&gt;
&lt;li&gt;Production incidents involve debugging 4+ services simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your monolith doesn't need decomposing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy in under 10 minutes&lt;/li&gt;
&lt;li&gt;Teams work in clearly separated namespaces&lt;/li&gt;
&lt;li&gt;On-call incidents debuggable with a single log stream&lt;/li&gt;
&lt;li&gt;Can scale horizontally without issues&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;This is a summary of the fourth post in the Cloud Defaults Reconsidered series. The full article includes the complete distributed system tax breakdown, Conway's Law analysis, modular monolith implementation patterns, and the full decomposition decision framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/microservices-by-default/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=microservices-by-default" rel="noopener noreferrer"&gt;Microservices by Default — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full article includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete distributed system tax enumeration (network, observability, deployment, data)&lt;/li&gt;
&lt;li&gt;Conway's Law and the Inverse Conway Maneuver in depth&lt;/li&gt;
&lt;li&gt;Three common misconceptions debunked (scaling, deployment safety, "get boundaries right later")&lt;/li&gt;
&lt;li&gt;Modular monolith implementation with enforcement tooling&lt;/li&gt;
&lt;li&gt;Warning signs for distributed monolith vs well-structured monolith&lt;/li&gt;
&lt;li&gt;Full decomposition decision framework (pain point identification, prerequisite checklist, strangler fig extraction)&lt;/li&gt;
&lt;li&gt;Key takeaways with actionable thresholds&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>microservices</category>
      <category>architecture</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>GitOps Repo Structure and Application Patterns: App-of-Apps, ApplicationSets, and AppProjects</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:34:10 +0000</pubDate>
      <link>https://dev.to/aloknecessary/gitops-repo-structure-and-application-patterns-app-of-apps-applicationsets-and-appprojects-2ae4</link>
      <guid>https://dev.to/aloknecessary/gitops-repo-structure-and-application-patterns-app-of-apps-applicationsets-and-appprojects-2ae4</guid>
      <description>&lt;p&gt;Once you have ArgoCD running across clusters — hub-and-spoke or per-cluster — the next question is how to organize what it deploys. How should Git repositories be structured? When does App-of-Apps break down? What actually stops one team from deploying into another team's namespace?&lt;/p&gt;

&lt;p&gt;Those decisions compound. Get the repo structure wrong and every later choice about ApplicationSets and AppProjects inherits the mess.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo Structure: Where Change Friction Lives
&lt;/h2&gt;

&lt;p&gt;Three shapes dominate in practice. The choice isn't aesthetic — it determines where friction lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mono-repo&lt;/strong&gt; — one repository, all teams, all environments, organized by directory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wins: one place to search, one PR history, easy to see what's deployed where&lt;/li&gt;
&lt;li&gt;Costs: PR review load and merge contention scale with team count; access control is directory-level CODEOWNERS, not repo permissions — weaker isolation than compliance-conscious teams want&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repo-per-team&lt;/strong&gt; — each team owns a repository for everything it deploys.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wins: access control is a repo permission; a team's deploy cadence doesn't create merge contention with others; blast radius of a bad commit is scoped to one team&lt;/li&gt;
&lt;li&gt;Costs: cross-cutting changes (shared base image bump, cluster-wide policy) now touch N repos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repo-per-app&lt;/strong&gt; — finest grain, one repo per service.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wins: tightest blast radius and access control&lt;/li&gt;
&lt;li&gt;Costs: repo sprawl; cross-app changes are the worst version of the mono-repo problem — N repos, each requiring a separate PR&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service count&lt;/th&gt;
&lt;th&gt;Team count&lt;/th&gt;
&lt;th&gt;Access control need&lt;/th&gt;
&lt;th&gt;Recommended structure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under 8&lt;/td&gt;
&lt;td&gt;1 platform team&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Mono-repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8–30&lt;/td&gt;
&lt;td&gt;2–5 independent teams&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Repo-per-team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30+&lt;/td&gt;
&lt;td&gt;5+ teams, some with regulatory isolation&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Repo-per-app, or repo-per-team with sensitive services split out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  App-of-Apps: Simple, Until It Isn't
&lt;/h2&gt;

&lt;p&gt;The simplest multi-Application pattern: a root Application whose only job is to deploy more Applications.&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;argoproj.io/v1alpha1&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;Application&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;root-checkout-team&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;argocd&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;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-team&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example-org/checkout-team-gitops.git&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&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;argocd&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It breaks down in two predictable ways at scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync-wave ordering is not a dependency gate.&lt;/strong&gt; A &lt;code&gt;shared-config&lt;/code&gt; Application at &lt;code&gt;sync-wave: "0"&lt;/code&gt; and a &lt;code&gt;checkout-service&lt;/code&gt; at &lt;code&gt;sync-wave: "1"&lt;/code&gt; looks correct — until &lt;code&gt;shared-config&lt;/code&gt; fails to sync. ArgoCD's wave ordering governs &lt;em&gt;when&lt;/em&gt; a sync is attempted, not whether it's a hard prerequisite. &lt;code&gt;checkout-service&lt;/code&gt; still syncs on schedule, its Deployment references a ConfigMap key that was never written, and the failure surfaces as a CrashLoopBackOff with no obvious link to the upstream cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding a new cluster or environment means hand-writing a new Application manifest every time.&lt;/strong&gt; At 3 clusters and 5 services that's 15 manifests. At 12 clusters and 30 services it's 360 — every one written, not generated.&lt;/p&gt;




&lt;h2&gt;
  
  
  ApplicationSets: The Scaling Answer
&lt;/h2&gt;

&lt;p&gt;Instead of writing N Application manifests, write one ApplicationSet with a generator that produces them.&lt;/p&gt;

&lt;p&gt;The generators that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt; — generates one Application per registered cluster, optionally filtered by label. Register a new spoke cluster and Applications for it appear automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git directory&lt;/strong&gt; — generates Applications from repo structure: one per subdirectory. Add a new service directory and a new Application appears — no ApplicationSet edit required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix&lt;/strong&gt; — combines two generators, producing the cross-product. Cluster × Git directory is the combination that matters most: every service in the repo, deployed to every cluster carrying a matching label.
&lt;/li&gt;
&lt;/ul&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;argoproj.io/v1alpha1&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;ApplicationSet&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;checkout-team-services&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;argocd&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;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example-org/checkout-team-gitops.git&lt;/span&gt;
              &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
              &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/*&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;clusters&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="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
                  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{path.basename}}-{{name}}'&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;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-team&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example-org/checkout-team-gitops.git&lt;/span&gt;
        &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{path}}'&lt;/span&gt;
      &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{server}}'&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;checkout&lt;/span&gt;
      &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a new service directory — it deploys to every matching cluster automatically. Register a new cluster with the right labels — every existing service deploys to it automatically. Neither event requires touching the ApplicationSet.&lt;/p&gt;

&lt;p&gt;That same automatic fan-out is why an ApplicationSet change deserves a dry run before it's applied — a mistyped label selector doesn't fail one Application, it fails every Application it would have generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;argocd appset generate applicationset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This renders the full set of Applications the ApplicationSet would produce without applying anything — catching "this now matches four more clusters than intended" before it becomes a live sync.&lt;/p&gt;




&lt;h2&gt;
  
  
  AppProjects: The Multi-Tenancy Boundary
&lt;/h2&gt;

&lt;p&gt;Everything so far describes how Applications get created. It says nothing about what stops one team's ApplicationSet from deploying into another team's namespace. That boundary is &lt;code&gt;AppProject&lt;/code&gt;.&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;argoproj.io/v1alpha1&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;AppProject&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;checkout-team&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;argocd&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;sourceRepos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example-org/checkout-team-gitops.git&lt;/span&gt;
  &lt;span class="na"&gt;destinations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://spoke-eks-prod-us-east-1&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;checkout&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://spoke-eks-prod-us-east-1&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;cart&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://spoke-aks-prod-westeurope&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;checkout&lt;/span&gt;
  &lt;span class="na"&gt;clusterResourceWhitelist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
  &lt;span class="na"&gt;namespaceResourceBlacklist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&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;ResourceQuota&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&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;LimitRange&lt;/span&gt;
  &lt;span class="na"&gt;roles&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;checkout-team-sync&lt;/span&gt;
      &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;p, proj:checkout-team:checkout-team-sync, applications, sync, checkout-team/*, allow&lt;/span&gt;
      &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;checkout-team-engineers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth being deliberate about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sourceRepos&lt;/code&gt; should list the team's actual repos, not &lt;code&gt;*&lt;/code&gt;.&lt;/strong&gt; A wildcarded source list means any Application in this project can pull manifests from anywhere — defeating the point of repo-per-team access control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;destinations&lt;/code&gt; should be an explicit cluster/namespace list, not a wildcard.&lt;/strong&gt; This is the line that stops a checkout-team ApplicationSet, misconfigured or compromised, from deploying into a payments-team namespace. A Matrix generator with a slightly-too-broad label selector (&lt;code&gt;env: prod&lt;/code&gt; instead of &lt;code&gt;team: checkout, env: prod&lt;/code&gt;) will happily generate Applications targeting every production cluster it can see. Without an AppProject destinations list, ArgoCD will attempt every one of them. The label selector mistake is the proximate cause; the AppProject destinations list is what turns that mistake into a rejected sync instead of an actual cross-team deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;clusterResourceWhitelist: []&lt;/code&gt;&lt;/strong&gt; is a deliberately restrictive default. Most application teams have no legitimate reason to create &lt;code&gt;ClusterRole&lt;/code&gt;s or &lt;code&gt;Namespace&lt;/code&gt;s through their own AppProject. Reserve that capability for a separate, tightly held platform-team project.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Three Pieces Compose
&lt;/h2&gt;

&lt;p&gt;Repo structure determines what the ApplicationSet's Git generator sees. The ApplicationSet's Matrix generator combines that with cluster registration to produce Applications — if you haven't read &lt;a href="https://aloknecessary.in/blogs/multi-cluster-argocd-architecture/" rel="noopener noreferrer"&gt;Multi-Cluster ArgoCD Architecture&lt;/a&gt;, that's the foundation this builds on. The AppProject referenced by every one of those Applications keeps the resulting sync operations inside the boundary that repo was ever supposed to have.&lt;/p&gt;

&lt;p&gt;Skip AppProjects and the first two pieces still function — they just function without a backstop, which tends to be fine until the day it very much isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The full article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detailed cost/benefit analysis for all three repo structure shapes with the decision table&lt;/li&gt;
&lt;li&gt;App-of-Apps sync-wave failure mode explained in full with the CrashLoopBackOff scenario&lt;/li&gt;
&lt;li&gt;Complete Matrix generator YAML with all four ApplicationSet generator types explained&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;argocd appset generate&lt;/code&gt; dry-run workflow for catching label selector mistakes before they go live&lt;/li&gt;
&lt;li&gt;Full AppProject YAML with &lt;code&gt;sourceRepos&lt;/code&gt;, &lt;code&gt;destinations&lt;/code&gt;, &lt;code&gt;clusterResourceWhitelist&lt;/code&gt;, &lt;code&gt;namespaceResourceBlacklist&lt;/code&gt;, and role definitions&lt;/li&gt;
&lt;li&gt;The cross-team deployment scenario that AppProject destinations prevent — and why the label selector mistake alone isn't enough to cause it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/gitops-repo-structure-application-patterns/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=gitops-repo-structure-application-patterns" rel="noopener noreferrer"&gt;GitOps Repo Structure and Application Patterns — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>argocd</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Multi-Agent Systems Architecture: Patterns, Pitfalls, and Production Reality</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:32:23 +0000</pubDate>
      <link>https://dev.to/aloknecessary/multi-agent-systems-architecture-patterns-pitfalls-and-production-reality-35h3</link>
      <guid>https://dev.to/aloknecessary/multi-agent-systems-architecture-patterns-pitfalls-and-production-reality-35h3</guid>
      <description>&lt;p&gt;An agent stuck in an infinite retry loop doesn't show up in your error rate. It shows up in your AWS bill — eleven days later.&lt;/p&gt;

&lt;p&gt;A team ran a multi-agent system for eleven days. Latency normal. Error rate 0.0%. Every dashboard green. Cloud bill: $47,000. The agents were stuck in an infinite retry loop the entire time — because "wrong" doesn't show up in Grafana.&lt;/p&gt;

&lt;p&gt;This incident is representative of a failure class unique to multi-agent architecture: agents operating correctly by every infrastructure metric while producing incorrect, looping, or redundant behavior at the application layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Multi-Agent Is Not the Answer
&lt;/h2&gt;

&lt;p&gt;Most teams reach for multi-agent too early. Stay with a single agent when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The task fits in one context window (200K Claude / 1M Gemini)&lt;/li&gt;
&lt;li&gt;Latency SLA is under 10 seconds&lt;/li&gt;
&lt;li&gt;Your single-agent baseline isn't tuned yet&lt;/li&gt;
&lt;li&gt;The work is sequential (no parallelism gain)&lt;/li&gt;
&lt;li&gt;You don't have observability infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multi-agent earns its complexity for: parallel subtasks, distinct tool access requirements, or failure isolation needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four Coordination Patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Orchestrator-Worker&lt;/strong&gt; (~70% of production deployments): central planner decomposes tasks, dispatches to specialist workers, synthesizes results. Single point of failure but best observability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential Pipeline&lt;/strong&gt;: fixed linear chain. Deterministic, easy to debug, but latency stacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic Handoff (Swarm)&lt;/strong&gt;: no central coordinator — agents decide who handles what at runtime. Flexible but prone to infinite handoff loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hierarchical&lt;/strong&gt;: orchestrators managing sub-orchestrators. For large-scale decomposition where a single orchestrator can't hold the full planning state.&lt;/p&gt;




&lt;h2&gt;
  
  
  State Persistence: Demo vs Production
&lt;/h2&gt;

&lt;p&gt;The single capability that separates them: persisting intermediate results so workflows can pause, resume, and retry individual failed agents without restarting from scratch. Checkpoint at every task-level status transition to durable storage (Postgres, Temporal).&lt;/p&gt;




&lt;h2&gt;
  
  
  Inter-Agent Contracts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; for agent-to-tool communication. &lt;strong&gt;A2A&lt;/strong&gt; for agent-to-agent. Beyond protocols: every handoff needs a typed, validated Pydantic model. Validation runs at the boundary — not three agents downstream when bad data causes inscrutable errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Controls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Budget per workflow run&lt;/strong&gt; — total token budget tracked and enforced across all agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model tier routing&lt;/strong&gt; — frontier model only for reasoning-heavy steps; Haiku for routing/classification/formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard retry ceiling&lt;/strong&gt; — &lt;code&gt;attempt_count &amp;gt;= 3&lt;/code&gt; terminates, not retries forever&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential before parallel&lt;/strong&gt; — fan-out increases total spend even when it reduces latency&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Observability: Detecting "Wrong"
&lt;/h2&gt;

&lt;p&gt;Standard metrics detect whether agents are &lt;em&gt;running&lt;/em&gt;. Multi-agent systems need metrics that detect whether agents are &lt;em&gt;making progress&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflow completion rate&lt;/strong&gt; — not error rate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry anomaly detection&lt;/strong&gt; — total retries significantly exceeding task count = stuck loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost per workflow run&lt;/strong&gt; — token counts aggregated per workflow ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-agent failure rate&lt;/strong&gt; — which role is the common failure point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The $47K alert: flag any workflow where &lt;code&gt;sum(attempt_count) &amp;gt; 2 * len(tasks)&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;This is a summary of my deep dive into multi-agent production architecture. The full article covers all patterns with implementation examples:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/multi-agent-systems-architecture/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=multi-agent-systems-architecture" rel="noopener noreferrer"&gt;Multi-Agent Systems Architecture — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full article includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When multi-agent is NOT the answer (5 heuristics)&lt;/li&gt;
&lt;li&gt;Four coordination patterns with architecture diagrams&lt;/li&gt;
&lt;li&gt;Orchestration vs choreography comparison table&lt;/li&gt;
&lt;li&gt;WorkflowState persistence with checkpoint-level granularity (Python dataclasses)&lt;/li&gt;
&lt;li&gt;Inter-agent contracts with Pydantic validation&lt;/li&gt;
&lt;li&gt;MCP and A2A protocol roles explained&lt;/li&gt;
&lt;li&gt;Circuit breaker implementation for agent calls&lt;/li&gt;
&lt;li&gt;Saga pattern for side-effectful agent workflows&lt;/li&gt;
&lt;li&gt;Cost control patterns (budget caps, model tiers, retry ceilings)&lt;/li&gt;
&lt;li&gt;OTel tracing decorator for agent calls&lt;/li&gt;
&lt;li&gt;Retry anomaly detection alert logic&lt;/li&gt;
&lt;li&gt;Production deployment checklist (15 items)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>python</category>
    </item>
    <item>
      <title>Service Mesh Everywhere? The Operational Cost of Cluster-Wide mTLS</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:05:14 +0000</pubDate>
      <link>https://dev.to/aloknecessary/service-mesh-everywhere-the-operational-cost-of-cluster-wide-mtls-4pop</link>
      <guid>https://dev.to/aloknecessary/service-mesh-everywhere-the-operational-cost-of-cluster-wide-mtls-4pop</guid>
      <description>&lt;p&gt;"Add a service mesh" has become the Kubernetes equivalent of "make everything private." The reasoning seems unassailable: mTLS, observability, traffic management — all handled transparently. But the recommendation rarely comes with the operational bill attached.&lt;/p&gt;

&lt;p&gt;At 1,000 pods, traditional Envoy sidecars consume approximately 70 GB of memory — before a single byte of application traffic. The latency overhead of Istio's traditional sidecar mTLS: +166% at high load. Compared to 8% for Istio Ambient and 33% for Linkerd.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Service Mesh Actually Provides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mTLS&lt;/strong&gt; — encrypted, mutually authenticated pod-to-pod communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic management&lt;/strong&gt; — weighted routing, canary, fault injection, circuit breaking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — automatic golden signals for every service-to-service call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy&lt;/strong&gt; — authorisation rules enforced at the network layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does NOT provide: free performance, simpler operations, automatic security, or a substitute for application-level observability.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Cost
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional sidecar overhead (Istio + Envoy):
  1,000 pods: ~72 GB memory overhead
  Monthly cost for proxy memory alone: ~$4,284

Latency at high load:
  Istio sidecar:  +166%
  Linkerd:        +33%
  Istio Ambient:  +8%
  Cilium (L7):    +99%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus: operational overhead of debugging Envoy config, pilot reconciliation, sidecar injection, and mesh upgrade coordination.&lt;/p&gt;




&lt;h2&gt;
  
  
  When a Mesh Is Unnecessary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&amp;lt; 10 services&lt;/strong&gt; — NetworkPolicy + cert-manager covers it at a fraction of the cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev/staging clusters&lt;/strong&gt; — adds debugging surface without production benefit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monolithic deployments&lt;/strong&gt; — one service-to-service call doesn't justify a mesh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams without dedicated platform ownership&lt;/strong&gt; — the mesh will add more incidents than it prevents&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When a Mesh IS Worth It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;50+ services across multiple teams with genuine policy requirements&lt;/li&gt;
&lt;li&gt;Canary deployments and traffic shaping at scale&lt;/li&gt;
&lt;li&gt;Regulated environments with explicit mTLS and auditability requirements&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The 2026 Landscape Has Changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Istio Ambient Mode&lt;/strong&gt; (GA in 1.25): per-node ztunnel handles L4 mTLS, 90%+ memory reduction, 8% latency overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cilium Service Mesh&lt;/strong&gt;: eBPF at kernel level, no sidecar, 40–60% network overhead reduction vs traditional proxies.&lt;/p&gt;

&lt;p&gt;If overhead was your objection before mid-2025, re-evaluate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lighter Alternatives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mTLS&lt;/strong&gt;: cert-manager + NetworkPolicy + IRSA/Workload Identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: OpenTelemetry with DaemonSet collector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic management&lt;/strong&gt;: Argo Rollouts + NGINX Ingress weighted routing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;This is a summary of the third post in the Cloud Defaults Reconsidered series. The full article includes the complete decision framework, detailed cost breakdowns, operational overhead analysis, and architecture selection guide:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/service-mesh-everywhere/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=service-mesh-everywhere" rel="noopener noreferrer"&gt;Service Mesh Everywhere? — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The full article includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory and latency overhead calculations at scale&lt;/li&gt;
&lt;li&gt;Operational debugging scenarios and upgrade coordination costs&lt;/li&gt;
&lt;li&gt;Common misconceptions debunked (mTLS ≠ mesh, "free" observability, upgrade safety)&lt;/li&gt;
&lt;li&gt;When a mesh is unnecessary vs when it's justified&lt;/li&gt;
&lt;li&gt;Istio Ambient vs Cilium vs Linkerd vs traditional sidecars comparison&lt;/li&gt;
&lt;li&gt;Complete 3-step decision framework&lt;/li&gt;
&lt;li&gt;Lighter alternatives that close 80% of the gap&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>architecture</category>
      <category>servicemesh</category>
    </item>
    <item>
      <title>Multi-Cluster ArgoCD Architecture: Hub-and-Spoke vs. Per-Cluster, Done Right</title>
      <dc:creator>Alok Ranjan Daftuar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:48:33 +0000</pubDate>
      <link>https://dev.to/aloknecessary/multi-cluster-argocd-architecture-hub-and-spoke-vs-per-cluster-done-right-p17</link>
      <guid>https://dev.to/aloknecessary/multi-cluster-argocd-architecture-hub-and-spoke-vs-per-cluster-done-right-p17</guid>
      <description>&lt;p&gt;Every ArgoCD tutorial ends with one cluster, one ArgoCD instance, and &lt;code&gt;kubectl config current-context&lt;/code&gt; pointing at the same place ArgoCD is installed. That works fine — until you have two clusters. By the time you're at 10 or 15, across AWS and Azure, the architecture you picked on day one is either quietly paying for itself or quietly costing you an incident a quarter.&lt;/p&gt;

&lt;p&gt;The two dominant patterns — hub-and-spoke and ArgoCD-per-cluster — are both reasonable in the right context and both wrong when applied past the cluster count and team topology they were designed for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Bad Defaults Teams Back Into
&lt;/h2&gt;

&lt;p&gt;Rather than choosing deliberately, most teams end up at one of two failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD-per-cluster sprawl&lt;/strong&gt; — every cluster gets its own ArgoCD because that's what the getting-started guide showed. Nobody has a single view of what's deployed where. Upgrading ArgoCD becomes N separate change requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single hub that becomes a bottleneck&lt;/strong&gt; — one team stands up ArgoCD once, registers every cluster against it, and doesn't revisit that decision until the application controller is falling behind on reconciliation or a hub outage takes down deployments for every team at once.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Pattern 1: Hub-and-Spoke
&lt;/h2&gt;

&lt;p&gt;One management cluster runs ArgoCD. Every other cluster is registered as a remote destination via a &lt;code&gt;Secret&lt;/code&gt; containing that cluster's API server address and credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; single pane of glass, centralized RBAC and SSO, one audit trail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs you:&lt;/strong&gt; the hub becomes a scaling bottleneck at high cluster/Application count; blast radius covers every spoke if the hub is compromised; network reachability to every spoke's API server is a real design requirement, not just an IAM policy.&lt;/p&gt;

&lt;p&gt;Two blast-radius shapes worth planning for specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis under memory pressure&lt;/strong&gt; — ArgoCD's application controller caches live-vs-desired state in Redis across every Application it manages. An OOM or eviction storm on that single Redis instance stalls reconciliation for every cluster the hub manages simultaneously. From the outside it looks like "ArgoCD is stuck everywhere" — the root cause is capacity planning on a component most teams treat as an implementation detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A quietly broken peering path&lt;/strong&gt; — a security group rule tightened on one spoke's cluster security group doesn't fail loudly. It shows up as that spoke going &lt;code&gt;Unknown&lt;/code&gt; in the UI while everything else stays green, easy to dismiss as a blip until someone needs to ship a fix to that cluster and can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Network Reachability — The Part Tutorials Skip
&lt;/h3&gt;

&lt;p&gt;On &lt;strong&gt;AWS&lt;/strong&gt;: VPC peering or Transit Gateway gets the hub's control plane traffic to each spoke's private EKS API server endpoint. Private API server endpoints are the right default for spoke clusters, but every private-by-default decision adds a network path the hub now has to be deliberately connected to.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;Azure&lt;/strong&gt;: VNet peering or Azure Private Link between the hub's VNet and each spoke AKS cluster's VNet. Same shape of problem — the hub's egress needs a routable, authorized path to a spoke's control plane.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controller Sharding Past 10 Clusters
&lt;/h3&gt;

&lt;p&gt;A hub managing 12 spoke clusters with ~50 Applications each (600 total) on a single unsharded controller will start lagging live cluster state by 3-4 minutes during normal operation, and considerably longer after a bulk change. The fix is multiple controller replicas with shard annotations on cluster secrets:&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="c1"&gt;# argocd-application-controller StatefulSet&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;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cluster Secret with shard annotation&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;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;argocd.argoproj.io/shard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ARGOCD_CONTROLLER_REPLICAS=4&lt;/code&gt; and shard annotations spread across 12 spoke secrets, each replica watches 3 clusters instead of all 12 — reconciliation lag drops back to single-digit seconds. This change isn't complete without also moving to a Redis HA (Sentinel-backed) deployment, since a single Redis becomes the new bottleneck once four controller replicas hit it concurrently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2: ArgoCD-per-Cluster
&lt;/h2&gt;

&lt;p&gt;Each cluster runs its own ArgoCD instance and manages only itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; fault isolation, no cross-cluster network dependency, clean blast radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs you:&lt;/strong&gt; N places to upgrade and patch; fragmented visibility; RBAC and project config duplicated N times with all the drift that implies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this genuinely wins:&lt;/strong&gt; regulated environments where a cluster's isolation boundary is a compliance requirement; air-gapped clusters with no viable network path back to a central hub; edge deployments where each site operates independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cluster Registration — EKS
&lt;/h2&gt;

&lt;p&gt;The clean way to register a spoke EKS cluster is IRSA or EKS access entries — not a static kubeconfig with a long-lived token. The cluster secret uses the &lt;code&gt;aws eks get-token&lt;/code&gt; exec plugin:&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;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;Secret&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;spoke-eks-prod-us-east-1&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;argocd&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;argocd.argoproj.io/secret-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&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;spoke-eks-prod-us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://EXAMPLE1234567890.gr7.us-east-1.eks.amazonaws.com&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"execProviderConfig": {&lt;/span&gt;
        &lt;span class="s"&gt;"command": "aws",&lt;/span&gt;
        &lt;span class="s"&gt;"args": [&lt;/span&gt;
          &lt;span class="s"&gt;"eks", "get-token",&lt;/span&gt;
          &lt;span class="s"&gt;"--cluster-name", "spoke-eks-prod",&lt;/span&gt;
          &lt;span class="s"&gt;"--region", "us-east-1",&lt;/span&gt;
          &lt;span class="s"&gt;"--role-arn", "arn:aws:iam::111122223333:role/argocd-hub-spoke-access"&lt;/span&gt;
        &lt;span class="s"&gt;],&lt;/span&gt;
        &lt;span class="s"&gt;"apiVersion": "client.authentication.k8s.io/v1beta1"&lt;/span&gt;
      &lt;span class="s"&gt;},&lt;/span&gt;
      &lt;span class="s"&gt;"tlsClientConfig": { "insecure": false, "caData": "&amp;lt;base64 cluster CA&amp;gt;" }&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full chain: create the IAM role on the spoke account with a trust policy scoped to the hub's IRSA role → create an EKS access entry mapping that role to a Kubernetes group → bind that group to a scoped &lt;code&gt;ClusterRole&lt;/code&gt; (not cluster-admin) → grant the hub's IRSA service account &lt;code&gt;sts:AssumeRole&lt;/code&gt; → apply the Secret → verify with &lt;code&gt;argocd cluster list&lt;/code&gt;. A &lt;code&gt;Successful&lt;/code&gt; status confirms the exec plugin chain worked end to end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cluster Registration — AKS
&lt;/h2&gt;

&lt;p&gt;On AKS, the equivalent is Azure AD Workload Identity with the &lt;code&gt;kubelogin&lt;/code&gt; exec plugin:&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;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;Secret&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;spoke-aks-prod-westeurope&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;argocd&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;argocd.argoproj.io/secret-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&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;spoke-aks-prod-westeurope&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://spoke-aks-prod-dns-a1b2c3d4.hcp.westeurope.azmk8s.io&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"execProviderConfig": {&lt;/span&gt;
        &lt;span class="s"&gt;"command": "kubelogin",&lt;/span&gt;
        &lt;span class="s"&gt;"args": [&lt;/span&gt;
          &lt;span class="s"&gt;"get-token",&lt;/span&gt;
          &lt;span class="s"&gt;"--login", "workloadidentity",&lt;/span&gt;
          &lt;span class="s"&gt;"--server-id", "6dae42f8-4368-4678-94ff-3960e28e3630"&lt;/span&gt;
        &lt;span class="s"&gt;],&lt;/span&gt;
        &lt;span class="s"&gt;"apiVersion": "client.authentication.k8s.io/v1beta1"&lt;/span&gt;
      &lt;span class="s"&gt;},&lt;/span&gt;
      &lt;span class="s"&gt;"tlsClientConfig": { "insecure": false, "caData": "&amp;lt;base64 cluster CA&amp;gt;" }&lt;/span&gt;
    &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full chain: create an Azure AD application + federated identity credential binding the hub's ArgoCD service account to it → grant that application a scoped AKS RBAC role on the spoke cluster → label the hub's ArgoCD service account for workload identity → apply the Secret → verify with &lt;code&gt;argocd cluster list&lt;/code&gt;. Auth failures here are almost always a &lt;code&gt;--subject&lt;/code&gt; mismatch in the federated credential or the workload identity label missing from the pod spec.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cluster count&lt;/th&gt;
&lt;th&gt;Compliance boundary&lt;/th&gt;
&lt;th&gt;Team topology&lt;/th&gt;
&lt;th&gt;Recommended pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Single platform team&lt;/td&gt;
&lt;td&gt;Hub-and-spoke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-5&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Single or lightly federated&lt;/td&gt;
&lt;td&gt;Hub-and-spoke with AppProjects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-10&lt;/td&gt;
&lt;td&gt;Some (staging vs. prod)&lt;/td&gt;
&lt;td&gt;Multiple product teams&lt;/td&gt;
&lt;td&gt;Hub-and-spoke, watch controller sharding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-15&lt;/td&gt;
&lt;td&gt;Regulatory/contractual isolation on specific clusters&lt;/td&gt;
&lt;td&gt;Multiple teams, some regulated&lt;/td&gt;
&lt;td&gt;Hybrid — hub for general fleet, per-cluster for isolated outliers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;Air-gapped / no viable hub network path&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;td&gt;ArgoCD-per-cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hybrid row in practice: a platform team running 13 clusters — 10 standard clusters registering against the hub via IRSA/workload identity, plus 3 PCI-scoped clusters each running their own ArgoCD with no network path back to the hub, syncing from a separate access-restricted Git repository. The platform team accepts fragmented visibility for those 3 clusters in exchange for not having to argue, in every audit cycle, that the hub's blast radius doesn't touch the PCI boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Full Article
&lt;/h2&gt;

&lt;p&gt;The full article includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hub-and-spoke network reachability deep-dive for EKS (VPC peering, private API server endpoints, Transit Gateway) and AKS (VNet peering, Azure Private Link)&lt;/li&gt;
&lt;li&gt;Complete step-by-step EKS auth chain: IAM role creation, EKS access entry, ClusterRoleBinding, hub-side IRSA policy&lt;/li&gt;
&lt;li&gt;Complete step-by-step AKS auth chain: Azure AD app, federated credential, AKS RBAC role assignment, workload identity label&lt;/li&gt;
&lt;li&gt;Controller sharding mechanics and Redis HA requirements for fleets past 10-15 clusters&lt;/li&gt;
&lt;li&gt;The trust-boundary thinking connecting IRSA/Workload Identity to ArgoCD's cluster auth model&lt;/li&gt;
&lt;li&gt;What's coming in Article 2: repo structure, App-of-Apps, ApplicationSets, and AppProjects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;👉 &lt;a href="https://aloknecessary.in/blogs/multi-cluster-argocd-architecture/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blog_syndication&amp;amp;utm_content=multi-cluster-argocd-architecture" rel="noopener noreferrer"&gt;Multi-Cluster ArgoCD Architecture — Full Article&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>gitops</category>
      <category>devops</category>
      <category>argocd</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
