<?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: Raphael Trovão</title>
    <description>The latest articles on DEV Community by Raphael Trovão (@rgtrovao).</description>
    <link>https://dev.to/rgtrovao</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%2F4016314%2Ff3aca356-a625-4c16-ac23-19ddeec8be44.png</url>
      <title>DEV Community: Raphael Trovão</title>
      <link>https://dev.to/rgtrovao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rgtrovao"/>
    <language>en</language>
    <item>
      <title>Hybrid DNS on AWS That Actually Works: Route 53 Resolver Endpoints End to End</title>
      <dc:creator>Raphael Trovão</dc:creator>
      <pubDate>Sun, 12 Jul 2026 19:29:14 +0000</pubDate>
      <link>https://dev.to/rgtrovao/hybrid-dns-on-aws-that-actually-works-route-53-resolver-endpoints-end-to-end-16l7</link>
      <guid>https://dev.to/rgtrovao/hybrid-dns-on-aws-that-actually-works-route-53-resolver-endpoints-end-to-end-16l7</guid>
      <description>&lt;p&gt;Every hybrid environment I've ever touched has the same ghost story: an application in AWS that sometimes can't resolve an on-premises hostname. Not always — sometimes. It works from one VPC and not another. It works for the sysadmin testing with &lt;code&gt;nslookup&lt;/code&gt; and fails for the Java app two minutes later. Somebody eventually says "it's never DNS", everyone laughs nervously, and it's DNS.&lt;/p&gt;

&lt;p&gt;I run the AWS side of a hybrid environment for a court system — 22 accounts, production EKS, and a datacenter full of systems that aren't going anywhere. Our cloud workloads need to resolve internal names that live on on-prem DNS servers, and on-prem systems need to resolve names that only exist in Route 53 private hosted zones. Getting that to work reliably in both directions is one of those problems that looks simple on the whiteboard and then eats a week of your life.&lt;/p&gt;

&lt;p&gt;This is the end-to-end setup that works, plus the six mistakes I see over and over — including one that creates a genuine infinite loop between your datacenter and AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, understand who answers DNS inside a VPC
&lt;/h2&gt;

&lt;p&gt;Before touching Resolver endpoints, you need to be clear about one thing: &lt;strong&gt;every VPC already has a DNS server&lt;/strong&gt;. It lives at the VPC CIDR base plus two — for a &lt;code&gt;10.20.0.0/16&lt;/code&gt; VPC, that's &lt;code&gt;10.20.0.2&lt;/code&gt;. People call it "VPC+2", the docs call it the Amazon Route 53 Resolver, your instances get it via DHCP as &lt;code&gt;AmazonProvidedDNS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That resolver is what makes AWS DNS magic work: it answers for private hosted zones associated with the VPC, resolves AWS service endpoints (including PrivateLink and VPC endpoint names), and recurses to the public internet for everything else. Your EC2 instances, your pods (via CoreDNS, which forwards to it), your Lambda functions in a VPC — everyone asks VPC+2.&lt;/p&gt;

&lt;p&gt;Here's the thing VPC+2 can't do by itself: it has no idea your datacenter exists. Ask it for &lt;code&gt;intranet.corp.internal&lt;/code&gt; and it will go look on the public internet, come back with NXDOMAIN, and your app breaks. And the reverse is also true — your on-prem DNS servers have no idea what &lt;code&gt;db.prd.internal.example.com&lt;/code&gt; in a private hosted zone means.&lt;/p&gt;

&lt;p&gt;Route 53 Resolver endpoints are the bridge, and there are exactly two kinds because there are exactly two directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two directions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Outbound endpoint: AWS asks on-prem.&lt;/strong&gt; An outbound endpoint is a set of ENIs in your VPC that can &lt;em&gt;forward&lt;/em&gt; queries out. You pair it with &lt;strong&gt;forwarding rules&lt;/strong&gt;: "any query for &lt;code&gt;corp.internal&lt;/code&gt; → send it to &lt;code&gt;192.168.10.10&lt;/code&gt; and &lt;code&gt;192.168.10.11&lt;/code&gt;". The rule is the brain, the endpoint is the pipe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inbound endpoint: on-prem asks AWS.&lt;/strong&gt; An inbound endpoint is a set of ENIs with fixed private IPs that your on-prem DNS servers can forward &lt;em&gt;to&lt;/em&gt;. On the datacenter side, you create conditional forwarders on your AD DNS or BIND: "anything under &lt;code&gt;internal.example.com&lt;/code&gt; → ask &lt;code&gt;10.20.5.10&lt;/code&gt; and &lt;code&gt;10.20.6.10&lt;/code&gt;". Those IPs answer exactly like VPC+2 would — private hosted zones, PrivateLink names, all of it.&lt;/p&gt;

&lt;p&gt;Most real environments need both. Ours certainly does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it: the order that avoids rework
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — plan the subnets and security groups.&lt;/strong&gt; Each endpoint needs IPs in at least two AZs (that's your HA — don't cheat on this). The security group needs &lt;strong&gt;both TCP and UDP on port 53&lt;/strong&gt;. Write that down, because it's mistake #3 below and it produces the most maddening symptom in this entire article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — the outbound endpoint and rules.&lt;/strong&gt; 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_route53_resolver_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"outbound"&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;"to-onprem"&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OUTBOUND"&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_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_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolver&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;ip_address&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;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;ip_address&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;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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route53_resolver_rule"&lt;/span&gt; &lt;span class="s2"&gt;"corp"&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;"corp-internal"&lt;/span&gt;
  &lt;span class="nx"&gt;domain_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"corp.internal"&lt;/span&gt;
  &lt;span class="nx"&gt;rule_type&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FORWARD"&lt;/span&gt;
  &lt;span class="nx"&gt;resolver_endpoint_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_resolver_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbound&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;target_ip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"192.168.10.10"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;target_ip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"192.168.10.11"&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_route53_resolver_rule_association"&lt;/span&gt; &lt;span class="s2"&gt;"corp_vpc_main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;resolver_rule_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route53_resolver_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;corp&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule only takes effect in VPCs it's &lt;strong&gt;associated&lt;/strong&gt; with. That word is doing a lot of work — remember it for mistake #5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — share the rules across the Organization.&lt;/strong&gt; If you have more than a handful of accounts, do not recreate rules everywhere. Create them once in a networking account and share via AWS RAM:&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;"resolver_rules"&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;"resolver-rules"&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;"corp"&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_route53_resolver_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;corp&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;resolver_rules&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;"org"&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_organizations_organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&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;resolver_rules&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;Spoke accounts then just associate the shared rule with their VPCs. In our 22-account setup this is the difference between "DNS is managed" and "DNS is 22 slightly different snowflakes". One outbound endpoint in the hub serves the whole Organization — the rule points at the hub's endpoint regardless of which VPC the query starts in, as long as your Transit Gateway or peering can carry the traffic back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — the inbound endpoint and the on-prem side.&lt;/strong&gt;&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_route53_resolver_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"inbound"&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;"from-onprem"&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INBOUND"&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_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_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolver&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;ip_address&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;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;ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.20.5.10"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ip_address&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;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;ip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.20.6.10"&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;Pin the IPs explicitly — your on-prem DNS team will configure conditional forwarders pointing at them, and you do not want those IPs changing on a rebuild. On Windows DNS: New Conditional Forwarder → &lt;code&gt;internal.example.com&lt;/code&gt; → both inbound IPs. On BIND, a &lt;code&gt;forwarders&lt;/code&gt; block in a zone stanza. Same idea anywhere: &lt;em&gt;for this domain, ask AWS&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — test both directions before declaring victory.&lt;/strong&gt; From an EC2 instance: &lt;code&gt;dig intranet.corp.internal&lt;/code&gt; (should come back with an on-prem answer). From an on-prem box: &lt;code&gt;dig db.prd.internal.example.com @10.20.5.10&lt;/code&gt;, then again without the &lt;code&gt;@&lt;/code&gt; once the forwarder is configured. Test from a &lt;em&gt;spoke&lt;/em&gt; VPC too, not just the hub. Then test something that only exists in a private hosted zone, and something behind PrivateLink. Five minutes of testing here saves the 2am call later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six classic mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The circular forwarder (the infinite DNS loop).&lt;/strong&gt; This is the one that earns war-story status. On-prem forwards &lt;code&gt;example.com&lt;/code&gt; to the inbound endpoint. Someone later creates an &lt;em&gt;outbound&lt;/em&gt; rule forwarding &lt;code&gt;example.com&lt;/code&gt; back to on-prem — maybe because one subdomain genuinely lives there. Now a query for a name that doesn't exist anywhere bounces: on-prem → inbound → (no answer in AWS, rule matches) → outbound → on-prem → inbound → ... until something times out. Symptoms: slow SERVFAILs, resolver ENIs busy doing nothing useful, query volume that makes no sense.&lt;/p&gt;

&lt;p&gt;The rule that prevents it: &lt;strong&gt;never forward a domain to a resolver that forwards the same domain back to you.&lt;/strong&gt; Split the namespace instead — &lt;code&gt;aws.example.com&lt;/code&gt; lives in Route 53 and on-prem forwards it to inbound; &lt;code&gt;corp.example.com&lt;/code&gt; lives on-prem and AWS forwards it out. Clean boundaries, no cycles. If a domain genuinely must be split at a deeper level, forward the specific subdomains, never the parent from both sides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pointing instances at on-prem DNS via DHCP option sets.&lt;/strong&gt; It feels like the obvious shortcut: change the VPC's DHCP options to hand out &lt;code&gt;192.168.10.10&lt;/code&gt; and skip the whole endpoint conversation. Then private hosted zones stop resolving, VPC endpoint and PrivateLink names stop resolving, EKS gets weird, and every DNS packet from every instance crosses your Direct Connect. Leave &lt;code&gt;AmazonProvidedDNS&lt;/code&gt; alone and let VPC+2 do the routing via rules. The DHCP shortcut is how you trade one week of proper setup for a year of mystery incidents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Security group without TCP/53.&lt;/strong&gt; DNS uses UDP until the answer doesn't fit or DNSSEC gets involved — then it retries over TCP. If your resolver security group only allows UDP, small answers work and large ones fail. The symptom is beautiful in its cruelty: &lt;code&gt;dig&lt;/code&gt; works, the application intermittently doesn't, and the failure correlates with &lt;em&gt;which name&lt;/em&gt; you look up. Allow TCP and UDP 53, both directions that apply, and move on with your life.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Forgetting rule associations in spoke VPCs.&lt;/strong&gt; The rule exists, the endpoint works, the hub VPC resolves fine — and the new application VPC gets NXDOMAIN. Rules don't apply Organization-wide by magic; each VPC needs the association (RAM sharing gets the rule &lt;em&gt;visible&lt;/em&gt; to the account; associating it to the VPC is still a step). Bake the association into your VPC Terraform module so no one has to remember. Same story for &lt;strong&gt;private hosted zones&lt;/strong&gt; — a PHZ answers only in VPCs it's associated with, and cross-account PHZ association is a two-step CLI dance (&lt;code&gt;create-vpc-association-authorization&lt;/code&gt; on the zone owner, &lt;code&gt;associate-vpc-with-hosted-zone&lt;/code&gt; on the VPC owner) that nobody discovers until it fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Under-provisioned endpoints and mystery latency.&lt;/strong&gt; Each endpoint IP handles up to about 10,000 queries per second, and — the part people miss — &lt;strong&gt;VPC+2 itself caps at 1,024 packets per second per ENI&lt;/strong&gt;. A busy EKS node can hit that limit all by itself, thanks to &lt;code&gt;ndots:5&lt;/code&gt; turning every lookup of &lt;code&gt;api.example.com&lt;/code&gt; into five queries. The fix on the cluster side is NodeLocal DNSCache (and sensible &lt;code&gt;ndots&lt;/code&gt; in pod specs); on the endpoint side, add IPs before you need them and watch the &lt;code&gt;InboundQueryVolume&lt;/code&gt;/&lt;code&gt;OutboundQueryVolume&lt;/code&gt; metrics. DNS latency looks exactly like application latency to everyone upstream of you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Nobody can see what's being asked.&lt;/strong&gt; Turn on &lt;strong&gt;Resolver query logging&lt;/strong&gt; from day one — to S3, cheap, same pattern as flow logs (I wrote a whole piece on querying that kind of data with Athena). When something misbehaves, the query log answers in one search what "can you try nslookup again?" answers in an afternoon: what was asked, by which ENI, and what came back. The pair of flow logs + query logs is the closest thing AWS gives you to network omniscience, and together they cost less than one incident bridge call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that makes it all stick
&lt;/h2&gt;

&lt;p&gt;Resolution order inside a VPC, simplified but honest: the resolver first matches your &lt;strong&gt;forwarding rules&lt;/strong&gt; (most specific domain wins), then &lt;strong&gt;private hosted zones&lt;/strong&gt; and autodefined AWS names, then public recursion. When two rules could match, the longer domain name takes precedence — &lt;code&gt;db.corp.internal&lt;/code&gt; beats &lt;code&gt;corp.internal&lt;/code&gt;. That's the entire debugging framework: for any failing name, ask "which of the three paths should this take, and which one is it actually taking?" &lt;code&gt;dig&lt;/code&gt; from inside the VPC plus the query logs answer the second half.&lt;/p&gt;

&lt;p&gt;Cost, for the FinOps-minded: endpoints are billed per ENI-hour plus per-query, and the minimum HA setup (2 IPs inbound + 2 outbound) runs a few hundred dollars a year — check current pricing for your region. Compare that to a shared services architecture where one hub endpoint pair serves 22 accounts, and the per-account cost becomes a rounding error. Centralize; this is one of the clearest hub-and-spoke wins in AWS networking.&lt;/p&gt;

&lt;p&gt;Hybrid DNS is unforgiving of improvisation and very kind to boring, explicit design: one clean namespace boundary, rules shared from a hub, TCP and UDP open, associations automated in the VPC module, query logging on. Do those things and DNS quietly stops being on your incident bingo card — which, in this line of work, is the highest compliment an infrastructure component can earn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about AWS networking from the packet up — VPC, hybrid connectivity, EKS networking. Previous article: querying VPC Flow Logs with Athena. Follow along here or on &lt;a href="https://linkedin.com/in/rgtrovao" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>dns</category>
      <category>devops</category>
    </item>
    <item>
      <title>Transit Gateway vs VPC Peering vs PrivateLink: a Decision Framework from a 22-Account Org</title>
      <dc:creator>Raphael Trovão</dc:creator>
      <pubDate>Sun, 12 Jul 2026 18:56:21 +0000</pubDate>
      <link>https://dev.to/rgtrovao/transit-gateway-vs-vpc-peering-vs-privatelink-a-decision-framework-from-a-22-account-org-3h33</link>
      <guid>https://dev.to/rgtrovao/transit-gateway-vs-vpc-peering-vs-privatelink-a-decision-framework-from-a-22-account-org-3h33</guid>
      <description>&lt;p&gt;The question lands in my queue almost every week, always wearing different clothes: "the team in account A needs to reach a service in account B — can you connect us?" Simple request. Except AWS gives you three completely different tools that all answer it, and picking the wrong one either costs you money forever or walls you in six months later.&lt;/p&gt;

&lt;p&gt;I run networking for a 22-account AWS Organization, and over the years we've used all three — Transit Gateway as our backbone, peering where it quietly saves us real money, and PrivateLink in the places where the other two would have been a mistake. This is the decision framework I wish someone had handed me at account number five, with the actual costs and the practical limits that the comparison tables usually skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three tools, without the marketing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VPC Peering is a cable.&lt;/strong&gt; A private, point-to-point connection between exactly two VPCs. Traffic flows both ways, you update route tables on both sides, done. The two things that define it: it's &lt;strong&gt;non-transitive&lt;/strong&gt; (A↔B and B↔C does not give A↔C, ever), and it has &lt;strong&gt;no hourly charge&lt;/strong&gt; — you pay only for data transfer, and traffic that stays within the same AZ over a peering costs nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transit Gateway is a router.&lt;/strong&gt; A managed hub that every VPC (and your VPN and Direct Connect) attaches to once. It's transitive by design — everyone attached can reach everyone else, &lt;em&gt;unless you say otherwise&lt;/em&gt;, and "saying otherwise" is the feature: TGW route tables let you build isolated routing domains (production can't see staging, everything inbound goes through an inspection VPC) the same way a real network engineer would with VRFs. You pay for that convenience twice: per attachment-hour and per GB processed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PrivateLink is not a network connection at all.&lt;/strong&gt; This is the one people misfile. PrivateLink exposes &lt;em&gt;one service&lt;/em&gt; — something behind a Network Load Balancer — as an ENI that materializes inside the consumer's VPC. There's no routing between the networks. No CIDRs exchanged. The consumer sees an IP in their own subnet and nothing else of yours. It's a socket, not a cable, and one-directional by design.&lt;/p&gt;

&lt;p&gt;If you remember only one line from this article: &lt;strong&gt;PrivateLink for services, peering for pairs, Transit Gateway for networks.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The money, because it decides more than we admit
&lt;/h2&gt;

&lt;p&gt;Ballpark numbers for us-east-1 at the time of writing — check the pricing pages, these move:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Hourly&lt;/th&gt;
&lt;th&gt;Per GB&lt;/th&gt;
&lt;th&gt;The catch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC Peering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;data transfer only (free same-AZ, ~$0.01/GB each side cross-AZ intra-region)&lt;/td&gt;
&lt;td&gt;cheapest by far for heavy flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transit Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$0.05 per attachment&lt;/td&gt;
&lt;td&gt;~$0.02 processing on every GB&lt;/td&gt;
&lt;td&gt;the $0.02 applies to ALL traffic, forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PrivateLink&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$0.01 per endpoint per AZ&lt;/td&gt;
&lt;td&gt;~$0.01&lt;/td&gt;
&lt;td&gt;per-service, per-consumer endpoints add up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The number that matters is Transit Gateway's &lt;strong&gt;$0.02 per GB processed&lt;/strong&gt;. It sounds like nothing until you do the multiplication that FinOps taught me to always do: a steady 1 TB/day between two accounts is roughly $600/month in TGW processing — for traffic that would ride a peering connection for a fraction of that, or for free if you keep it same-AZ. We found exactly this pattern in our environment: two accounts exchanging heavy replication traffic 24/7, dutifully routed through the TGW because "everything goes through the TGW". Moving that one pair to a direct peering paid for the time it took before the month closed.&lt;/p&gt;

&lt;p&gt;That's not an argument against TGW. It's an argument against &lt;em&gt;defaults&lt;/em&gt;. The TGW fee buys you transitivity, segmentation, and one attachment instead of N peerings — genuinely worth it for the general case. Just don't make your top-talker pairs pay the router tax when they don't need routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework: four questions in order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question 1: are you connecting networks, or exposing a service?&lt;/strong&gt; If account B just needs to &lt;em&gt;consume an API&lt;/em&gt; that account A runs, stop — that's PrivateLink. Don't connect two entire CIDR spaces so one app can reach one port. This question alone reroutes half the requests I get, and it's also the security answer: the consumer gets an ENI, not a network path. Least privilege, but for topology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 2: do the CIDRs overlap?&lt;/strong&gt; Peering flatly refuses overlapping CIDRs, and TGW routing can't fix ambiguity either. If you're connecting to an acquired company, a partner, or your own legacy sins, overlapping ranges force the answer: PrivateLink doesn't care about CIDRs at all. It's the escape hatch that turns "we'd have to re-IP the whole environment" into a Tuesday task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 3: how many VPCs, and is it growing?&lt;/strong&gt; Two or three VPCs in a stable relationship: peer them and enjoy the free lunch. But peering meshes grow as n(n−1)/2 — at 10 VPCs that's 45 peerings and 45 pairs of route table entries to maintain, and there's a hard cap of 125 peerings per VPC before AWS stops you anyway. Somewhere between 4 and 10 VPCs, the honest math flips to TGW: one attachment each, routing managed in one place, and your on-prem connectivity (VPN/Direct Connect) plugs into the same hub. Ours flipped long ago; with 22 accounts there was no real decision left to make about the backbone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 4: do you need segmentation or inspection?&lt;/strong&gt; If compliance says production and non-production must not talk, or that cross-environment traffic goes through a firewall, TGW route tables are the tool — attach the inspection VPC, propagate routes asymmetrically, done. Peering has no equivalent; you'd be building segmentation out of security groups and hope. In our court environment, this requirement alone justified the TGW before any convenience argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits the comparison tables skip
&lt;/h2&gt;

&lt;p&gt;A few practical ceilings that have bitten people I know (occasionally including me):&lt;/p&gt;

&lt;p&gt;Peering, besides being non-transitive and capped at 125 per VPC, also &lt;strong&gt;doesn't propagate DNS by default&lt;/strong&gt; — enable DNS resolution support on the connection or private hosted zone names won't resolve across it, which presents as the world's most confusing intermittent bug. Transit Gateway attachments have bandwidth ceilings (generous, up to 100 Gbps per VPC attachment these days, but VPN attachments cap at 1.25 Gbps per tunnel — plan ECMP if you need more). PrivateLink requires the service side to sit behind an NLB (or use a Gateway Load Balancer for appliances), each endpoint serves &lt;em&gt;one&lt;/em&gt; service, and if you expose many services to many consumers, the endpoint-hours quietly become their own line item — bundle related APIs behind one NLB where it makes sense.&lt;/p&gt;

&lt;p&gt;And one that spans all three: &lt;strong&gt;DNS is a separate decision&lt;/strong&gt;. Connecting the networks doesn't make names resolve — private hosted zone associations, Resolver rules and endpoint policies are their own layer, and forgetting them is the #1 "it's connected but nothing works" cause. I wrote a whole piece on hybrid DNS for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 22 accounts actually looks like
&lt;/h2&gt;

&lt;p&gt;Our topology, as a reference point rather than a prescription: a &lt;strong&gt;Transit Gateway backbone&lt;/strong&gt; in the network account with separate route domains for production, non-production and shared services, an inspection VPC on the cross-domain paths, and Direct Connect into the same hub for the on-prem datacenter. &lt;strong&gt;Peering&lt;/strong&gt; exists in exactly the places where a pair of VPCs exchanges heavy, predictable traffic and the TGW tax made no sense. &lt;strong&gt;PrivateLink&lt;/strong&gt; exposes our shared internal services to workload accounts, and it's the only pattern we allow for anything crossing an organizational trust boundary — partners see an ENI, never a route.&lt;/p&gt;

&lt;p&gt;Boring, explicit, and every flow has a reason it takes the path it takes. In multi-account networking, boring is the compliment.&lt;/p&gt;

&lt;p&gt;The framework fits in one breath: expose a service → PrivateLink. Connect a stable pair (especially a chatty one) → peering. Connect many networks with segmentation and hybrid → Transit Gateway. Ask the four questions in order, run the per-GB math on your top talkers before defaulting everything to the hub, and remember DNS is always a second decision. That's it — that's the whole trick, and it took 22 accounts to learn.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write about AWS networking from the packet up — VPC, hybrid DNS, EKS networking, and multi-account architecture. Previous articles: &lt;a href="https://dev.to/rgtrovao/vpc-flow-logs-beyond-the-console-finding-real-answers-with-athena-2gkh"&gt;VPC Flow Logs with Athena&lt;/a&gt; and hybrid DNS with Route 53 Resolver. Follow along here or on &lt;a href="https://linkedin.com/in/rgtrovao" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>VPC Flow Logs Beyond the Console: Finding Real Answers with Athena</title>
      <dc:creator>Raphael Trovão</dc:creator>
      <pubDate>Sun, 05 Jul 2026 16:02:44 +0000</pubDate>
      <link>https://dev.to/rgtrovao/vpc-flow-logs-beyond-the-console-finding-real-answers-with-athena-2gkh</link>
      <guid>https://dev.to/rgtrovao/vpc-flow-logs-beyond-the-console-finding-real-answers-with-athena-2gkh</guid>
      <description>&lt;p&gt;A few months ago someone asked me a question that sounds simple: "can you tell me everything that talked to this database in the last 48 hours?" We run a 22-account AWS Organization for a court system, so questions like that aren't curiosity. They're audit requests, and "I think it was the app servers" is not an acceptable answer.&lt;/p&gt;

&lt;p&gt;The console's Flow Logs viewer gets you nowhere with that question. CloudWatch Logs Insights can do it, but at CloudWatch ingestion prices, keeping flow logs there across a whole Organization is the kind of line item that shows up in your FinOps review with a red circle around it.&lt;/p&gt;

&lt;p&gt;The setup that actually works, and costs a fraction: Flow Logs delivered to S3 in Parquet, a lifecycle policy so old logs age out of your bill, and an Athena table on top so you can answer real questions with SQL. Enable it once, and you have a queryable record of every network conversation in your VPC.&lt;/p&gt;

&lt;p&gt;This article is the full walkthrough: enabling it properly (there are three checkboxes people miss that make Athena 10x cheaper), the table DDL, and ten copy-paste queries I actually use. Grab a coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what flow logs are — and what they are not
&lt;/h2&gt;

&lt;p&gt;A flow log record is metadata about a network flow: source and destination IP, ports, protocol, bytes, packets, whether the traffic was accepted or rejected, and (in newer versions) very useful extras like which AWS service the traffic went to and which path it took to leave the VPC.&lt;/p&gt;

&lt;p&gt;What it is &lt;strong&gt;not&lt;/strong&gt;: packet capture. There's no payload, no HTTP headers, no DNS names. If you need to know &lt;em&gt;what&lt;/em&gt; was said, you need tcpdump or Traffic Mirroring. Flow logs tell you &lt;em&gt;who talked to whom, when, how much, and whether the firewall let it happen&lt;/em&gt;. In my experience that answers about 80% of the questions that reach a network team.&lt;/p&gt;

&lt;p&gt;One more thing worth knowing: records are aggregated over an interval (1 or 10 minutes), so a "flow" is a summary, not individual packets. For forensics and cost analysis that's exactly what you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why S3 instead of CloudWatch Logs
&lt;/h2&gt;

&lt;p&gt;Short version: delivery to S3 costs roughly half of CloudWatch ingestion per GB, and then storage is S3 storage — pennies, and you control it with lifecycle rules. CloudWatch keeps charging you ingestion for every GB, and flow logs are &lt;em&gt;chatty&lt;/em&gt;. On a busy VPC the difference is not academic; it's the difference between "always on everywhere" and "we only enable it during incidents", and flow logs you didn't enable before the incident are worth exactly nothing.&lt;/p&gt;

&lt;p&gt;The trade-off is losing Logs Insights and near-real-time alerting on the raw stream. For alerting, GuardDuty already analyzes flow log data internally without you enabling anything. For investigation — which is what you actually do with flow logs — Athena on S3 is better anyway: real SQL, joins, and you pay per query instead of per GB stored.&lt;/p&gt;

&lt;p&gt;Check the current numbers on the pricing page for your region before quoting them to your boss, but in every account I've run the math for, S3 wins by a wide margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling it right: the three options that matter
&lt;/h2&gt;

&lt;p&gt;You can enable flow logs at the VPC, subnet, or ENI level. Enable at the &lt;strong&gt;VPC level&lt;/strong&gt; — it covers every ENI in the VPC, including the ones that get created tomorrow.&lt;/p&gt;

&lt;p&gt;When you create the flow log (Console: VPC → your VPC → Flow logs → Create flow log), the defaults will work, and they will also quietly cost you money later. Set these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Format: Parquet, not plain text.&lt;/strong&gt; Under "Log file format", pick Parquet. Athena scans columnar Parquet dramatically more efficiently than gzipped text — AWS's own docs cite queries running faster and cheaper, and in practice I've seen scans drop to a tenth of the bytes. This single checkbox is most of your future Athena bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Hive-compatible prefixes + partition by hour.&lt;/strong&gt; Two more checkboxes ("Hive-compatible S3 prefix", "Partition logs by time"). They organize the S3 layout so Athena can skip everything outside the time window you're querying. Without partitions, every query scans your entire history; with them, "yesterday, 2pm to 4pm" scans two hours of data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Custom format with all the fields.&lt;/strong&gt; The default format is fine, but the v3–v7 fields are where the good stuff lives. Select all fields — storage is cheap and you can't add fields retroactively. The ones you'll thank yourself for later: &lt;code&gt;flow-direction&lt;/code&gt;, &lt;code&gt;traffic-path&lt;/code&gt;, &lt;code&gt;pkt-src-aws-service&lt;/code&gt;, &lt;code&gt;pkt-dst-aws-service&lt;/code&gt;, &lt;code&gt;tcp-flags&lt;/code&gt;, &lt;code&gt;az-id&lt;/code&gt;, and &lt;code&gt;pkt-srcaddr&lt;/code&gt;/&lt;code&gt;pkt-dstaddr&lt;/code&gt; (the &lt;em&gt;original&lt;/em&gt; addresses when traffic goes through a NAT gateway — without these, everything behind NAT looks like the NAT).&lt;/p&gt;

&lt;p&gt;In Terraform, the whole thing looks like this:&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_flow_log"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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;traffic_type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt;
  &lt;span class="nx"&gt;log_destination_type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;log_destination&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&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;max_aggregation_interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;   &lt;span class="c1"&gt;# 10 min; use 60 for finer forensics, ~more records&lt;/span&gt;

  &lt;span class="nx"&gt;destination_options&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;file_format&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"parquet"&lt;/span&gt;
    &lt;span class="nx"&gt;hive_compatible_partitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;per_hour_partition&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="c1"&gt;# All fields up to v7 — order matters, it defines your table schema&lt;/span&gt;
  &lt;span class="nx"&gt;log_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$${version} $${account-id} $${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${protocol} $${packets} $${bytes} $${start} $${end} $${action} $${log-status} $${vpc-id} $${subnet-id} $${instance-id} $${tcp-flags} $${type} $${pkt-srcaddr} $${pkt-dstaddr} $${region} $${az-id} $${pkt-src-aws-service} $${pkt-dst-aws-service} $${flow-direction} $${traffic-path}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bucket: policy and lifecycle
&lt;/h2&gt;

&lt;p&gt;The delivery service needs permission to write. If you create the flow log through the console pointing at your bucket, AWS adds this policy for you; in Terraform you own it:&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"flow_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"myorg-vpc-flow-logs-prd"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"flow_logs_delivery"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"delivery.logs.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${aws_s3_bucket.flow_logs.arn}/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:x-amz-acl"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bucket-owner-full-control"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"delivery.logs.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketAcl"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&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_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&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="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_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"flow_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&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;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs_delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the part everyone forgets until the storage line starts growing: &lt;strong&gt;lifecycle&lt;/strong&gt;. Decide how long flow logs are useful to you. My rule of thumb: hot for 30 days (that covers almost every investigation), cold for audit retention, gone after that. Adjust the archive tier and the expiration to your compliance requirements — in a regulated environment, that number comes from your retention policy, not from a blog post:&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_s3_bucket_lifecycle_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"flow_logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flow_logs&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;rule&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="s2"&gt;"flow-logs-aging"&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;

    &lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
      &lt;span class="nx"&gt;storage_class&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STANDARD_IA"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;
      &lt;span class="nx"&gt;storage_class&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GLACIER"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;expiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;abort_incomplete_multipart_upload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days_after_initiation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
    &lt;span class="p"&gt;}&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;One caveat: Athena can't query objects that already moved to Glacier. Your queryable window is whatever you keep in Standard/IA. That's another reason the 30/90 split works well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Athena table
&lt;/h2&gt;

&lt;p&gt;Create a workgroup first if you don't have one, set a query result location, and — do this, seriously — set a per-query data scan limit. It turns a careless &lt;code&gt;SELECT *&lt;/code&gt; over a year of logs from a bill into an error message.&lt;/p&gt;

&lt;p&gt;Then the table. Because we enabled Hive-compatible prefixes and hourly partitions, we can use &lt;strong&gt;partition projection&lt;/strong&gt; and never think about &lt;code&gt;MSCK REPAIR TABLE&lt;/code&gt; or Glue crawlers again — Athena computes the partitions from the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;version&lt;/span&gt;               &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;            &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;interface_id&lt;/span&gt;          &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;srcaddr&lt;/span&gt;               &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dstaddr&lt;/span&gt;               &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;srcport&lt;/span&gt;               &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dstport&lt;/span&gt;               &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;protocol&lt;/span&gt;              &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;packets&lt;/span&gt;               &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;bytes&lt;/span&gt;                 &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;start&lt;/span&gt;                 &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`end`&lt;/span&gt;                 &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;                &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;log_status&lt;/span&gt;            &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;vpc_id&lt;/span&gt;                &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;subnet_id&lt;/span&gt;             &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;instance_id&lt;/span&gt;           &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tcp_flags&lt;/span&gt;             &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;                  &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pkt_srcaddr&lt;/span&gt;           &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pkt_dstaddr&lt;/span&gt;           &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt;                &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;az_id&lt;/span&gt;                 &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pkt_src_aws_service&lt;/span&gt;   &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;pkt_dst_aws_service&lt;/span&gt;   &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flow_direction&lt;/span&gt;        &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;traffic_path&lt;/span&gt;          &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;`aws-account-id`&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`aws-region`&lt;/span&gt;     &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`year`&lt;/span&gt;           &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`month`&lt;/span&gt;          &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`day`&lt;/span&gt;            &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;`hour`&lt;/span&gt;           &lt;span class="n"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STORED&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;PARQUET&lt;/span&gt;
&lt;span class="k"&gt;LOCATION&lt;/span&gt; &lt;span class="s1"&gt;'s3://myorg-vpc-flow-logs-prd/AWSLogs/'&lt;/span&gt;
&lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.enabled"&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.aws-account-id.type"&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"injected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.aws-region.type"&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.aws-region.values"&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"us-east-1,sa-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.year.type"&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.year.range"&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"2024,2030"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.month.type"&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.month.range"&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"1,12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.month.digits"&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.day.type"&lt;/span&gt;                 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.day.range"&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"1,31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.day.digits"&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.hour.type"&lt;/span&gt;                &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.hour.range"&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"0,23"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"projection.hour.digits"&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"storage.location.template"&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"s3://myorg-vpc-flow-logs-prd/AWSLogs/aws-account-id=${aws-account-id}/aws-service=vpcflowlogs/aws-region=${aws-region}/year=${year}/month=${month}/day=${day}/hour=${hour}"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adjust the bucket name, the region list, and the year range. The column names match the custom format we defined — dashes become underscores in Parquet. If you used a different field order, your table must match it: the format string &lt;em&gt;is&lt;/em&gt; your schema contract.&lt;/p&gt;

&lt;p&gt;Two habits that keep queries fast and cheap: &lt;strong&gt;always filter on the partition columns&lt;/strong&gt; (&lt;code&gt;year&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;hour&lt;/code&gt; — and &lt;code&gt;aws-account-id&lt;/code&gt; if you centralize the Organization's logs in one bucket), and select only the columns you need. Parquet only reads what you ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 queries
&lt;/h2&gt;

&lt;p&gt;All of these assume you replace the date filters with your window. I'm using a &lt;code&gt;WHERE&lt;/code&gt; block like this everywhere — copy it once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- reusable time filter: adjust and paste into each query&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Who talked to this IP? (the audit question)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srcaddr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10.20.5.44'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10.20.5.44'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. What are my security groups and NACLs rejecting, and who's knocking?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rejected_flows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'REJECT'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;flow_direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ingress'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rejected_flows&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Exposure check: is anything from the internet actually reaching SSH/RDP?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3389&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACCEPT'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;flow_direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ingress'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;regexp_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^(10&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;|172&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;(1[6-9]|2[0-9]|3[01])&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;|192&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;168&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns rows you can't explain, stop reading and go fix a security group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Top talkers by volume (start of every data transfer cost investigation)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkt_dst_aws_service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;flow_direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'egress'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkt_dst_aws_service&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Who is burning the NAT Gateway?&lt;/strong&gt; (NAT processing charges are a classic FinOps finding — this names the culprits)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- interface_id = the NAT Gateway's ENI (find it in the console or via CLI)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pkt_srcaddr&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;original_source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;interface_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'eni-0abc123def456'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;flow_direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'egress'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pkt_srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;pkt_srcaddr&lt;/code&gt; — that's the field that survives NAT and shows you the actual pod or instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. East-west heavy hitters (candidates for same-AZ placement or a cost conversation)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;regexp_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^10&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;regexp_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^10&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cross-reference the top pairs with your subnet-to-AZ map: high-volume pairs crossing AZs are paying the cross-AZ tax on every byte, twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Hybrid debugging: everything crossing to on-prem&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- replace with your on-prem CIDR&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regexp_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^192&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;168&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;regexp_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^192&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;168&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_bytes&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When "the application can't reach the mainframe", this tells you in one query whether traffic is leaving, being rejected, or never arriving back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. The asymmetric NACL bug, caught red-handed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stateless NACLs need the return path open on ephemeral ports. When someone forgets, the request goes out and the &lt;em&gt;response&lt;/em&gt; gets rejected — which looks exactly like a timeout from the application side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srcport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rejected_returns&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'REJECT'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;srcport&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1433&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;-- responses FROM service ports&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;65535&lt;/span&gt;           &lt;span class="c1"&gt;-- TO ephemeral ports&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;srcport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rejected_returns&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rows here = a NACL missing its ephemeral return rule. This query has ended multi-hour debugging sessions in five minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Port scan detection (one source, many ports)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distinct_ports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;flow_direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ingress'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distinct_ports&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Connections attempted but never established (SYN with no reply)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tcp_flags&lt;/code&gt; in a flow record is the OR of the flags seen in the window. A flow showing only SYN (value 2) means somebody tried and nobody answered — great for spotting half-open scans, dead dependencies, or a service that stopped listening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;syn_only_flows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vpc_flow_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'07'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'03'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tcp_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;srcaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dstport&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;syn_only_flows&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What this setup won't do
&lt;/h2&gt;

&lt;p&gt;Flow logs won't give you DNS names (pair them with Route 53 Resolver query logs and you can join IPs to the names that were resolved — that's a future article), won't show payloads, and won't capture traffic to some AWS-internal interfaces (the docs keep a list of exclusions — instance metadata and Amazon DNS among them). And remember the Glacier caveat: your Athena window is what's still in Standard/IA.&lt;/p&gt;

&lt;p&gt;None of that reduces the value here. For the daily reality of a network team — "who talked to X", "why is this timing out", "where is the NAT bill coming from", "is anything from outside reaching port 22" — this is the highest ratio of answers-per-dollar I know on AWS.&lt;/p&gt;

&lt;p&gt;Enable it before you need it. Flow logs are like backups that way: the ones you start collecting after the incident tell you nothing about the incident.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, I write about AWS networking from the packet up — VPC, hybrid DNS, EKS networking. Follow along here or on &lt;a href="https://linkedin.com/in/rgtrovao" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
